alepha 0.20.3 → 0.20.4
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.
- package/dist/api/audits/index.d.ts.map +1 -1
- package/dist/api/files/index.d.ts.map +1 -1
- package/dist/api/jobs/index.d.ts +14 -14
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/keys/index.d.ts +4 -4
- package/dist/api/organizations/index.d.ts.map +1 -1
- package/dist/api/parameters/index.d.ts +8 -3
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/parameters/index.js +20 -4
- package/dist/api/parameters/index.js.map +1 -1
- package/dist/api/payments/index.d.ts.map +1 -1
- package/dist/api/users/index.browser.js +6 -0
- package/dist/api/users/index.browser.js.map +1 -1
- package/dist/api/users/index.d.ts +5037 -139
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +58 -10
- package/dist/api/users/index.js.map +1 -1
- package/dist/bucket/index.d.ts +77 -107
- package/dist/bucket/index.d.ts.map +1 -1
- package/dist/bucket/index.js +148 -4
- package/dist/bucket/index.js.map +1 -1
- package/dist/bucket/index.workerd.js +7 -1
- package/dist/bucket/index.workerd.js.map +1 -1
- package/dist/cache/core/index.d.ts +26 -0
- package/dist/cache/core/index.d.ts.map +1 -1
- package/dist/cache/core/index.js +11 -1
- package/dist/cache/core/index.js.map +1 -1
- package/dist/cache/core/index.workerd.js +11 -1
- package/dist/cache/core/index.workerd.js.map +1 -1
- package/dist/cli/config/index.d.ts +7 -5
- package/dist/cli/config/index.d.ts.map +1 -1
- package/dist/cli/config/index.js +2 -3
- package/dist/cli/config/index.js.map +1 -1
- package/dist/cli/core/index.d.ts +420 -13
- package/dist/cli/core/index.d.ts.map +1 -1
- package/dist/cli/core/index.js +22 -511
- package/dist/cli/core/index.js.map +1 -1
- package/dist/cli/devtools/index.d.ts +4 -8
- package/dist/cli/devtools/index.d.ts.map +1 -1
- package/dist/cli/devtools/index.js +13 -15
- package/dist/cli/devtools/index.js.map +1 -1
- package/dist/cli/platform/index.d.ts +10 -13
- package/dist/cli/platform/index.d.ts.map +1 -1
- package/dist/cli/platform/index.js +18 -15
- package/dist/cli/platform/index.js.map +1 -1
- package/dist/cli/vendor/index.d.ts +10 -13
- package/dist/cli/vendor/index.d.ts.map +1 -1
- package/dist/cli/vendor/index.js +16 -13
- package/dist/cli/vendor/index.js.map +1 -1
- package/dist/core/index.browser.js +27 -3
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +6 -3
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +27 -3
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +27 -3
- package/dist/core/index.native.js.map +1 -1
- package/dist/core/index.workerd.js +27 -3
- package/dist/core/index.workerd.js.map +1 -1
- package/dist/datetime/index.d.ts +69 -10
- package/dist/datetime/index.d.ts.map +1 -1
- package/dist/datetime/index.js +135 -13
- package/dist/datetime/index.js.map +1 -1
- package/dist/email/smtp/index.js +10636 -2
- package/dist/email/smtp/index.js.map +1 -1
- package/dist/fake/index.d.ts +8085 -4
- package/dist/fake/index.d.ts.map +1 -1
- package/dist/fake/index.js +33554 -3
- package/dist/fake/index.js.map +1 -1
- package/dist/lock/core/index.d.ts +30 -2
- package/dist/lock/core/index.d.ts.map +1 -1
- package/dist/lock/core/index.js +35 -12
- package/dist/lock/core/index.js.map +1 -1
- package/dist/mcp/index.d.ts +238 -31
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +198 -71
- package/dist/mcp/index.js.map +1 -1
- package/dist/orm/core/index.browser.js +1 -1
- package/dist/orm/core/index.browser.js.map +1 -1
- package/dist/orm/core/index.bun.js +4 -3
- package/dist/orm/core/index.bun.js.map +1 -1
- package/dist/orm/core/index.d.ts +4877 -9
- package/dist/orm/core/index.d.ts.map +1 -1
- package/dist/orm/core/index.js +4 -3
- package/dist/orm/core/index.js.map +1 -1
- package/dist/orm/postgres/index.d.ts +608 -1
- package/dist/orm/postgres/index.d.ts.map +1 -1
- package/dist/react/core/index.d.ts +102 -1
- package/dist/react/core/index.d.ts.map +1 -1
- package/dist/react/core/index.js +65 -1
- package/dist/react/core/index.js.map +1 -1
- package/dist/react/form/index.d.ts +6 -0
- package/dist/react/form/index.d.ts.map +1 -1
- package/dist/react/form/index.js +7 -7
- package/dist/react/form/index.js.map +1 -1
- package/dist/react/i18n/index.d.ts +7 -1
- package/dist/react/i18n/index.d.ts.map +1 -1
- package/dist/react/i18n/index.js +6 -0
- package/dist/react/i18n/index.js.map +1 -1
- package/dist/react/router/index.browser.js +20 -2
- package/dist/react/router/index.browser.js.map +1 -1
- package/dist/react/router/index.d.ts +36 -4
- package/dist/react/router/index.d.ts.map +1 -1
- package/dist/react/router/index.js +20 -2
- package/dist/react/router/index.js.map +1 -1
- package/dist/react/testing/chunk-6Ep1yQYe.js +16 -0
- package/dist/react/testing/index.d.ts +411 -1
- package/dist/react/testing/index.d.ts.map +1 -1
- package/dist/react/testing/index.js +12293 -13
- package/dist/react/testing/index.js.map +1 -1
- package/dist/react/ui/index.d.ts +195 -1
- package/dist/react/ui/index.d.ts.map +1 -1
- package/dist/react/ui/index.js +61 -1
- package/dist/react/ui/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +84 -3
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +390 -1
- package/dist/scheduler/index.js.map +1 -1
- package/dist/scheduler/index.workerd.js +390 -1
- package/dist/scheduler/index.workerd.js.map +1 -1
- package/dist/security/index.d.ts +325 -2
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +1361 -2
- package/dist/security/index.js.map +1 -1
- package/dist/server/auth/index.d.ts +1054 -1
- package/dist/server/auth/index.d.ts.map +1 -1
- package/dist/server/auth/index.js +1223 -1
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/core/index.browser.js +10 -3
- package/dist/server/core/index.browser.js.map +1 -1
- package/dist/server/core/index.d.ts.map +1 -1
- package/dist/server/core/index.js +28 -5
- package/dist/server/core/index.js.map +1 -1
- package/dist/server/metrics/index.d.ts +514 -1
- package/dist/server/metrics/index.d.ts.map +1 -1
- package/dist/server/metrics/index.js +4374 -4
- package/dist/server/metrics/index.js.map +1 -1
- package/dist/server/swagger/index.d.ts.map +1 -1
- package/dist/server/swagger/index.js +3 -4
- package/dist/server/swagger/index.js.map +1 -1
- package/dist/websocket/index.browser.js +11 -5
- package/dist/websocket/index.browser.js.map +1 -1
- package/dist/websocket/index.d.ts +3 -1
- package/dist/websocket/index.d.ts.map +1 -1
- package/dist/websocket/index.js +21 -6
- package/dist/websocket/index.js.map +1 -1
- package/package.json +671 -263
- package/src/api/parameters/services/ParameterProvider.ts +21 -4
- package/src/api/users/__tests__/SessionService.spec.ts +99 -0
- package/src/api/users/__tests__/UserJobs.spec.ts +67 -0
- package/src/api/users/atoms/realmAuthSettingsAtom.ts +15 -0
- package/src/api/users/entities/sessions.ts +6 -0
- package/src/api/users/jobs/UserJobs.ts +44 -17
- package/src/api/users/providers/RealmProvider.ts +4 -0
- package/src/api/users/services/SessionService.ts +27 -0
- package/src/bucket/__tests__/NodeS3BucketProvider.spec.ts +74 -0
- package/src/bucket/index.ts +19 -2
- package/src/bucket/primitives/$bucket.ts +9 -1
- package/src/bucket/providers/CloudflareR2Provider.ts +2 -137
- package/src/bucket/providers/NodeS3BucketProvider.ts +218 -0
- package/src/cache/core/index.ts +29 -0
- package/src/cache/core/primitives/$cache.ts +14 -1
- package/src/cli/config/defineConfig.ts +13 -15
- package/src/cli/core/__tests__/init.spec.ts +6 -7
- package/src/cli/core/services/ProjectScaffolder.ts +18 -14
- package/src/cli/core/tasks/BuildCloudflareTask.ts +5 -0
- package/src/cli/core/templates/agentMd.ts +2 -10
- package/src/cli/core/templates/saasAdminLayoutTsx.ts +3 -3
- package/src/cli/devtools/index.ts +12 -26
- package/src/cli/platform/index.ts +15 -24
- package/src/cli/vendor/atoms/vendorOptions.ts +1 -1
- package/src/cli/vendor/index.ts +14 -23
- package/src/core/Alepha.ts +11 -1
- package/src/core/helpers/ref.ts +18 -0
- package/src/core/index.shared.ts +1 -0
- package/src/core/providers/SchemaValidator.ts +9 -1
- package/src/core/providers/TypeProvider.ts +1 -2
- package/src/datetime/REFACTORING.md +118 -0
- package/src/datetime/providers/DateTimeProvider.ts +203 -24
- package/src/lock/core/index.ts +31 -0
- package/src/lock/core/primitives/$lock.ts +14 -1
- package/src/mcp/__tests__/jsonrpc.spec.ts +1 -1
- package/src/mcp/helpers/jsonrpc.ts +26 -1
- package/src/mcp/index.ts +10 -5
- package/src/mcp/interfaces/McpTypes.ts +83 -6
- package/src/mcp/primitives/$prompt.ts +18 -1
- package/src/mcp/primitives/$resource.ts +18 -1
- package/src/mcp/primitives/$tool.ts +83 -7
- package/src/mcp/providers/McpServerProvider.ts +74 -16
- package/src/mcp/transports/StreamableHttpMcpTransport.ts +226 -0
- package/src/orm/REFACTORING.md +330 -0
- package/src/orm/core/primitives/$transactional.ts +11 -0
- package/src/orm/core/schemas/updateSchema.ts +1 -1
- package/src/orm/core/services/PgRelationManager.ts +4 -2
- package/src/react/core/__tests__/useQuery.browser.spec.tsx +86 -0
- package/src/react/core/hooks/useQuery.ts +153 -0
- package/src/react/core/index.ts +1 -0
- package/src/react/form/services/FormModel.ts +15 -6
- package/src/react/form/services/parseField.ts +8 -0
- package/src/react/i18n/providers/I18nProvider.ts +8 -2
- package/src/react/router/__tests__/$page.spec.tsx +0 -16
- package/src/react/router/__tests__/ssr.spec.tsx +339 -0
- package/src/react/router/primitives/$page.ts +28 -4
- package/src/react/router/providers/ReactPageProvider.ts +27 -9
- package/src/react/ui/atoms/uiThemeListAtom.ts +36 -0
- package/src/react/ui/index.ts +6 -0
- package/src/react/ui/services/SchemaControl.ts +209 -0
- package/src/security/primitives/$issuer.ts +6 -3
- package/src/server/core/__tests__/ServerRouterProvider-serializationError.spec.ts +75 -0
- package/src/server/core/__tests__/ServerRouterProvider-validationError.spec.ts +306 -0
- package/src/server/core/errors/ValidationError.ts +13 -1
- package/src/server/core/primitives/$action.ts +16 -5
- package/src/server/core/providers/ServerRouterProvider.ts +26 -4
- package/src/server/swagger/providers/ServerSwaggerProvider.ts +5 -7
- package/src/websocket/providers/NodeWebSocketServerProvider.ts +10 -4
- package/src/websocket/services/WebSocketClient.ts +11 -5
- package/src/mcp/transports/SseMcpTransport.ts +0 -182
|
@@ -45,6 +45,12 @@ export interface FieldMeta {
|
|
|
45
45
|
constraints: FieldConstraints;
|
|
46
46
|
testId?: string;
|
|
47
47
|
schema: TSchema;
|
|
48
|
+
/**
|
|
49
|
+
* Raw `$control` value from the schema, untyped here. The UI layer
|
|
50
|
+
* (`alepha/react/ui`) provides the strict {@link SchemaControl} type and
|
|
51
|
+
* a `resolveSchemaControl` helper to evaluate the function form.
|
|
52
|
+
*/
|
|
53
|
+
control?: unknown;
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
export interface ParseFieldOptions {
|
|
@@ -77,6 +83,7 @@ export const parseField = (
|
|
|
77
83
|
pattern?: string;
|
|
78
84
|
properties?: unknown;
|
|
79
85
|
items?: { properties?: unknown };
|
|
86
|
+
$control?: unknown;
|
|
80
87
|
};
|
|
81
88
|
|
|
82
89
|
const label =
|
|
@@ -132,6 +139,7 @@ export const parseField = (
|
|
|
132
139
|
| string
|
|
133
140
|
| undefined,
|
|
134
141
|
schema: input.schema,
|
|
142
|
+
control: schema.$control,
|
|
135
143
|
};
|
|
136
144
|
};
|
|
137
145
|
|
|
@@ -230,15 +230,21 @@ export class I18nProvider<
|
|
|
230
230
|
return value;
|
|
231
231
|
};
|
|
232
232
|
|
|
233
|
+
/**
|
|
234
|
+
* Look up `key` in the registered dictionaries. The `(string & {})` arm
|
|
235
|
+
* keeps autocomplete for the typed dictionary keys while allowing shared
|
|
236
|
+
* library components to pass arbitrary string keys (with a `default`
|
|
237
|
+
* fallback) without casting to `as never`.
|
|
238
|
+
*/
|
|
233
239
|
public readonly tr = (
|
|
234
|
-
key: keyof ServiceDictionary<S>[K],
|
|
240
|
+
key: keyof ServiceDictionary<S>[K] | (string & {}),
|
|
235
241
|
options: {
|
|
236
242
|
args?: string[];
|
|
237
243
|
default?: string;
|
|
238
244
|
} = {},
|
|
239
245
|
) => {
|
|
240
246
|
const translation = this.translate(key as string, options.args || []);
|
|
241
|
-
if (translation === key && options.default) {
|
|
247
|
+
if (translation === (key as string) && options.default) {
|
|
242
248
|
return options.default;
|
|
243
249
|
}
|
|
244
250
|
return translation;
|
|
@@ -419,22 +419,6 @@ describe("$page primitive tests", () => {
|
|
|
419
419
|
expect(await app.staticPage.fetch().then((it) => it.html)).toBe(html);
|
|
420
420
|
});
|
|
421
421
|
|
|
422
|
-
test("$page - client-side only rendering", async ({ expect }) => {
|
|
423
|
-
class App {
|
|
424
|
-
clientOnly = $page({
|
|
425
|
-
path: "/client",
|
|
426
|
-
client: true,
|
|
427
|
-
component: () => "Client only",
|
|
428
|
-
});
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
const app = alepha.inject(App);
|
|
432
|
-
await alepha.start();
|
|
433
|
-
|
|
434
|
-
const clientRendered = await app.clientOnly.fetch();
|
|
435
|
-
expect(clientRendered.html).toBe("");
|
|
436
|
-
});
|
|
437
|
-
|
|
438
422
|
test("$page - server response handler", async ({ expect }) => {
|
|
439
423
|
const mockHandler = vi.fn();
|
|
440
424
|
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { Alepha } from "alepha";
|
|
2
|
+
import { beforeEach, describe, test } from "vitest";
|
|
3
|
+
import { $page, NestedView } from "../index.ts";
|
|
4
|
+
|
|
5
|
+
describe("$page ssr option", () => {
|
|
6
|
+
let alepha: Alepha;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
alepha = Alepha.create();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("default (no ssr option) renders server-side", async ({ expect }) => {
|
|
13
|
+
class App {
|
|
14
|
+
home = $page({
|
|
15
|
+
path: "/",
|
|
16
|
+
component: () => "Home content",
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const app = alepha.inject(App);
|
|
21
|
+
await alepha.start();
|
|
22
|
+
|
|
23
|
+
const rendered = await app.home.render();
|
|
24
|
+
expect(rendered.html).toBe("Home content");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("ssr: true renders server-side", async ({ expect }) => {
|
|
28
|
+
class App {
|
|
29
|
+
home = $page({
|
|
30
|
+
path: "/",
|
|
31
|
+
ssr: true,
|
|
32
|
+
component: () => "Home content",
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const app = alepha.inject(App);
|
|
37
|
+
await alepha.start();
|
|
38
|
+
|
|
39
|
+
const rendered = await app.home.render();
|
|
40
|
+
expect(rendered.html).toBe("Home content");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("ssr: false skips server rendering on a leaf page", async ({
|
|
44
|
+
expect,
|
|
45
|
+
}) => {
|
|
46
|
+
class App {
|
|
47
|
+
home = $page({
|
|
48
|
+
path: "/",
|
|
49
|
+
ssr: false,
|
|
50
|
+
component: () => "Home content",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const app = alepha.inject(App);
|
|
55
|
+
await alepha.start();
|
|
56
|
+
|
|
57
|
+
const rendered = await app.home.render();
|
|
58
|
+
expect(rendered.html).toBe("");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("ssr: false still runs the loader server-side", async ({ expect }) => {
|
|
62
|
+
let loaderCalls = 0;
|
|
63
|
+
|
|
64
|
+
class App {
|
|
65
|
+
home = $page({
|
|
66
|
+
path: "/",
|
|
67
|
+
ssr: false,
|
|
68
|
+
loader: () => {
|
|
69
|
+
loaderCalls += 1;
|
|
70
|
+
return { msg: "loaded" };
|
|
71
|
+
},
|
|
72
|
+
component: ({ msg }: { msg: string }) => msg,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const app = alepha.inject(App);
|
|
77
|
+
await alepha.start();
|
|
78
|
+
|
|
79
|
+
const rendered = await app.home.render();
|
|
80
|
+
expect(rendered.html).toBe("");
|
|
81
|
+
expect(loaderCalls).toBe(1);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("parent ssr: false cascades to child without override", async ({
|
|
85
|
+
expect,
|
|
86
|
+
}) => {
|
|
87
|
+
class App {
|
|
88
|
+
parent = $page({
|
|
89
|
+
path: "/parent",
|
|
90
|
+
ssr: false,
|
|
91
|
+
component: () => (
|
|
92
|
+
<>
|
|
93
|
+
Parent
|
|
94
|
+
<NestedView />
|
|
95
|
+
</>
|
|
96
|
+
),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
child = $page({
|
|
100
|
+
path: "/child",
|
|
101
|
+
parent: this.parent,
|
|
102
|
+
component: () => "Child",
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const app = alepha.inject(App);
|
|
107
|
+
await alepha.start();
|
|
108
|
+
|
|
109
|
+
const rendered = await app.child.render();
|
|
110
|
+
expect(rendered.html).toBe("");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("child ssr: true overrides parent ssr: false (both render)", async ({
|
|
114
|
+
expect,
|
|
115
|
+
}) => {
|
|
116
|
+
class App {
|
|
117
|
+
parent = $page({
|
|
118
|
+
path: "/parent",
|
|
119
|
+
ssr: false,
|
|
120
|
+
component: () => (
|
|
121
|
+
<>
|
|
122
|
+
Parent
|
|
123
|
+
<NestedView />
|
|
124
|
+
</>
|
|
125
|
+
),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
child = $page({
|
|
129
|
+
path: "/child",
|
|
130
|
+
parent: this.parent,
|
|
131
|
+
ssr: true,
|
|
132
|
+
component: () => "Child",
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const app = alepha.inject(App);
|
|
137
|
+
await alepha.start();
|
|
138
|
+
|
|
139
|
+
const rendered = await app.child.render();
|
|
140
|
+
expect(rendered.html).toBe("Parent<!-- -->Child");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("child ssr: false overrides parent ssr: true (skip everything)", async ({
|
|
144
|
+
expect,
|
|
145
|
+
}) => {
|
|
146
|
+
class App {
|
|
147
|
+
parent = $page({
|
|
148
|
+
path: "/parent",
|
|
149
|
+
ssr: true,
|
|
150
|
+
component: () => (
|
|
151
|
+
<>
|
|
152
|
+
Parent
|
|
153
|
+
<NestedView />
|
|
154
|
+
</>
|
|
155
|
+
),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
child = $page({
|
|
159
|
+
path: "/child",
|
|
160
|
+
parent: this.parent,
|
|
161
|
+
ssr: false,
|
|
162
|
+
component: () => "Child",
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const app = alepha.inject(App);
|
|
167
|
+
await alepha.start();
|
|
168
|
+
|
|
169
|
+
const rendered = await app.child.render();
|
|
170
|
+
expect(rendered.html).toBe("");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("siblings: one inherits parent ssr: false, the other overrides", async ({
|
|
174
|
+
expect,
|
|
175
|
+
}) => {
|
|
176
|
+
class App {
|
|
177
|
+
parent = $page({
|
|
178
|
+
path: "/parent",
|
|
179
|
+
ssr: false,
|
|
180
|
+
component: () => (
|
|
181
|
+
<>
|
|
182
|
+
Parent
|
|
183
|
+
<NestedView />
|
|
184
|
+
</>
|
|
185
|
+
),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
home = $page({
|
|
189
|
+
path: "/home",
|
|
190
|
+
parent: this.parent,
|
|
191
|
+
ssr: true,
|
|
192
|
+
component: () => "Home",
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
about = $page({
|
|
196
|
+
path: "/about",
|
|
197
|
+
parent: this.parent,
|
|
198
|
+
component: () => "About",
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const app = alepha.inject(App);
|
|
203
|
+
await alepha.start();
|
|
204
|
+
|
|
205
|
+
const home = await app.home.render();
|
|
206
|
+
expect(home.html).toBe("Parent<!-- -->Home");
|
|
207
|
+
|
|
208
|
+
const about = await app.about.render();
|
|
209
|
+
expect(about.html).toBe("");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("3-level chain: nearest explicit ssr wins (leaf decides)", async ({
|
|
213
|
+
expect,
|
|
214
|
+
}) => {
|
|
215
|
+
class App {
|
|
216
|
+
grand = $page({
|
|
217
|
+
path: "/grand",
|
|
218
|
+
ssr: false,
|
|
219
|
+
component: () => (
|
|
220
|
+
<>
|
|
221
|
+
Grand
|
|
222
|
+
<NestedView />
|
|
223
|
+
</>
|
|
224
|
+
),
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// no ssr → inherits from grand → false (default for descendants)
|
|
228
|
+
mid = $page({
|
|
229
|
+
path: "/mid",
|
|
230
|
+
parent: this.grand,
|
|
231
|
+
component: () => (
|
|
232
|
+
<>
|
|
233
|
+
Mid
|
|
234
|
+
<NestedView />
|
|
235
|
+
</>
|
|
236
|
+
),
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// explicit ssr: true → overrides inherited false
|
|
240
|
+
leaf = $page({
|
|
241
|
+
path: "/leaf",
|
|
242
|
+
parent: this.mid,
|
|
243
|
+
ssr: true,
|
|
244
|
+
component: () => "Leaf",
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const app = alepha.inject(App);
|
|
249
|
+
await alepha.start();
|
|
250
|
+
|
|
251
|
+
const rendered = await app.leaf.render();
|
|
252
|
+
expect(rendered.html).toBe("Grand<!-- -->Mid<!-- -->Leaf");
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("3-level chain: middle ssr: true does not affect leaf without explicit value", async ({
|
|
256
|
+
expect,
|
|
257
|
+
}) => {
|
|
258
|
+
class App {
|
|
259
|
+
grand = $page({
|
|
260
|
+
path: "/grand",
|
|
261
|
+
ssr: false,
|
|
262
|
+
component: () => (
|
|
263
|
+
<>
|
|
264
|
+
Grand
|
|
265
|
+
<NestedView />
|
|
266
|
+
</>
|
|
267
|
+
),
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// explicit true overrides grand's false for itself + descendants default
|
|
271
|
+
mid = $page({
|
|
272
|
+
path: "/mid",
|
|
273
|
+
parent: this.grand,
|
|
274
|
+
ssr: true,
|
|
275
|
+
component: () => (
|
|
276
|
+
<>
|
|
277
|
+
Mid
|
|
278
|
+
<NestedView />
|
|
279
|
+
</>
|
|
280
|
+
),
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
// no ssr → walks up: mid.ssr === true → leaf renders
|
|
284
|
+
leaf = $page({
|
|
285
|
+
path: "/leaf",
|
|
286
|
+
parent: this.mid,
|
|
287
|
+
component: () => "Leaf",
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const app = alepha.inject(App);
|
|
292
|
+
await alepha.start();
|
|
293
|
+
|
|
294
|
+
const rendered = await app.leaf.render();
|
|
295
|
+
expect(rendered.html).toBe("Grand<!-- -->Mid<!-- -->Leaf");
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("parent ssr: false: loaders still run for parent and child", async ({
|
|
299
|
+
expect,
|
|
300
|
+
}) => {
|
|
301
|
+
let parentLoaderCalls = 0;
|
|
302
|
+
let childLoaderCalls = 0;
|
|
303
|
+
|
|
304
|
+
class App {
|
|
305
|
+
parent = $page({
|
|
306
|
+
path: "/parent",
|
|
307
|
+
ssr: false,
|
|
308
|
+
loader: () => {
|
|
309
|
+
parentLoaderCalls += 1;
|
|
310
|
+
return { fromParent: "p" };
|
|
311
|
+
},
|
|
312
|
+
component: () => (
|
|
313
|
+
<>
|
|
314
|
+
Parent
|
|
315
|
+
<NestedView />
|
|
316
|
+
</>
|
|
317
|
+
),
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
child = $page({
|
|
321
|
+
path: "/child",
|
|
322
|
+
parent: this.parent,
|
|
323
|
+
loader: ({ fromParent }) => {
|
|
324
|
+
childLoaderCalls += 1;
|
|
325
|
+
return { fromChild: `${fromParent}-c` };
|
|
326
|
+
},
|
|
327
|
+
component: ({ fromChild }: { fromChild: string }) => fromChild,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const app = alepha.inject(App);
|
|
332
|
+
await alepha.start();
|
|
333
|
+
|
|
334
|
+
const rendered = await app.child.render();
|
|
335
|
+
expect(rendered.html).toBe("");
|
|
336
|
+
expect(parentLoaderCalls).toBe(1);
|
|
337
|
+
expect(childLoaderCalls).toBe(1);
|
|
338
|
+
});
|
|
339
|
+
});
|
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
type TSchema,
|
|
11
11
|
} from "alepha";
|
|
12
12
|
import { $cache } from "alepha/cache";
|
|
13
|
-
import type { ClientOnlyProps } from "alepha/react";
|
|
14
13
|
import type { Head } from "alepha/react/head";
|
|
15
14
|
import type { ServerRequest } from "alepha/server";
|
|
16
15
|
import type { FC, ReactNode } from "react";
|
|
@@ -297,10 +296,35 @@ export interface PagePrimitiveOptions<
|
|
|
297
296
|
};
|
|
298
297
|
|
|
299
298
|
/**
|
|
300
|
-
*
|
|
301
|
-
*
|
|
299
|
+
* Enable or disable server-side rendering for this page.
|
|
300
|
+
*
|
|
301
|
+
* - `true` (default): the page component is rendered on the server and
|
|
302
|
+
* hydrated on the client.
|
|
303
|
+
* - `false`: the loader still runs on the server (so data is preloaded and
|
|
304
|
+
* serialized for hydration), but the component is rendered only on the
|
|
305
|
+
* client. The server emits no HTML for this page.
|
|
306
|
+
*
|
|
307
|
+
* **Decided at the leaf, inherited as default by descendants.**
|
|
308
|
+
*
|
|
309
|
+
* The effective value is determined by the matched leaf page: walk up the
|
|
310
|
+
* parent chain and use the nearest explicit `ssr` value. Setting
|
|
311
|
+
* `ssr: false` on a parent therefore acts as the default for its children;
|
|
312
|
+
* a child can override with `ssr: true`.
|
|
313
|
+
*
|
|
314
|
+
* Skipping rendering while keeping the loader is the recommended strategy
|
|
315
|
+
* for CPU-constrained server environments (e.g. Cloudflare Workers) and
|
|
316
|
+
* heavy admin/dashboard views where SSR provides little SEO value.
|
|
317
|
+
*
|
|
318
|
+
* @example
|
|
319
|
+
* ```ts
|
|
320
|
+
* root = $page({ ssr: false }); // default for children
|
|
321
|
+
* home = $page({ parent: root, ssr: true }); // overrides → SSR
|
|
322
|
+
* about = $page({ parent: root }); // inherits → no SSR
|
|
323
|
+
* ```
|
|
324
|
+
*
|
|
325
|
+
* @default true
|
|
302
326
|
*/
|
|
303
|
-
|
|
327
|
+
ssr?: boolean;
|
|
304
328
|
|
|
305
329
|
/**
|
|
306
330
|
* Called before the server response is sent to the client. (server only)
|
|
@@ -470,6 +470,14 @@ export class ReactPageProvider {
|
|
|
470
470
|
}
|
|
471
471
|
}
|
|
472
472
|
|
|
473
|
+
// If the matched leaf opts out of SSR (own value or inherited from
|
|
474
|
+
// parents), wrap the root layer in ClientOnly so the server emits no
|
|
475
|
+
// HTML for the route chain. Loaders have already run above.
|
|
476
|
+
if (state.layers.length > 0 && !this.isSSR(route)) {
|
|
477
|
+
const rootLayer = state.layers[0];
|
|
478
|
+
rootLayer.element = createElement(ClientOnly, {}, rootLayer.element);
|
|
479
|
+
}
|
|
480
|
+
|
|
473
481
|
return { state };
|
|
474
482
|
}
|
|
475
483
|
|
|
@@ -598,14 +606,6 @@ export class ReactPageProvider {
|
|
|
598
606
|
): ReactNode {
|
|
599
607
|
view ??= this.renderEmptyView();
|
|
600
608
|
|
|
601
|
-
const element = page.client
|
|
602
|
-
? createElement(
|
|
603
|
-
ClientOnly,
|
|
604
|
-
typeof page.client === "object" ? page.client : {},
|
|
605
|
-
view,
|
|
606
|
-
)
|
|
607
|
-
: view;
|
|
608
|
-
|
|
609
609
|
return createElement(
|
|
610
610
|
RouterLayerContext.Provider,
|
|
611
611
|
{
|
|
@@ -616,10 +616,28 @@ export class ReactPageProvider {
|
|
|
616
616
|
this.getErrorHandler(page) ?? ((error) => this.renderError(error)),
|
|
617
617
|
},
|
|
618
618
|
},
|
|
619
|
-
|
|
619
|
+
view,
|
|
620
620
|
);
|
|
621
621
|
}
|
|
622
622
|
|
|
623
|
+
/**
|
|
624
|
+
* Resolve the effective `ssr` value for a route by walking up the parent
|
|
625
|
+
* chain. Returns the nearest explicit `ssr` value, defaulting to `true`.
|
|
626
|
+
*
|
|
627
|
+
* The decision is made at the leaf: a parent's `ssr` only acts as a default
|
|
628
|
+
* for descendants that did not set their own value.
|
|
629
|
+
*/
|
|
630
|
+
public isSSR(route: PageRoute): boolean {
|
|
631
|
+
let current: PageRoute | undefined = route;
|
|
632
|
+
while (current) {
|
|
633
|
+
if (typeof current.ssr === "boolean") {
|
|
634
|
+
return current.ssr;
|
|
635
|
+
}
|
|
636
|
+
current = current.parent;
|
|
637
|
+
}
|
|
638
|
+
return true;
|
|
639
|
+
}
|
|
640
|
+
|
|
623
641
|
protected map(
|
|
624
642
|
pages: Array<PagePrimitive>,
|
|
625
643
|
target: PagePrimitive,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { $atom, type Static, t } from "alepha";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Available themes the user can pick from. Apps populate this atom on boot
|
|
5
|
+
* (e.g. `alepha.store.set(uiThemeListAtom, MY_THEMES)`); UI consumers like
|
|
6
|
+
* `<ButtonTheme/>` read it to render a picker. The selected theme id is
|
|
7
|
+
* persisted separately in `uiAtom.theme`.
|
|
8
|
+
*
|
|
9
|
+
* Defaults to a single `"default"` entry so the registry stays usable when
|
|
10
|
+
* an app doesn't declare its own list.
|
|
11
|
+
*/
|
|
12
|
+
export const uiThemeListAtom = $atom({
|
|
13
|
+
name: "alepha.react.ui.themes",
|
|
14
|
+
schema: t.array(
|
|
15
|
+
t.object({
|
|
16
|
+
/** Stable id stored in `uiAtom.theme`. Mapped to a CSS class on `<html>`. */
|
|
17
|
+
id: t.string(),
|
|
18
|
+
/** Human-readable label shown in the picker. */
|
|
19
|
+
label: t.string(),
|
|
20
|
+
/**
|
|
21
|
+
* Optional 4-color preview swatch in 2×2 order (TL, TR, BL, BR). Any
|
|
22
|
+
* CSS-valid color string.
|
|
23
|
+
*/
|
|
24
|
+
swatch: t.optional(t.array(t.string(), { minItems: 4, maxItems: 4 })),
|
|
25
|
+
/**
|
|
26
|
+
* Optional stylesheet URL (typically Google Fonts) loaded lazily when
|
|
27
|
+
* the theme is selected.
|
|
28
|
+
*/
|
|
29
|
+
fontHref: t.optional(t.string()),
|
|
30
|
+
}),
|
|
31
|
+
),
|
|
32
|
+
default: [{ id: "default", label: "Default" }],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
export type UiThemeList = Static<typeof uiThemeListAtom.schema>;
|
|
36
|
+
export type UiTheme = UiThemeList[number];
|
package/src/react/ui/index.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { $module } from "alepha";
|
|
2
2
|
import type { UiState } from "./atoms/uiAtom.ts";
|
|
3
|
+
import type { UiThemeList } from "./atoms/uiThemeListAtom.ts";
|
|
4
|
+
import { uiThemeListAtom } from "./atoms/uiThemeListAtom.ts";
|
|
3
5
|
import { UiPersistence } from "./services/UiPersistence.ts";
|
|
4
6
|
|
|
5
7
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
6
8
|
|
|
7
9
|
export * from "./atoms/uiAtom.ts";
|
|
10
|
+
export * from "./atoms/uiThemeListAtom.ts";
|
|
8
11
|
export * from "./components/ColorScheme.tsx";
|
|
9
12
|
export * from "./hooks/useColorMode.ts";
|
|
10
13
|
export * from "./hooks/useSidebarState.ts";
|
|
11
14
|
export * from "./hooks/useTheme.ts";
|
|
15
|
+
export * from "./services/SchemaControl.ts";
|
|
12
16
|
export * from "./services/UiPersistence.ts";
|
|
13
17
|
|
|
14
18
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
@@ -16,6 +20,7 @@ export * from "./services/UiPersistence.ts";
|
|
|
16
20
|
declare module "alepha" {
|
|
17
21
|
export interface State {
|
|
18
22
|
"alepha.react.ui": UiState;
|
|
23
|
+
"alepha.react.ui.themes": UiThemeList;
|
|
19
24
|
}
|
|
20
25
|
}
|
|
21
26
|
|
|
@@ -31,5 +36,6 @@ declare module "alepha" {
|
|
|
31
36
|
*/
|
|
32
37
|
export const AlephaReactUi = $module({
|
|
33
38
|
name: "alepha.react.ui",
|
|
39
|
+
atoms: [uiThemeListAtom],
|
|
34
40
|
services: [UiPersistence],
|
|
35
41
|
});
|