alepha 0.20.1 → 0.20.2
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/files/index.js +2 -1
- package/dist/api/files/index.js.map +1 -1
- package/dist/api/jobs/index.browser.js +64 -148
- package/dist/api/jobs/index.browser.js.map +1 -1
- package/dist/api/jobs/index.d.ts +371 -573
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/jobs/index.js +605 -1012
- package/dist/api/jobs/index.js.map +1 -1
- package/dist/api/notifications/index.d.ts +78 -17
- package/dist/api/notifications/index.d.ts.map +1 -1
- package/dist/api/notifications/index.js +90 -23
- package/dist/api/notifications/index.js.map +1 -1
- package/dist/api/payments/index.d.ts +2 -1
- package/dist/api/payments/index.d.ts.map +1 -1
- package/dist/api/payments/index.js +4 -2
- package/dist/api/payments/index.js.map +1 -1
- package/dist/api/users/index.d.ts +34 -31
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +13 -7
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/verifications/index.js +2 -1
- package/dist/api/verifications/index.js.map +1 -1
- package/dist/cli/core/index.d.ts +8 -34
- package/dist/cli/core/index.d.ts.map +1 -1
- package/dist/cli/core/index.js +43 -232
- package/dist/cli/core/index.js.map +1 -1
- package/dist/cli/platform/index.d.ts +36 -11
- package/dist/cli/platform/index.d.ts.map +1 -1
- package/dist/cli/platform/index.js +93 -27
- package/dist/cli/platform/index.js.map +1 -1
- package/dist/command/index.d.ts +1 -1
- package/dist/core/index.browser.js +6 -0
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +6 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +6 -0
- package/dist/core/index.native.js.map +1 -1
- package/dist/core/index.workerd.js +6 -0
- package/dist/core/index.workerd.js.map +1 -1
- package/dist/react/form/index.d.ts +60 -1
- package/dist/react/form/index.d.ts.map +1 -1
- package/dist/react/form/index.js +86 -1
- package/dist/react/form/index.js.map +1 -1
- package/dist/react/head/index.browser.js +16 -1
- package/dist/react/head/index.browser.js.map +1 -1
- package/dist/react/head/index.d.ts +6 -0
- package/dist/react/head/index.d.ts.map +1 -1
- package/dist/react/head/index.js +16 -1
- package/dist/react/head/index.js.map +1 -1
- package/dist/react/router/index.browser.js +0 -10
- package/dist/react/router/index.browser.js.map +1 -1
- package/dist/react/router/index.d.ts +35 -12
- package/dist/react/router/index.d.ts.map +1 -1
- package/dist/react/router/index.js +0 -10
- package/dist/react/router/index.js.map +1 -1
- package/dist/react/ui/index.d.ts +124 -0
- package/dist/react/ui/index.d.ts.map +1 -0
- package/dist/react/ui/index.js +206 -0
- package/dist/react/ui/index.js.map +1 -0
- package/dist/router/index.d.ts +13 -13
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +45 -32
- package/dist/router/index.js.map +1 -1
- package/dist/system/index.d.ts.map +1 -1
- package/dist/system/index.js +1 -0
- package/dist/system/index.js.map +1 -1
- package/dist/topic/core/index.js +1 -1
- package/dist/topic/core/index.js.map +1 -1
- package/package.json +6 -23
- package/src/api/files/jobs/FileJobs.ts +2 -1
- package/src/api/jobs/__tests__/$job.spec.ts +316 -2867
- package/src/api/jobs/controllers/AdminJobController.ts +29 -138
- package/src/api/jobs/entities/jobExecutionEntity.ts +27 -19
- package/src/api/jobs/index.browser.ts +5 -7
- package/src/api/jobs/index.ts +23 -51
- package/src/api/jobs/primitives/$job.ts +66 -58
- package/src/api/jobs/providers/JobProvider.ts +561 -566
- package/src/api/jobs/providers/JobQueueProvider.ts +18 -19
- package/src/api/jobs/schemas/jobConfigAtom.ts +20 -23
- package/src/api/jobs/schemas/jobExecutionQuerySchema.ts +3 -27
- package/src/api/jobs/schemas/jobExecutionResourceSchema.ts +5 -7
- package/src/api/jobs/schemas/jobRegistrationSchema.ts +7 -4
- package/src/api/jobs/schemas/triggerJobSchema.ts +0 -1
- package/src/api/jobs/services/JobService.ts +90 -483
- package/src/api/notifications/controllers/AdminNotificationController.ts +19 -12
- package/src/api/notifications/index.ts +7 -4
- package/src/api/notifications/jobs/NotificationJobs.ts +83 -12
- package/src/api/payments/services/PaymentService.ts +4 -2
- package/src/api/users/__tests__/UserJobs.spec.ts +10 -49
- package/src/api/users/audits/UserAudits.ts +3 -1
- package/src/api/users/buckets/UserBuckets.ts +2 -1
- package/src/api/users/index.ts +1 -4
- package/src/api/users/jobs/UserJobs.ts +5 -4
- package/src/api/verifications/jobs/VerificationJobs.ts +2 -1
- package/src/cli/core/__tests__/init.spec.ts +1 -1
- package/src/cli/core/commands/init.ts +0 -12
- package/src/cli/core/services/PackageManagerUtils.ts +2 -9
- package/src/cli/core/services/ProjectScaffolder.ts +17 -65
- package/src/cli/core/templates/agentMd.ts +2 -8
- package/src/cli/core/templates/apiIndexTs.ts +4 -18
- package/src/cli/core/templates/mainCss.ts +1 -36
- package/src/cli/core/templates/vitestConfigTs.ts +17 -0
- package/src/cli/core/templates/webAppRouterTs.ts +2 -85
- package/src/cli/platform/__tests__/CloudflareAdapter.spec.ts +22 -71
- package/src/cli/platform/adapters/CloudflareAdapter.ts +12 -11
- package/src/cli/platform/atoms/platformOptions.ts +9 -0
- package/src/cli/platform/schemas/cloudflare.ts +3 -2
- package/src/cli/platform/services/CloudflareApi.ts +164 -25
- package/src/cli/platform/services/WranglerApi.ts +0 -17
- package/src/core/Alepha.ts +9 -0
- package/src/react/form/index.ts +2 -0
- package/src/react/form/services/parseField.ts +163 -0
- package/src/react/form/services/prettyName.ts +19 -0
- package/src/react/head/providers/BrowserHeadProvider.ts +31 -10
- package/src/react/router/primitives/$page.ts +35 -12
- package/src/react/ui/atoms/uiAtom.ts +28 -0
- package/src/react/ui/components/ColorScheme.tsx +36 -0
- package/src/react/ui/hooks/useColorMode.ts +49 -0
- package/src/react/ui/hooks/useSidebarState.ts +26 -0
- package/src/react/ui/hooks/useTheme.ts +22 -0
- package/src/react/ui/index.ts +35 -0
- package/src/react/ui/services/UiPersistence.ts +41 -0
- package/src/router/TemplatedPathParser.ts +50 -51
- package/src/router/__tests__/RouterProvider.spec.ts +62 -0
- package/src/router/__tests__/TemplatedPathParser.spec.ts +18 -0
- package/src/router/providers/RouterProvider.ts +10 -5
- package/src/system/providers/NodeShellProvider.ts +1 -0
- package/src/topic/core/providers/TopicProvider.ts +1 -1
- package/dist/api/invitations/index.d.ts +0 -790
- package/dist/api/invitations/index.d.ts.map +0 -1
- package/dist/api/invitations/index.js +0 -662
- package/dist/api/invitations/index.js.map +0 -1
- package/dist/api/issues/index.d.ts +0 -810
- package/dist/api/issues/index.d.ts.map +0 -1
- package/dist/api/issues/index.js +0 -444
- package/dist/api/issues/index.js.map +0 -1
- package/dist/api/subscriptions/index.d.ts +0 -1692
- package/dist/api/subscriptions/index.d.ts.map +0 -1
- package/dist/api/subscriptions/index.js +0 -1867
- package/dist/api/subscriptions/index.js.map +0 -1
- package/dist/api/workflows/index.browser.js +0 -246
- package/dist/api/workflows/index.browser.js.map +0 -1
- package/dist/api/workflows/index.d.ts +0 -1618
- package/dist/api/workflows/index.d.ts.map +0 -1
- package/dist/api/workflows/index.js +0 -1495
- package/dist/api/workflows/index.js.map +0 -1
- package/src/api/invitations/__tests__/InvitationService.spec.ts +0 -439
- package/src/api/invitations/controllers/AdminInvitationController.ts +0 -86
- package/src/api/invitations/controllers/InvitationController.ts +0 -84
- package/src/api/invitations/entities/invitations.ts +0 -33
- package/src/api/invitations/index.ts +0 -58
- package/src/api/invitations/jobs/InvitationJobs.ts +0 -37
- package/src/api/invitations/providers/InvitationProvider.ts +0 -45
- package/src/api/invitations/schemas/createInvitationSchema.ts +0 -12
- package/src/api/invitations/schemas/invitationConfigAtom.ts +0 -20
- package/src/api/invitations/schemas/invitationQuerySchema.ts +0 -15
- package/src/api/invitations/schemas/invitationResourceSchema.ts +0 -6
- package/src/api/invitations/schemas/invitationWithResourceInfoSchema.ts +0 -22
- package/src/api/invitations/schemas/myInvitationsQuerySchema.ts +0 -10
- package/src/api/invitations/services/InvitationService.ts +0 -556
- package/src/api/issues/__tests__/IssueService.spec.ts +0 -263
- package/src/api/issues/controllers/AdminIssueController.ts +0 -149
- package/src/api/issues/controllers/IssueController.ts +0 -44
- package/src/api/issues/entities/issues.ts +0 -49
- package/src/api/issues/index.ts +0 -50
- package/src/api/issues/schemas/createIssueSchema.ts +0 -13
- package/src/api/issues/schemas/issueConfigAtom.ts +0 -13
- package/src/api/issues/schemas/issueQuerySchema.ts +0 -18
- package/src/api/issues/schemas/issueResourceSchema.ts +0 -6
- package/src/api/issues/schemas/myIssueQuerySchema.ts +0 -10
- package/src/api/issues/schemas/updateIssueSchema.ts +0 -13
- package/src/api/issues/services/IssueService.ts +0 -264
- package/src/api/jobs/__tests__/$job-middleware.spec.ts +0 -126
- package/src/api/jobs/__tests__/JobService.spec.ts +0 -31
- package/src/api/jobs/entities/jobExecutionLogEntity.ts +0 -13
- package/src/api/jobs/schemas/jobActivitySchema.ts +0 -15
- package/src/api/jobs/schemas/jobCronInfoSchema.ts +0 -22
- package/src/api/jobs/schemas/jobExecutionDetailResourceSchema.ts +0 -20
- package/src/api/jobs/schemas/jobFailureSchema.ts +0 -9
- package/src/api/jobs/schemas/jobQueueDepthSchema.ts +0 -14
- package/src/api/jobs/schemas/jobStatsSchema.ts +0 -14
- package/src/api/jobs/services/JobService-tests.ts +0 -157
- package/src/api/subscriptions/__tests__/BillingService.spec.ts +0 -218
- package/src/api/subscriptions/__tests__/SubscriptionService.spec.ts +0 -278
- package/src/api/subscriptions/controllers/AdminSubscriptionController.ts +0 -212
- package/src/api/subscriptions/controllers/SubscriptionController.ts +0 -189
- package/src/api/subscriptions/entities/subscriptionEvents.ts +0 -54
- package/src/api/subscriptions/entities/subscriptions.ts +0 -68
- package/src/api/subscriptions/index.ts +0 -133
- package/src/api/subscriptions/jobs/SubscriptionJobs.ts +0 -382
- package/src/api/subscriptions/middleware/$requireLimit.ts +0 -50
- package/src/api/subscriptions/middleware/$requirePlan.ts +0 -49
- package/src/api/subscriptions/notifications/SubscriptionNotifications.ts +0 -110
- package/src/api/subscriptions/schemas/cancelSubscriptionSchema.ts +0 -8
- package/src/api/subscriptions/schemas/changePlanSchema.ts +0 -9
- package/src/api/subscriptions/schemas/createSubscriptionSchema.ts +0 -11
- package/src/api/subscriptions/schemas/entitlementsSchema.ts +0 -21
- package/src/api/subscriptions/schemas/mrrSchema.ts +0 -13
- package/src/api/subscriptions/schemas/planDefinitionSchema.ts +0 -71
- package/src/api/subscriptions/schemas/planResourceSchema.ts +0 -25
- package/src/api/subscriptions/schemas/subscriptionEventResourceSchema.ts +0 -8
- package/src/api/subscriptions/schemas/subscriptionQuerySchema.ts +0 -19
- package/src/api/subscriptions/schemas/subscriptionResourceSchema.ts +0 -6
- package/src/api/subscriptions/schemas/subscriptionSettingsSchema.ts +0 -32
- package/src/api/subscriptions/schemas/subscriptionStatsSchema.ts +0 -23
- package/src/api/subscriptions/services/BillingService.ts +0 -437
- package/src/api/subscriptions/services/SubscriptionConfig.ts +0 -56
- package/src/api/subscriptions/services/SubscriptionService.ts +0 -867
- package/src/api/subscriptions/services/UsageService.ts +0 -118
- package/src/api/workflows/__tests__/$workflow.spec.ts +0 -616
- package/src/api/workflows/controllers/AdminWorkflowController.ts +0 -191
- package/src/api/workflows/entities/workflowExecutions.ts +0 -74
- package/src/api/workflows/entities/workflowStepExecutions.ts +0 -74
- package/src/api/workflows/entities/workflowStepLogs.ts +0 -13
- package/src/api/workflows/index.browser.ts +0 -22
- package/src/api/workflows/index.ts +0 -115
- package/src/api/workflows/jobs/WorkflowJobs.ts +0 -77
- package/src/api/workflows/primitives/$workflow.ts +0 -202
- package/src/api/workflows/providers/WorkflowProvider.ts +0 -1284
- package/src/api/workflows/schemas/workflowActivitySchema.ts +0 -15
- package/src/api/workflows/schemas/workflowConfigAtom.ts +0 -51
- package/src/api/workflows/schemas/workflowExecutionDetailSchema.ts +0 -18
- package/src/api/workflows/schemas/workflowExecutionQuerySchema.ts +0 -26
- package/src/api/workflows/schemas/workflowExecutionResourceSchema.ts +0 -30
- package/src/api/workflows/schemas/workflowRegistrationSchema.ts +0 -26
- package/src/api/workflows/schemas/workflowStatsSchema.ts +0 -16
- package/src/api/workflows/schemas/workflowStepExecutionResourceSchema.ts +0 -15
- package/src/api/workflows/services/WorkflowService.ts +0 -382
- package/src/cli/core/templates/apiAppSecurityTs.ts +0 -43
- package/src/cli/core/templates/webAdminDashboardTsx.ts +0 -17
|
@@ -22,9 +22,9 @@ import {
|
|
|
22
22
|
cloudflareKVSchema,
|
|
23
23
|
cloudflareQueueConsumerSchema,
|
|
24
24
|
cloudflareQueueSchema,
|
|
25
|
-
|
|
25
|
+
cloudflareR2Schema,
|
|
26
26
|
cloudflareSecretSchema,
|
|
27
|
-
|
|
27
|
+
cloudflareVersionSchema,
|
|
28
28
|
cloudflareWorkerSchema,
|
|
29
29
|
createD1BodySchema,
|
|
30
30
|
createHyperdriveBodySchema,
|
|
@@ -81,6 +81,16 @@ export class CloudflareApi {
|
|
|
81
81
|
this.jurisdiction = jurisdiction;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Override the Cloudflare account ID (from platform config).
|
|
86
|
+
*
|
|
87
|
+
* When unset, `resolveAccountId` falls back to `CLOUDFLARE_ACCOUNT_ID` env
|
|
88
|
+
* var or the token's single account.
|
|
89
|
+
*/
|
|
90
|
+
public setAccountId(accountId?: string): void {
|
|
91
|
+
this.accountId = accountId;
|
|
92
|
+
}
|
|
93
|
+
|
|
84
94
|
// -------------------------------------------------------------------------
|
|
85
95
|
// Auth
|
|
86
96
|
// -------------------------------------------------------------------------
|
|
@@ -107,6 +117,12 @@ export class CloudflareApi {
|
|
|
107
117
|
return this.accountId;
|
|
108
118
|
}
|
|
109
119
|
|
|
120
|
+
const fromEnv = process.env.CLOUDFLARE_ACCOUNT_ID;
|
|
121
|
+
if (fromEnv) {
|
|
122
|
+
this.accountId = fromEnv;
|
|
123
|
+
return this.accountId;
|
|
124
|
+
}
|
|
125
|
+
|
|
110
126
|
const res = await this.fetch<CloudflareAccount[]>("/accounts", {
|
|
111
127
|
schema: t.array(cloudflareAccountSchema),
|
|
112
128
|
});
|
|
@@ -115,6 +131,15 @@ export class CloudflareApi {
|
|
|
115
131
|
throw new AlephaError("No Cloudflare accounts found for this token.");
|
|
116
132
|
}
|
|
117
133
|
|
|
134
|
+
if (res.length > 1) {
|
|
135
|
+
const list = res.map((a) => ` - ${a.id} ${a.name}`).join("\n");
|
|
136
|
+
throw new AlephaError(
|
|
137
|
+
`Cloudflare token has access to ${res.length} accounts; set ` +
|
|
138
|
+
`\`CLOUDFLARE_ACCOUNT_ID\` or the \`accountId\` field in your ` +
|
|
139
|
+
`platform config to pick one:\n${list}`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
118
143
|
this.accountId = res[0].id;
|
|
119
144
|
return this.accountId;
|
|
120
145
|
}
|
|
@@ -125,9 +150,9 @@ export class CloudflareApi {
|
|
|
125
150
|
|
|
126
151
|
public async listD1(): Promise<CloudflareD1[]> {
|
|
127
152
|
const accountId = await this.resolveAccountId();
|
|
128
|
-
return await this.
|
|
153
|
+
return await this.paginate<CloudflareD1>(
|
|
129
154
|
`/accounts/${accountId}/d1/database`,
|
|
130
|
-
|
|
155
|
+
cloudflareD1Schema,
|
|
131
156
|
);
|
|
132
157
|
}
|
|
133
158
|
|
|
@@ -136,15 +161,19 @@ export class CloudflareApi {
|
|
|
136
161
|
location = "weur", // TODO: move to config (or auto-resolve based on account info, or ask ?)
|
|
137
162
|
): Promise<CloudflareD1> {
|
|
138
163
|
const accountId = await this.resolveAccountId();
|
|
164
|
+
// When jurisdiction is set, `primary_location_hint` is silently ignored
|
|
165
|
+
// by the API, so omit it to avoid confusion.
|
|
166
|
+
const body: Record<string, unknown> = { name };
|
|
167
|
+
if (this.jurisdiction) {
|
|
168
|
+
body.jurisdiction = this.jurisdiction;
|
|
169
|
+
} else {
|
|
170
|
+
body.primary_location_hint = location;
|
|
171
|
+
}
|
|
139
172
|
return await this.fetch<CloudflareD1>(
|
|
140
173
|
`/accounts/${accountId}/d1/database`,
|
|
141
174
|
{
|
|
142
175
|
method: "POST",
|
|
143
|
-
body
|
|
144
|
-
name,
|
|
145
|
-
primary_location_hint: location,
|
|
146
|
-
...(this.jurisdiction ? { jurisdiction: this.jurisdiction } : {}),
|
|
147
|
-
},
|
|
176
|
+
body,
|
|
148
177
|
bodySchema: createD1BodySchema,
|
|
149
178
|
schema: cloudflareD1Schema,
|
|
150
179
|
},
|
|
@@ -164,9 +193,10 @@ export class CloudflareApi {
|
|
|
164
193
|
|
|
165
194
|
public async listKV(): Promise<CloudflareKV[]> {
|
|
166
195
|
const accountId = await this.resolveAccountId();
|
|
167
|
-
return await this.
|
|
196
|
+
return await this.paginate<CloudflareKV>(
|
|
168
197
|
`/accounts/${accountId}/storage/kv/namespaces`,
|
|
169
|
-
|
|
198
|
+
cloudflareKVSchema,
|
|
199
|
+
100, // KV list caps at 100 per page
|
|
170
200
|
);
|
|
171
201
|
}
|
|
172
202
|
|
|
@@ -197,11 +227,11 @@ export class CloudflareApi {
|
|
|
197
227
|
|
|
198
228
|
public async listR2(): Promise<CloudflareR2[]> {
|
|
199
229
|
const accountId = await this.resolveAccountId();
|
|
200
|
-
|
|
230
|
+
return await this.paginateCursor<CloudflareR2>(
|
|
201
231
|
`/accounts/${accountId}/r2/buckets`,
|
|
202
|
-
|
|
232
|
+
"buckets",
|
|
233
|
+
cloudflareR2Schema,
|
|
203
234
|
);
|
|
204
|
-
return res.buckets;
|
|
205
235
|
}
|
|
206
236
|
|
|
207
237
|
public async createR2(name: string): Promise<void> {
|
|
@@ -226,9 +256,9 @@ export class CloudflareApi {
|
|
|
226
256
|
|
|
227
257
|
public async listQueues(): Promise<CloudflareQueue[]> {
|
|
228
258
|
const accountId = await this.resolveAccountId();
|
|
229
|
-
return await this.
|
|
259
|
+
return await this.paginate<CloudflareQueue>(
|
|
230
260
|
`/accounts/${accountId}/queues`,
|
|
231
|
-
|
|
261
|
+
cloudflareQueueSchema,
|
|
232
262
|
);
|
|
233
263
|
}
|
|
234
264
|
|
|
@@ -253,9 +283,9 @@ export class CloudflareApi {
|
|
|
253
283
|
queueId: string,
|
|
254
284
|
): Promise<CloudflareQueueConsumer[]> {
|
|
255
285
|
const accountId = await this.resolveAccountId();
|
|
256
|
-
return await this.
|
|
286
|
+
return await this.paginate<CloudflareQueueConsumer>(
|
|
257
287
|
`/accounts/${accountId}/queues/${queueId}/consumers`,
|
|
258
|
-
|
|
288
|
+
cloudflareQueueConsumerSchema,
|
|
259
289
|
);
|
|
260
290
|
}
|
|
261
291
|
|
|
@@ -264,8 +294,13 @@ export class CloudflareApi {
|
|
|
264
294
|
consumerService: string,
|
|
265
295
|
): Promise<void> {
|
|
266
296
|
const accountId = await this.resolveAccountId();
|
|
297
|
+
const consumers = await this.listQueueConsumers(queueId);
|
|
298
|
+
const consumer = consumers.find((c) => c.service === consumerService);
|
|
299
|
+
if (!consumer) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
267
302
|
await this.fetch(
|
|
268
|
-
`/accounts/${accountId}/queues/${queueId}/consumers/${
|
|
303
|
+
`/accounts/${accountId}/queues/${queueId}/consumers/${consumer.consumer_id}`,
|
|
269
304
|
{ method: "DELETE" },
|
|
270
305
|
);
|
|
271
306
|
}
|
|
@@ -276,9 +311,9 @@ export class CloudflareApi {
|
|
|
276
311
|
|
|
277
312
|
public async listHyperdrive(): Promise<CloudflareHyperdrive[]> {
|
|
278
313
|
const accountId = await this.resolveAccountId();
|
|
279
|
-
return await this.
|
|
314
|
+
return await this.paginate<CloudflareHyperdrive>(
|
|
280
315
|
`/accounts/${accountId}/hyperdrive/configs`,
|
|
281
|
-
|
|
316
|
+
cloudflareHyperdriveSchema,
|
|
282
317
|
);
|
|
283
318
|
}
|
|
284
319
|
|
|
@@ -338,20 +373,22 @@ export class CloudflareApi {
|
|
|
338
373
|
scriptName: string,
|
|
339
374
|
): Promise<CloudflareDeployment[]> {
|
|
340
375
|
const accountId = await this.resolveAccountId();
|
|
376
|
+
// Deployments list is wrapped in `{ deployments }` and returns newest
|
|
377
|
+
// first; for picking the active deployment we only need the top page.
|
|
341
378
|
const res = await this.fetch<{ deployments: CloudflareDeployment[] }>(
|
|
342
379
|
`/accounts/${accountId}/workers/scripts/${scriptName}/deployments`,
|
|
343
|
-
{ schema: cloudflareDeploymentListSchema },
|
|
380
|
+
{ schema: cloudflareDeploymentListSchema, query: { per_page: "100" } },
|
|
344
381
|
);
|
|
345
382
|
return res.deployments;
|
|
346
383
|
}
|
|
347
384
|
|
|
348
385
|
public async listVersions(scriptName: string): Promise<CloudflareVersion[]> {
|
|
349
386
|
const accountId = await this.resolveAccountId();
|
|
350
|
-
|
|
387
|
+
return await this.paginateCursor<CloudflareVersion>(
|
|
351
388
|
`/accounts/${accountId}/workers/scripts/${scriptName}/versions`,
|
|
352
|
-
|
|
389
|
+
"items",
|
|
390
|
+
cloudflareVersionSchema,
|
|
353
391
|
);
|
|
354
|
-
return res.items;
|
|
355
392
|
}
|
|
356
393
|
|
|
357
394
|
// -------------------------------------------------------------------------
|
|
@@ -428,6 +465,13 @@ export class CloudflareApi {
|
|
|
428
465
|
success: boolean;
|
|
429
466
|
result: T;
|
|
430
467
|
errors: CloudflareApiError[];
|
|
468
|
+
result_info?: {
|
|
469
|
+
page: number;
|
|
470
|
+
per_page: number;
|
|
471
|
+
total_pages?: number;
|
|
472
|
+
count?: number;
|
|
473
|
+
total_count?: number;
|
|
474
|
+
};
|
|
431
475
|
};
|
|
432
476
|
|
|
433
477
|
if (!json.success) {
|
|
@@ -444,6 +488,101 @@ export class CloudflareApi {
|
|
|
444
488
|
return json.result;
|
|
445
489
|
}
|
|
446
490
|
|
|
491
|
+
/**
|
|
492
|
+
* Paginate a page-based list endpoint (`result_info.total_pages`).
|
|
493
|
+
*
|
|
494
|
+
* Cloudflare defaults to `per_page=20`; we push it to 1000 (max on most
|
|
495
|
+
* list endpoints) and loop if more pages exist. Each page is validated
|
|
496
|
+
* against the item schema.
|
|
497
|
+
*/
|
|
498
|
+
protected async paginate<T>(
|
|
499
|
+
path: string,
|
|
500
|
+
itemSchema: TSchema,
|
|
501
|
+
perPage = 1000,
|
|
502
|
+
): Promise<T[]> {
|
|
503
|
+
const results: T[] = [];
|
|
504
|
+
let page = 1;
|
|
505
|
+
|
|
506
|
+
while (true) {
|
|
507
|
+
const token = await this.resolveToken();
|
|
508
|
+
const url = `${CloudflareApi.BASE}${path}?per_page=${perPage}&page=${page}`;
|
|
509
|
+
|
|
510
|
+
const headers: Record<string, string> = {
|
|
511
|
+
Authorization: `Bearer ${token}`,
|
|
512
|
+
};
|
|
513
|
+
if (this.jurisdiction && /\/r2\//.test(path)) {
|
|
514
|
+
headers["cf-r2-jurisdiction"] = this.jurisdiction;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const response = await globalThis.fetch(url, { method: "GET", headers });
|
|
518
|
+
const json = (await response.json()) as {
|
|
519
|
+
success: boolean;
|
|
520
|
+
result: T[];
|
|
521
|
+
errors: CloudflareApiError[];
|
|
522
|
+
result_info?: { page: number; total_pages?: number };
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
if (!json.success) {
|
|
526
|
+
const messages = json.errors.map((e) => e.message).join(", ");
|
|
527
|
+
throw new AlephaError(
|
|
528
|
+
`Cloudflare API error (GET ${path}): ${messages}`,
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const validated = this.alepha.codec.validate(
|
|
533
|
+
t.array(itemSchema),
|
|
534
|
+
json.result,
|
|
535
|
+
) as T[];
|
|
536
|
+
results.push(...validated);
|
|
537
|
+
|
|
538
|
+
const totalPages = json.result_info?.total_pages;
|
|
539
|
+
if (!totalPages || page >= totalPages || validated.length === 0) {
|
|
540
|
+
break;
|
|
541
|
+
}
|
|
542
|
+
page++;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
return results;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Paginate a cursor-based list endpoint where `result` is an object
|
|
550
|
+
* containing both the items array and a `cursor` field (R2 buckets,
|
|
551
|
+
* Workers versions). Returns the flattened item array.
|
|
552
|
+
*/
|
|
553
|
+
protected async paginateCursor<T>(
|
|
554
|
+
path: string,
|
|
555
|
+
itemsKey: string,
|
|
556
|
+
itemSchema: TSchema,
|
|
557
|
+
perPage = 1000,
|
|
558
|
+
): Promise<T[]> {
|
|
559
|
+
const results: T[] = [];
|
|
560
|
+
let cursor: string | undefined;
|
|
561
|
+
|
|
562
|
+
while (true) {
|
|
563
|
+
const query: Record<string, string> = { per_page: String(perPage) };
|
|
564
|
+
if (cursor) {
|
|
565
|
+
query.cursor = cursor;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const res = await this.fetch<Record<string, unknown>>(path, { query });
|
|
569
|
+
const items = (res[itemsKey] as unknown[]) ?? [];
|
|
570
|
+
const validated = this.alepha.codec.validate(
|
|
571
|
+
t.array(itemSchema),
|
|
572
|
+
items,
|
|
573
|
+
) as T[];
|
|
574
|
+
results.push(...validated);
|
|
575
|
+
|
|
576
|
+
const nextCursor = res.cursor as string | undefined;
|
|
577
|
+
if (!nextCursor || validated.length === 0) {
|
|
578
|
+
break;
|
|
579
|
+
}
|
|
580
|
+
cursor = nextCursor;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return results;
|
|
584
|
+
}
|
|
585
|
+
|
|
447
586
|
// -------------------------------------------------------------------------
|
|
448
587
|
// Helpers
|
|
449
588
|
// -------------------------------------------------------------------------
|
|
@@ -124,21 +124,4 @@ export class WranglerApi {
|
|
|
124
124
|
{ resolve: true, env: { CI: "1" } },
|
|
125
125
|
);
|
|
126
126
|
}
|
|
127
|
-
|
|
128
|
-
// -------------------------------------------------------------------------
|
|
129
|
-
// Secrets
|
|
130
|
-
// -------------------------------------------------------------------------
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Push secrets in bulk to a worker.
|
|
134
|
-
*/
|
|
135
|
-
public async secretBulk(
|
|
136
|
-
secretsPath: string,
|
|
137
|
-
workerName: string,
|
|
138
|
-
): Promise<void> {
|
|
139
|
-
await this.runShell(
|
|
140
|
-
`wrangler secret bulk ${secretsPath} --name=${workerName}`,
|
|
141
|
-
{ resolve: true },
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
127
|
}
|
package/src/core/Alepha.ts
CHANGED
|
@@ -826,6 +826,15 @@ export class Alepha {
|
|
|
826
826
|
return this;
|
|
827
827
|
}
|
|
828
828
|
|
|
829
|
+
/**
|
|
830
|
+
* Alias for {@link Alepha#with}.
|
|
831
|
+
*/
|
|
832
|
+
public register<T extends object>(
|
|
833
|
+
serviceEntry: ServiceEntry<T> | { default: ServiceEntry<T> },
|
|
834
|
+
): this {
|
|
835
|
+
return this.with(serviceEntry);
|
|
836
|
+
}
|
|
837
|
+
|
|
829
838
|
/**
|
|
830
839
|
* Get an instance of the specified service from the container.
|
|
831
840
|
*
|
package/src/react/form/index.ts
CHANGED
|
@@ -9,6 +9,8 @@ export * from "./hooks/useForm.ts";
|
|
|
9
9
|
export * from "./hooks/useFormState.ts";
|
|
10
10
|
export * from "./hooks/useFormValues.ts";
|
|
11
11
|
export * from "./services/FormModel.ts";
|
|
12
|
+
export * from "./services/parseField.ts";
|
|
13
|
+
export * from "./services/prettyName.ts";
|
|
12
14
|
|
|
13
15
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
14
16
|
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { type TSchema, TypeBoxError } from "alepha";
|
|
2
|
+
import type { BaseInputField } from "./FormModel.ts";
|
|
3
|
+
import { prettyName } from "./prettyName.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Semantic icon hint derived from schema metadata. UI layers map this
|
|
7
|
+
* to their own icon set — this module is headless and ships no JSX.
|
|
8
|
+
*/
|
|
9
|
+
export type IconHint =
|
|
10
|
+
| "email"
|
|
11
|
+
| "password"
|
|
12
|
+
| "phone"
|
|
13
|
+
| "url"
|
|
14
|
+
| "number"
|
|
15
|
+
| "calendar"
|
|
16
|
+
| "clock"
|
|
17
|
+
| "list"
|
|
18
|
+
| "text"
|
|
19
|
+
| "user"
|
|
20
|
+
| "file"
|
|
21
|
+
| "switch";
|
|
22
|
+
|
|
23
|
+
export interface FieldConstraints {
|
|
24
|
+
minLength?: number;
|
|
25
|
+
maxLength?: number;
|
|
26
|
+
minimum?: number;
|
|
27
|
+
maximum?: number;
|
|
28
|
+
pattern?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface FieldMeta {
|
|
32
|
+
id?: string;
|
|
33
|
+
label: string;
|
|
34
|
+
description?: string;
|
|
35
|
+
error?: string;
|
|
36
|
+
required: boolean;
|
|
37
|
+
type?: string;
|
|
38
|
+
format?: string;
|
|
39
|
+
isEnum: boolean;
|
|
40
|
+
isArray: boolean;
|
|
41
|
+
isObject: boolean;
|
|
42
|
+
isArrayOfObjects: boolean;
|
|
43
|
+
enum?: readonly unknown[];
|
|
44
|
+
iconHint?: IconHint;
|
|
45
|
+
constraints: FieldConstraints;
|
|
46
|
+
testId?: string;
|
|
47
|
+
schema: TSchema;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ParseFieldOptions {
|
|
51
|
+
label?: string;
|
|
52
|
+
description?: string;
|
|
53
|
+
error?: Error;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Derives a {@link FieldMeta} from an `InputField` (from `useForm`) plus
|
|
58
|
+
* optional overrides. Pure — no React, no JSX, no UI library coupling.
|
|
59
|
+
*
|
|
60
|
+
* UI components consume this metadata to render labels, descriptions,
|
|
61
|
+
* error messages, icons, and validation constraints.
|
|
62
|
+
*/
|
|
63
|
+
export const parseField = (
|
|
64
|
+
input: BaseInputField,
|
|
65
|
+
options: ParseFieldOptions = {},
|
|
66
|
+
): FieldMeta => {
|
|
67
|
+
const schema = input.schema as TSchema & {
|
|
68
|
+
type?: string;
|
|
69
|
+
format?: string;
|
|
70
|
+
title?: string;
|
|
71
|
+
description?: string;
|
|
72
|
+
enum?: readonly unknown[];
|
|
73
|
+
minLength?: number;
|
|
74
|
+
maxLength?: number;
|
|
75
|
+
minimum?: number;
|
|
76
|
+
maximum?: number;
|
|
77
|
+
pattern?: string;
|
|
78
|
+
properties?: unknown;
|
|
79
|
+
items?: { properties?: unknown };
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const label =
|
|
83
|
+
options.label ??
|
|
84
|
+
(typeof schema.title === "string" ? schema.title : undefined) ??
|
|
85
|
+
prettyName(input.path);
|
|
86
|
+
|
|
87
|
+
const description =
|
|
88
|
+
options.description ??
|
|
89
|
+
(typeof schema.description === "string" ? schema.description : undefined);
|
|
90
|
+
|
|
91
|
+
const error =
|
|
92
|
+
options.error instanceof TypeBoxError
|
|
93
|
+
? (options.error as TypeBoxError).value?.message
|
|
94
|
+
: undefined;
|
|
95
|
+
|
|
96
|
+
const type = schema.type;
|
|
97
|
+
const format = typeof schema.format === "string" ? schema.format : undefined;
|
|
98
|
+
const isEnum = Array.isArray(schema.enum);
|
|
99
|
+
const isArray = type === "array";
|
|
100
|
+
const isObject = type === "object" && Boolean(schema.properties);
|
|
101
|
+
const isArrayOfObjects =
|
|
102
|
+
isArray && Boolean(schema.items && (schema.items as any).properties);
|
|
103
|
+
|
|
104
|
+
const name = input.props.name;
|
|
105
|
+
const iconHint = inferIconHint({ type, format, name, isEnum, isArray });
|
|
106
|
+
|
|
107
|
+
const constraints: FieldConstraints = {};
|
|
108
|
+
if (typeof schema.minLength === "number")
|
|
109
|
+
constraints.minLength = schema.minLength;
|
|
110
|
+
if (typeof schema.maxLength === "number")
|
|
111
|
+
constraints.maxLength = schema.maxLength;
|
|
112
|
+
if (typeof schema.minimum === "number") constraints.minimum = schema.minimum;
|
|
113
|
+
if (typeof schema.maximum === "number") constraints.maximum = schema.maximum;
|
|
114
|
+
if (typeof schema.pattern === "string") constraints.pattern = schema.pattern;
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
id: input.props.id,
|
|
118
|
+
label,
|
|
119
|
+
description,
|
|
120
|
+
error,
|
|
121
|
+
required: input.required,
|
|
122
|
+
type,
|
|
123
|
+
format,
|
|
124
|
+
isEnum,
|
|
125
|
+
isArray,
|
|
126
|
+
isObject,
|
|
127
|
+
isArrayOfObjects,
|
|
128
|
+
enum: schema.enum,
|
|
129
|
+
iconHint,
|
|
130
|
+
constraints,
|
|
131
|
+
testId: (input.props as Record<string, unknown>)["data-testid"] as
|
|
132
|
+
| string
|
|
133
|
+
| undefined,
|
|
134
|
+
schema: input.schema,
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const inferIconHint = (params: {
|
|
139
|
+
type?: string;
|
|
140
|
+
format?: string;
|
|
141
|
+
name?: string;
|
|
142
|
+
isEnum: boolean;
|
|
143
|
+
isArray: boolean;
|
|
144
|
+
}): IconHint | undefined => {
|
|
145
|
+
const { type, format, name, isEnum, isArray } = params;
|
|
146
|
+
|
|
147
|
+
if (format === "email") return "email";
|
|
148
|
+
if (format === "url" || format === "uri") return "url";
|
|
149
|
+
if (format === "tel" || format === "phone") return "phone";
|
|
150
|
+
if (format === "date" || format === "date-time") return "calendar";
|
|
151
|
+
if (format === "time") return "clock";
|
|
152
|
+
|
|
153
|
+
if (name?.toLowerCase().includes("password")) return "password";
|
|
154
|
+
if (name?.toLowerCase().includes("email")) return "email";
|
|
155
|
+
if (name?.toLowerCase().includes("phone")) return "phone";
|
|
156
|
+
|
|
157
|
+
if (type === "boolean") return "switch";
|
|
158
|
+
if (type === "number" || type === "integer") return "number";
|
|
159
|
+
if (isEnum || isArray) return "list";
|
|
160
|
+
if (type === "string") return "text";
|
|
161
|
+
|
|
162
|
+
return undefined;
|
|
163
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a path or identifier string into a pretty display name.
|
|
3
|
+
* For paths like "/contacts/0/name", extracts just the field name "Name".
|
|
4
|
+
* Handles camelCase and snake_case conversion to Title Case.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* prettyName("/userName") // "User Name"
|
|
8
|
+
* prettyName("/contacts/0/email") // "Email"
|
|
9
|
+
* prettyName("/address/streetName") // "Street Name"
|
|
10
|
+
* prettyName("first_name") // "First Name"
|
|
11
|
+
*/
|
|
12
|
+
export const prettyName = (name: string): string => {
|
|
13
|
+
const segments = name.split("/").filter((s) => s && !/^\d+$/.test(s));
|
|
14
|
+
const fieldName = segments[segments.length - 1] || name.replaceAll("/", "");
|
|
15
|
+
return fieldName
|
|
16
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
17
|
+
.replace(/_/g, " ")
|
|
18
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
19
|
+
};
|
|
@@ -154,10 +154,12 @@ export class BrowserHeadProvider {
|
|
|
154
154
|
| string
|
|
155
155
|
| (Record<string, string | boolean | undefined> & { content?: string }),
|
|
156
156
|
): void {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
//
|
|
157
|
+
// Plain string → inline script. Dedupe by exact content match against
|
|
158
|
+
// any existing inline script (handles SSR-emitted globals that would
|
|
159
|
+
// otherwise be re-appended on hydration).
|
|
160
160
|
if (typeof script === "string") {
|
|
161
|
+
if (this.findInlineScriptByContent(document, script)) return;
|
|
162
|
+
const el = document.createElement("script");
|
|
161
163
|
el.textContent = script;
|
|
162
164
|
document.head.appendChild(el);
|
|
163
165
|
return;
|
|
@@ -165,14 +167,18 @@ export class BrowserHeadProvider {
|
|
|
165
167
|
|
|
166
168
|
const { content, ...attrs } = script;
|
|
167
169
|
|
|
168
|
-
//
|
|
170
|
+
// src-based scripts: dedupe by src attribute (existing behaviour).
|
|
169
171
|
if (attrs.src) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
172
|
+
if (document.querySelector(`script[src="${attrs.src}"]`)) return;
|
|
173
|
+
} else if (typeof attrs.id === "string") {
|
|
174
|
+
// id-based dedupe — single source of truth per id.
|
|
175
|
+
if (document.querySelector(`script#${CSS.escape(attrs.id)}`)) return;
|
|
176
|
+
} else if (content) {
|
|
177
|
+
// Inline scripts with `content` and no src/id: fall back to content match.
|
|
178
|
+
if (this.findInlineScriptByContent(document, content)) return;
|
|
174
179
|
}
|
|
175
180
|
|
|
181
|
+
const el = document.createElement("script");
|
|
176
182
|
for (const [key, value] of Object.entries(attrs)) {
|
|
177
183
|
if (value === true) {
|
|
178
184
|
el.setAttribute(key, "");
|
|
@@ -180,14 +186,29 @@ export class BrowserHeadProvider {
|
|
|
180
186
|
el.setAttribute(key, String(value));
|
|
181
187
|
}
|
|
182
188
|
}
|
|
183
|
-
|
|
184
189
|
if (content) {
|
|
185
190
|
el.textContent = content;
|
|
186
191
|
}
|
|
187
|
-
|
|
188
192
|
document.head.appendChild(el);
|
|
189
193
|
}
|
|
190
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Find an existing inline `<script>` tag (no `src`) with matching textContent.
|
|
197
|
+
* Used to make `renderScriptTag` idempotent across hydration + navigation,
|
|
198
|
+
* so SSR-emitted global scripts aren't re-appended client-side.
|
|
199
|
+
*/
|
|
200
|
+
protected findInlineScriptByContent(
|
|
201
|
+
document: Document,
|
|
202
|
+
content: string,
|
|
203
|
+
): Element | null {
|
|
204
|
+
for (const existing of document.head.querySelectorAll(
|
|
205
|
+
"script:not([src])",
|
|
206
|
+
)) {
|
|
207
|
+
if (existing.textContent === content) return existing;
|
|
208
|
+
}
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
191
212
|
protected renderMetaTag(document: Document, meta: HeadMeta): void {
|
|
192
213
|
const { content } = meta;
|
|
193
214
|
|
|
@@ -50,16 +50,6 @@ import { ReactPageService } from "../services/ReactPageService.ts";
|
|
|
50
50
|
* - Hierarchical error handling (child → parent)
|
|
51
51
|
* - HTTP status code handling (404, 401, etc.)
|
|
52
52
|
*
|
|
53
|
-
* **Page Animations**
|
|
54
|
-
* - CSS-based enter/exit animations
|
|
55
|
-
* - Dynamic animations based on page state
|
|
56
|
-
* - Custom timing and easing functions
|
|
57
|
-
*
|
|
58
|
-
* **Lifecycle Management**
|
|
59
|
-
* - Server response hooks for headers and status codes
|
|
60
|
-
* - Page leave handlers for cleanup (browser only)
|
|
61
|
-
* - Permission-based access control
|
|
62
|
-
*
|
|
63
53
|
* @example Simple page with data fetching
|
|
64
54
|
* ```typescript
|
|
65
55
|
* const userProfile = $page({
|
|
@@ -202,13 +192,46 @@ export interface PagePrimitiveOptions<
|
|
|
202
192
|
lazy?: () => Promise<{ default: FC<TProps & TPropsParent> }>;
|
|
203
193
|
|
|
204
194
|
/**
|
|
205
|
-
* Attach child pages to create nested routes
|
|
206
|
-
*
|
|
195
|
+
* Attach child pages to create nested routes, adopting them as children of
|
|
196
|
+
* this page.
|
|
197
|
+
*
|
|
198
|
+
* Use this when you want a parent to own children it cannot modify — most
|
|
199
|
+
* notably pages that come from an injected router in another package, whose
|
|
200
|
+
* `$page` definitions are frozen and cannot declare `parent` themselves.
|
|
201
|
+
*
|
|
202
|
+
* ```ts
|
|
203
|
+
* layout = $page({
|
|
204
|
+
* path: "/app",
|
|
205
|
+
* children: () => [
|
|
206
|
+
* this.productRouter.catalogPage, // from $inject(ProductRouter)
|
|
207
|
+
* this.productRouter.checkoutPage,
|
|
208
|
+
* ],
|
|
209
|
+
* });
|
|
210
|
+
* ```
|
|
211
|
+
*
|
|
212
|
+
* Use a thunk (`() => [...]`) when the children are defined later in the
|
|
213
|
+
* same class.
|
|
214
|
+
*
|
|
215
|
+
* **Declare each edge from one side only.** If a child already sets
|
|
216
|
+
* `parent: thisPage`, do NOT also add it to `children` — the link is
|
|
217
|
+
* already established, and declaring it on both sides creates a TypeScript
|
|
218
|
+
* circular dependency between the two class fields (each references the
|
|
219
|
+
* other before it is initialised).
|
|
207
220
|
*/
|
|
208
221
|
children?: Array<PagePrimitive> | (() => Array<PagePrimitive>);
|
|
209
222
|
|
|
210
223
|
/**
|
|
211
224
|
* Define a parent page for nested routing.
|
|
225
|
+
*
|
|
226
|
+
* Use this when you own the child page and can edit its definition — it is
|
|
227
|
+
* the simplest way to nest routes and reads top-down. For pages you do NOT
|
|
228
|
+
* own (e.g. pages exposed by an injected router from another package), let
|
|
229
|
+
* the parent adopt them via its `children` option instead.
|
|
230
|
+
*
|
|
231
|
+
* **Declare each edge from one side only.** If you set `parent` here, do
|
|
232
|
+
* NOT also add this page to the parent's `children` array — the link is
|
|
233
|
+
* already established, and declaring it on both sides creates a TypeScript
|
|
234
|
+
* circular dependency between the two class fields.
|
|
212
235
|
*/
|
|
213
236
|
parent?: PagePrimitive<PageConfigSchema, TPropsParent, any>;
|
|
214
237
|
|