@tekki-forge/compiler 0.0.0-stage → 0.1.0
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/README.md +4 -2
- package/dist/compile.d.ts +17 -0
- package/dist/compile.js +691 -0
- package/dist/compile.js.map +1 -0
- package/dist/dashboard.d.ts +7 -0
- package/dist/dashboard.js +103 -0
- package/dist/dashboard.js.map +1 -0
- package/dist/features.d.ts +13 -0
- package/dist/features.js +247 -0
- package/dist/features.js.map +1 -0
- package/dist/files.d.ts +34 -0
- package/dist/files.js +32 -0
- package/dist/files.js.map +1 -0
- package/dist/i18n.d.ts +24 -0
- package/dist/i18n.js +168 -0
- package/dist/i18n.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/indexes.d.ts +13 -0
- package/dist/indexes.js +71 -0
- package/dist/indexes.js.map +1 -0
- package/dist/ir.d.ts +413 -0
- package/dist/ir.js +27 -0
- package/dist/ir.js.map +1 -0
- package/dist/landing.d.ts +5 -0
- package/dist/landing.js +73 -0
- package/dist/landing.js.map +1 -0
- package/dist/load.d.ts +12 -0
- package/dist/load.js +38 -0
- package/dist/load.js.map +1 -0
- package/dist/naming.d.ts +11 -0
- package/dist/naming.js +58 -0
- package/dist/naming.js.map +1 -0
- package/dist/notifications.d.ts +5 -0
- package/dist/notifications.js +80 -0
- package/dist/notifications.js.map +1 -0
- package/dist/rules.d.ts +4 -0
- package/dist/rules.js +43 -0
- package/dist/rules.js.map +1 -0
- package/dist/theme.d.ts +69 -0
- package/dist/theme.js +257 -0
- package/dist/theme.js.map +1 -0
- package/dist/ui.d.ts +83 -0
- package/dist/ui.js +102 -0
- package/dist/ui.js.map +1 -0
- package/package.json +41 -3
package/dist/ir.d.ts
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import type { FieldKind, SdkLanguage, SocialProvider } from "@tekki-forge/core";
|
|
2
|
+
/**
|
|
3
|
+
* The Forge intermediate representation. Every generator and runtime consumes this
|
|
4
|
+
* normalized, JSON-serializable structure — never the raw user config.
|
|
5
|
+
*/
|
|
6
|
+
export interface AppIR {
|
|
7
|
+
irVersion: 1;
|
|
8
|
+
name: string;
|
|
9
|
+
slug: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
database: DatabaseIR;
|
|
12
|
+
auth: AuthIR;
|
|
13
|
+
tenancy: TenancyIR;
|
|
14
|
+
/** All roles known to the authorization layer, "admin" first. */
|
|
15
|
+
roles: string[];
|
|
16
|
+
api: {
|
|
17
|
+
prefix: string;
|
|
18
|
+
docs: boolean;
|
|
19
|
+
};
|
|
20
|
+
/** Client SDKs `forge generate` emits. */
|
|
21
|
+
sdk: {
|
|
22
|
+
languages: SdkLanguage[];
|
|
23
|
+
};
|
|
24
|
+
admin: {
|
|
25
|
+
enabled: boolean;
|
|
26
|
+
};
|
|
27
|
+
theme: ThemeIR;
|
|
28
|
+
plugins: PluginIR[];
|
|
29
|
+
notifications: NotificationRuleIR[];
|
|
30
|
+
dashboard: {
|
|
31
|
+
widgets: WidgetIR[];
|
|
32
|
+
} | null;
|
|
33
|
+
i18n: {
|
|
34
|
+
defaultLocale: string;
|
|
35
|
+
locales: string[];
|
|
36
|
+
};
|
|
37
|
+
entities: EntityIR[];
|
|
38
|
+
/** Teams inside organizations (built-in Team/TeamMember models); on when an entity uses features.teams. */
|
|
39
|
+
teams?: {
|
|
40
|
+
enabled: boolean;
|
|
41
|
+
};
|
|
42
|
+
/** Scheduled backups (`backups` in forge.config.ts); absent when not configured. */
|
|
43
|
+
backups?: {
|
|
44
|
+
schedule: string | null;
|
|
45
|
+
keep: number;
|
|
46
|
+
};
|
|
47
|
+
/** Public landing page at `/` for signed-out visitors (absent when disabled). Unset texts are filled in, translated, at render time. */
|
|
48
|
+
landing?: LandingIR;
|
|
49
|
+
}
|
|
50
|
+
export interface LandingIR {
|
|
51
|
+
headline?: string;
|
|
52
|
+
subheadline?: string;
|
|
53
|
+
features?: {
|
|
54
|
+
title: string;
|
|
55
|
+
description: string;
|
|
56
|
+
icon: string;
|
|
57
|
+
}[];
|
|
58
|
+
cta?: {
|
|
59
|
+
label: string;
|
|
60
|
+
href: string;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export interface WidgetIR {
|
|
64
|
+
id: string;
|
|
65
|
+
type: "metric" | "chart" | "list";
|
|
66
|
+
title: string;
|
|
67
|
+
entity: string;
|
|
68
|
+
where: Record<string, string>;
|
|
69
|
+
span: 1 | 2 | 3;
|
|
70
|
+
aggregate: "count" | "sum" | "avg" | "min" | "max";
|
|
71
|
+
field?: string;
|
|
72
|
+
chart?: "bar" | "line" | "donut";
|
|
73
|
+
groupBy?: {
|
|
74
|
+
field: string;
|
|
75
|
+
by?: "day" | "week" | "month" | "year";
|
|
76
|
+
};
|
|
77
|
+
limit: number;
|
|
78
|
+
sort?: string;
|
|
79
|
+
columns?: string[];
|
|
80
|
+
/** Metrics: period-over-period comparison (absent when disabled). */
|
|
81
|
+
compare?: {
|
|
82
|
+
period: "week" | "month" | "quarter" | "year";
|
|
83
|
+
dateField: string;
|
|
84
|
+
trend: "up-is-good" | "down-is-good";
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export interface NotificationRuleIR {
|
|
88
|
+
entity: string;
|
|
89
|
+
action: "created" | "updated" | "deleted" | "restored" | "transitioned" | "action";
|
|
90
|
+
name?: string;
|
|
91
|
+
to: ({
|
|
92
|
+
kind: "owner";
|
|
93
|
+
} | {
|
|
94
|
+
kind: "field";
|
|
95
|
+
field: string;
|
|
96
|
+
} | {
|
|
97
|
+
kind: "role";
|
|
98
|
+
role: string;
|
|
99
|
+
})[];
|
|
100
|
+
title: string;
|
|
101
|
+
body?: string;
|
|
102
|
+
link: string;
|
|
103
|
+
email: boolean;
|
|
104
|
+
changed?: string;
|
|
105
|
+
}
|
|
106
|
+
export interface PluginIR {
|
|
107
|
+
name: string;
|
|
108
|
+
package: string;
|
|
109
|
+
version: string;
|
|
110
|
+
options: Record<string, unknown>;
|
|
111
|
+
runtime: {
|
|
112
|
+
api: boolean;
|
|
113
|
+
worker: boolean;
|
|
114
|
+
web: boolean;
|
|
115
|
+
};
|
|
116
|
+
env: {
|
|
117
|
+
name: string;
|
|
118
|
+
description: string;
|
|
119
|
+
default?: string;
|
|
120
|
+
docker?: string;
|
|
121
|
+
required: boolean;
|
|
122
|
+
secret: boolean;
|
|
123
|
+
}[];
|
|
124
|
+
docker: {
|
|
125
|
+
services: Record<string, unknown>;
|
|
126
|
+
volumes: string[];
|
|
127
|
+
};
|
|
128
|
+
nav: {
|
|
129
|
+
label: string;
|
|
130
|
+
path: string;
|
|
131
|
+
icon: string;
|
|
132
|
+
admin: boolean;
|
|
133
|
+
}[];
|
|
134
|
+
reservedPaths: string[];
|
|
135
|
+
/** Entities this plugin added to the schema. */
|
|
136
|
+
entities: string[];
|
|
137
|
+
}
|
|
138
|
+
export type ThemeStyleName = "simple" | "shadow" | "neumorphism" | "glassmorphism" | "skeuomorphism" | "material" | "brutalist" | "editorial" | "aurora" | "terminal" | "clay";
|
|
139
|
+
export type ThemePresetName = "midnight" | "sandstone" | "ocean" | "forest" | "sunset" | "graphite" | "candy" | "paper" | "phosphor";
|
|
140
|
+
export type ThemeFontName = "inter" | "geist" | "plex" | "grotesk" | "serif" | "rounded" | "mono" | "system";
|
|
141
|
+
export interface ThemeIR {
|
|
142
|
+
/** The preset the theme started from (explicit keys already applied on top). */
|
|
143
|
+
preset: ThemePresetName | null;
|
|
144
|
+
style: ThemeStyleName;
|
|
145
|
+
mode: "light" | "dark" | "system";
|
|
146
|
+
radius: "none" | "small" | "medium" | "large" | "full";
|
|
147
|
+
colors: {
|
|
148
|
+
primary: string;
|
|
149
|
+
/** Readable text colour on top of `primary` (WCAG contrast). */
|
|
150
|
+
primaryForeground: string;
|
|
151
|
+
accent: string;
|
|
152
|
+
accentForeground: string;
|
|
153
|
+
danger: string;
|
|
154
|
+
dangerForeground: string;
|
|
155
|
+
};
|
|
156
|
+
font: ThemeFontName;
|
|
157
|
+
/** Resolved font stacks and the Google Fonts stylesheet to load (null for "system"). */
|
|
158
|
+
fonts: {
|
|
159
|
+
body: string;
|
|
160
|
+
display: string;
|
|
161
|
+
stylesheet: string | null;
|
|
162
|
+
};
|
|
163
|
+
layout: "sidebar" | "topbar" | "compact";
|
|
164
|
+
density: "comfortable" | "compact";
|
|
165
|
+
/** Absolute URL or a path served from apps/web/public ("/logo.svg"). */
|
|
166
|
+
logo: string | null;
|
|
167
|
+
tagline: string | null;
|
|
168
|
+
}
|
|
169
|
+
export interface DatabaseIR {
|
|
170
|
+
provider: "postgresql";
|
|
171
|
+
}
|
|
172
|
+
export interface AuthIR {
|
|
173
|
+
emailPassword: boolean;
|
|
174
|
+
registration: boolean;
|
|
175
|
+
social: SocialProvider[];
|
|
176
|
+
emailVerification: boolean;
|
|
177
|
+
passwordReset: boolean;
|
|
178
|
+
twoFactor: boolean;
|
|
179
|
+
passkeys: boolean;
|
|
180
|
+
}
|
|
181
|
+
export interface TenancyIR {
|
|
182
|
+
enabled: boolean;
|
|
183
|
+
model: "organization";
|
|
184
|
+
}
|
|
185
|
+
export type Operation = "create" | "read" | "update" | "delete";
|
|
186
|
+
export interface PermissionIR {
|
|
187
|
+
roles: string[];
|
|
188
|
+
owner: boolean;
|
|
189
|
+
/** Members of the record's team (entities with features.teams). */
|
|
190
|
+
team?: boolean;
|
|
191
|
+
authenticated: boolean;
|
|
192
|
+
public: boolean;
|
|
193
|
+
policies: string[];
|
|
194
|
+
}
|
|
195
|
+
export interface EntityIR {
|
|
196
|
+
name: string;
|
|
197
|
+
label: string;
|
|
198
|
+
pluralLabel: string;
|
|
199
|
+
description?: string;
|
|
200
|
+
/** Prisma model name: the storage name. */
|
|
201
|
+
model: string;
|
|
202
|
+
/** What stored data is keyed on (Prisma model, enum types, link tables, rows naming the entity). Default: the name. */
|
|
203
|
+
storageName: string;
|
|
204
|
+
/** Prisma client delegate, e.g. `prisma.project`. */
|
|
205
|
+
delegate: string;
|
|
206
|
+
tableName: string;
|
|
207
|
+
/** URL segment, e.g. "projects". */
|
|
208
|
+
path: string;
|
|
209
|
+
/** Scalar field used as a record's label. */
|
|
210
|
+
display: string;
|
|
211
|
+
icon: string;
|
|
212
|
+
nav: boolean;
|
|
213
|
+
fields: FieldIR[];
|
|
214
|
+
inverseRelations: InverseRelationIR[];
|
|
215
|
+
/** Name of the relation field that designates the record owner. */
|
|
216
|
+
ownerField?: string;
|
|
217
|
+
/** Name of the relation field to the built-in Team model (features.teams), used by "team" rules. */
|
|
218
|
+
teamField?: string;
|
|
219
|
+
permissions: Record<Operation, PermissionIR>;
|
|
220
|
+
features: {
|
|
221
|
+
timestamps: boolean;
|
|
222
|
+
softDelete: boolean;
|
|
223
|
+
audit: boolean;
|
|
224
|
+
tenant: boolean;
|
|
225
|
+
export: boolean;
|
|
226
|
+
bulkDelete: boolean;
|
|
227
|
+
teams?: boolean;
|
|
228
|
+
/** Record comments with @mentions (built-in Comment model) and the Activity timeline. */
|
|
229
|
+
comments: boolean;
|
|
230
|
+
};
|
|
231
|
+
actions: ActionIR[];
|
|
232
|
+
list: {
|
|
233
|
+
columns: string[];
|
|
234
|
+
defaultSort: SortIR;
|
|
235
|
+
pageSize: number;
|
|
236
|
+
};
|
|
237
|
+
workflow?: WorkflowIR;
|
|
238
|
+
views: ViewsIR;
|
|
239
|
+
/** Computed fields in evaluation order (dependencies first). */
|
|
240
|
+
computedOrder: string[];
|
|
241
|
+
/**
|
|
242
|
+
* Parents whose aggregate computed fields depend on this entity's rows; recomputed after writes here.
|
|
243
|
+
* e.g. Invoice.total = sum(lines.amount) puts { entity: "Invoice", foreignKey: "invoiceId" } on InvoiceLine.
|
|
244
|
+
*/
|
|
245
|
+
aggregateParents: {
|
|
246
|
+
entity: string;
|
|
247
|
+
relation: string;
|
|
248
|
+
foreignKey: string;
|
|
249
|
+
}[];
|
|
250
|
+
}
|
|
251
|
+
export interface WorkflowIR {
|
|
252
|
+
field: string;
|
|
253
|
+
initial: string;
|
|
254
|
+
states: string[];
|
|
255
|
+
transitions: {
|
|
256
|
+
name: string;
|
|
257
|
+
label: string;
|
|
258
|
+
/** Empty means "from any state". */
|
|
259
|
+
from: string[];
|
|
260
|
+
to: string;
|
|
261
|
+
permission: PermissionIR;
|
|
262
|
+
confirm?: string;
|
|
263
|
+
variant: "default" | "danger";
|
|
264
|
+
}[];
|
|
265
|
+
}
|
|
266
|
+
export interface ViewsIR {
|
|
267
|
+
board?: {
|
|
268
|
+
groupBy: string;
|
|
269
|
+
title: string;
|
|
270
|
+
fields: string[];
|
|
271
|
+
};
|
|
272
|
+
calendar?: {
|
|
273
|
+
date: string;
|
|
274
|
+
end?: string;
|
|
275
|
+
title: string;
|
|
276
|
+
};
|
|
277
|
+
map?: {
|
|
278
|
+
location: string;
|
|
279
|
+
title: string;
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
export interface SortIR {
|
|
283
|
+
field: string;
|
|
284
|
+
direction: "asc" | "desc";
|
|
285
|
+
}
|
|
286
|
+
export type DefaultIR = {
|
|
287
|
+
kind: "value";
|
|
288
|
+
value: string | number | boolean | null | object;
|
|
289
|
+
} | {
|
|
290
|
+
kind: "now";
|
|
291
|
+
};
|
|
292
|
+
export interface FieldIR {
|
|
293
|
+
name: string;
|
|
294
|
+
kind: Exclude<FieldKind, never>;
|
|
295
|
+
label: string;
|
|
296
|
+
description?: string;
|
|
297
|
+
required: boolean;
|
|
298
|
+
unique: boolean;
|
|
299
|
+
index: boolean;
|
|
300
|
+
default?: DefaultIR;
|
|
301
|
+
searchable: boolean;
|
|
302
|
+
filterable: boolean;
|
|
303
|
+
sortable: boolean;
|
|
304
|
+
list: boolean;
|
|
305
|
+
hidden: boolean;
|
|
306
|
+
readonly: boolean;
|
|
307
|
+
widget: string;
|
|
308
|
+
placeholder?: string;
|
|
309
|
+
column: string;
|
|
310
|
+
/** Key used in API payloads and query strings (`ownerId` for a relation named `owner`). */
|
|
311
|
+
apiName: string;
|
|
312
|
+
validation: {
|
|
313
|
+
min?: number;
|
|
314
|
+
max?: number;
|
|
315
|
+
minLength?: number;
|
|
316
|
+
maxLength?: number;
|
|
317
|
+
pattern?: string;
|
|
318
|
+
};
|
|
319
|
+
enumValues?: {
|
|
320
|
+
value: string;
|
|
321
|
+
label: string;
|
|
322
|
+
}[];
|
|
323
|
+
currency?: string;
|
|
324
|
+
precision?: number;
|
|
325
|
+
scale?: number;
|
|
326
|
+
relation?: RelationIR;
|
|
327
|
+
/** Field-level access (in addition to the entity's permissions). */
|
|
328
|
+
access?: {
|
|
329
|
+
read?: PermissionIR;
|
|
330
|
+
write?: PermissionIR;
|
|
331
|
+
};
|
|
332
|
+
/** Present on computed fields; `kind` is then the result type and the field is readonly. */
|
|
333
|
+
computed?: {
|
|
334
|
+
expression: string;
|
|
335
|
+
/** Parsed AST from @tekki-forge/expression. */
|
|
336
|
+
ast: unknown;
|
|
337
|
+
fields: string[];
|
|
338
|
+
aggregates: {
|
|
339
|
+
fn: string;
|
|
340
|
+
relation: string;
|
|
341
|
+
field: string | null;
|
|
342
|
+
}[];
|
|
343
|
+
};
|
|
344
|
+
/** Present on file and image fields. */
|
|
345
|
+
file?: {
|
|
346
|
+
image: boolean;
|
|
347
|
+
accept: string[];
|
|
348
|
+
maxSize: number;
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
export interface RelationIR {
|
|
352
|
+
type: "many-to-one" | "many-to-many";
|
|
353
|
+
entity: string;
|
|
354
|
+
model: string;
|
|
355
|
+
delegate: string;
|
|
356
|
+
/** API path of the target when it is a user-defined entity. */
|
|
357
|
+
path?: string;
|
|
358
|
+
builtin: boolean;
|
|
359
|
+
/** Scalar FK for many-to-one; empty for many-to-many (implicit join table). */
|
|
360
|
+
foreignKey: string;
|
|
361
|
+
foreignKeyColumn: string;
|
|
362
|
+
onDelete: "cascade" | "restrict" | "setNull";
|
|
363
|
+
owner: boolean;
|
|
364
|
+
/** Collection name on the target entity. */
|
|
365
|
+
inverse: string;
|
|
366
|
+
/** Field of the target used as its label. */
|
|
367
|
+
targetDisplay: string;
|
|
368
|
+
}
|
|
369
|
+
export interface InverseRelationIR {
|
|
370
|
+
type: "one-to-many" | "many-to-many";
|
|
371
|
+
name: string;
|
|
372
|
+
label: string;
|
|
373
|
+
/** Source entity holding the foreign key. */
|
|
374
|
+
entity: string;
|
|
375
|
+
field: string;
|
|
376
|
+
foreignKey: string;
|
|
377
|
+
}
|
|
378
|
+
export interface ActionIR {
|
|
379
|
+
name: string;
|
|
380
|
+
label: string;
|
|
381
|
+
confirm?: string;
|
|
382
|
+
variant: "default" | "danger";
|
|
383
|
+
permission: PermissionIR;
|
|
384
|
+
}
|
|
385
|
+
/** Built-in models provided by the auth layer that user entities may relate to. */
|
|
386
|
+
export declare const BUILTIN_ENTITIES: {
|
|
387
|
+
readonly User: {
|
|
388
|
+
readonly model: "User";
|
|
389
|
+
readonly delegate: "user";
|
|
390
|
+
readonly display: "name";
|
|
391
|
+
};
|
|
392
|
+
readonly Organization: {
|
|
393
|
+
readonly model: "Organization";
|
|
394
|
+
readonly delegate: "organization";
|
|
395
|
+
readonly display: "name";
|
|
396
|
+
};
|
|
397
|
+
readonly Team: {
|
|
398
|
+
readonly model: "Team";
|
|
399
|
+
readonly delegate: "team";
|
|
400
|
+
readonly display: "name";
|
|
401
|
+
};
|
|
402
|
+
};
|
|
403
|
+
export declare const RESERVED_ENTITY_NAMES: readonly ["User", "Session", "Account", "Verification", "Organization", "Member", "Invitation", "TwoFactor", "Passkey", "AuditLog", "Team", "TeamMember", "ApiKey", "RecordComment"];
|
|
404
|
+
export declare const RESERVED_FIELD_NAMES: readonly ["id", "createdAt", "updatedAt", "deletedAt", "organizationId", "organization"];
|
|
405
|
+
export declare const SPECIAL_PERMISSION_TOKENS: readonly ["owner", "authenticated", "public", "team"];
|
|
406
|
+
export declare const isManyToMany: (f: {
|
|
407
|
+
relation?: {
|
|
408
|
+
type: string;
|
|
409
|
+
};
|
|
410
|
+
}) => boolean;
|
|
411
|
+
export declare const isFileField: (f: {
|
|
412
|
+
kind: string;
|
|
413
|
+
}) => boolean;
|
package/dist/ir.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Built-in models provided by the auth layer that user entities may relate to. */
|
|
2
|
+
export const BUILTIN_ENTITIES = {
|
|
3
|
+
User: { model: "User", delegate: "user", display: "name" },
|
|
4
|
+
Organization: { model: "Organization", delegate: "organization", display: "name" },
|
|
5
|
+
Team: { model: "Team", delegate: "team", display: "name" },
|
|
6
|
+
};
|
|
7
|
+
export const RESERVED_ENTITY_NAMES = [
|
|
8
|
+
"User",
|
|
9
|
+
"Session",
|
|
10
|
+
"Account",
|
|
11
|
+
"Verification",
|
|
12
|
+
"Organization",
|
|
13
|
+
"Member",
|
|
14
|
+
"Invitation",
|
|
15
|
+
"TwoFactor",
|
|
16
|
+
"Passkey",
|
|
17
|
+
"AuditLog",
|
|
18
|
+
"Team",
|
|
19
|
+
"TeamMember",
|
|
20
|
+
"ApiKey",
|
|
21
|
+
"RecordComment",
|
|
22
|
+
];
|
|
23
|
+
export const RESERVED_FIELD_NAMES = ["id", "createdAt", "updatedAt", "deletedAt", "organizationId", "organization"];
|
|
24
|
+
export const SPECIAL_PERMISSION_TOKENS = ["owner", "authenticated", "public", "team"];
|
|
25
|
+
export const isManyToMany = (f) => f.relation?.type === "many-to-many";
|
|
26
|
+
export const isFileField = (f) => f.kind === "file" || f.kind === "image";
|
|
27
|
+
//# sourceMappingURL=ir.js.map
|
package/dist/ir.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ir.js","sourceRoot":"","sources":["../src/ir.ts"],"names":[],"mappings":"AA6TA,mFAAmF;AACnF,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,IAAI,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE;IAC1D,YAAY,EAAE,EAAE,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,EAAE;IAClF,IAAI,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE;CAClD,CAAC;AAEX,MAAM,CAAC,MAAM,qBAAqB,GAAG;IACnC,MAAM;IACN,SAAS;IACT,SAAS;IACT,cAAc;IACd,cAAc;IACd,QAAQ;IACR,YAAY;IACZ,WAAW;IACX,SAAS;IACT,UAAU;IACV,MAAM;IACN,YAAY;IACZ,QAAQ;IACR,eAAe;CACP,CAAC;AAEX,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,IAAI,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,gBAAgB,EAAE,cAAc,CAAU,CAAC;AAE7H,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,CAAU,CAAC;AAE/F,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,CAAkC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE,IAAI,KAAK,cAAc,CAAC;AACxG,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,CAAmB,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,MAAM,IAAI,CAAC,CAAC,IAAI,KAAK,OAAO,CAAC"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { LandingConfig } from "@tekki-forge/core";
|
|
2
|
+
import type { LandingIR } from "./ir.js";
|
|
3
|
+
/** Icons a landing feature can use (the entity icons plus a few marketing ones); @tekki-forge/runtime-admin renders each. */
|
|
4
|
+
export declare const LANDING_ICONS: readonly ["folder", "users", "user", "briefcase", "building", "file-text", "box", "tag", "layers", "star", "calendar", "credit-card", "mail", "map-pin", "package", "shopping-cart", "ticket", "check-square", "zap", "shield", "chart", "sparkles", "lock", "globe", "clock", "bell", "search", "heart", "rocket", "message-square", "workflow", "database"];
|
|
5
|
+
export declare function compileLanding(landing: boolean | LandingConfig | undefined, issue: (path: string, message: string) => void): LandingIR | undefined;
|
package/dist/landing.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** Icons a landing feature can use (the entity icons plus a few marketing ones); @tekki-forge/runtime-admin renders each. */
|
|
2
|
+
export const LANDING_ICONS = [
|
|
3
|
+
"folder", "users", "user", "briefcase", "building", "file-text", "box", "tag", "layers", "star", "calendar", "credit-card", "mail", "map-pin", "package", "shopping-cart", "ticket", "check-square",
|
|
4
|
+
"zap", "shield", "chart", "sparkles", "lock", "globe", "clock", "bell", "search", "heart", "rocket", "message-square", "workflow", "database",
|
|
5
|
+
];
|
|
6
|
+
const MAX_FEATURES = 12;
|
|
7
|
+
const MAX_TEXT = 300;
|
|
8
|
+
/** A same-site path ("/sign-up", "/pricing#plans") or an absolute http(s) URL. */
|
|
9
|
+
function validHref(href) {
|
|
10
|
+
if (href.startsWith("/"))
|
|
11
|
+
return !href.startsWith("//") && !/\s/.test(href);
|
|
12
|
+
try {
|
|
13
|
+
return ["http:", "https:"].includes(new URL(href).protocol);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function compileLanding(landing, issue) {
|
|
20
|
+
if (!landing)
|
|
21
|
+
return undefined;
|
|
22
|
+
if (landing === true)
|
|
23
|
+
return {};
|
|
24
|
+
if (typeof landing !== "object") {
|
|
25
|
+
issue("landing", "must be true, false or { headline, subheadline, features, cta }");
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
if (landing.enabled === false)
|
|
29
|
+
return undefined;
|
|
30
|
+
const out = {};
|
|
31
|
+
const text = (key, value) => {
|
|
32
|
+
if (value === undefined)
|
|
33
|
+
return;
|
|
34
|
+
if (typeof value !== "string" || !value.trim())
|
|
35
|
+
issue(`landing.${key}`, "must be a non-empty string");
|
|
36
|
+
else if (value.length > MAX_TEXT)
|
|
37
|
+
issue(`landing.${key}`, `must be at most ${MAX_TEXT} characters`);
|
|
38
|
+
else
|
|
39
|
+
out[key] = value.trim();
|
|
40
|
+
};
|
|
41
|
+
text("headline", landing.headline);
|
|
42
|
+
text("subheadline", landing.subheadline);
|
|
43
|
+
if (landing.features !== undefined) {
|
|
44
|
+
if (!Array.isArray(landing.features))
|
|
45
|
+
issue("landing.features", "must be a list of { title, description, icon }");
|
|
46
|
+
else {
|
|
47
|
+
if (landing.features.length > MAX_FEATURES)
|
|
48
|
+
issue("landing.features", `at most ${MAX_FEATURES} features`);
|
|
49
|
+
out.features = landing.features.slice(0, MAX_FEATURES).map((f, i) => {
|
|
50
|
+
const at = `landing.features[${i}]`;
|
|
51
|
+
if (typeof f?.title !== "string" || !f.title.trim())
|
|
52
|
+
issue(`${at}.title`, "is required");
|
|
53
|
+
if (typeof f?.description !== "string" || !f.description.trim())
|
|
54
|
+
issue(`${at}.description`, "is required");
|
|
55
|
+
const icon = f?.icon ?? "sparkles";
|
|
56
|
+
if (!LANDING_ICONS.includes(icon))
|
|
57
|
+
issue(`${at}.icon`, `unknown icon "${String(icon)}" (choose ${LANDING_ICONS.join(", ")})`);
|
|
58
|
+
return { title: String(f?.title ?? "").trim(), description: String(f?.description ?? "").trim(), icon };
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (landing.cta !== undefined) {
|
|
63
|
+
const { label, href } = landing.cta ?? {};
|
|
64
|
+
if (typeof label !== "string" || !label.trim())
|
|
65
|
+
issue("landing.cta.label", "is required");
|
|
66
|
+
if (typeof href !== "string" || !validHref(href))
|
|
67
|
+
issue("landing.cta.href", 'must be a path such as "/sign-up" or an http(s) URL');
|
|
68
|
+
if (typeof label === "string" && typeof href === "string")
|
|
69
|
+
out.cta = { label: label.trim(), href };
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=landing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"landing.js","sourceRoot":"","sources":["../src/landing.ts"],"names":[],"mappings":"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"}
|
package/dist/load.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AppDefinition } from "@tekki-forge/core";
|
|
2
|
+
import type { AppIR } from "./ir.js";
|
|
3
|
+
export declare const CONFIG_FILES: string[];
|
|
4
|
+
export declare function findConfig(cwd: string): string | undefined;
|
|
5
|
+
/**
|
|
6
|
+
* Let configs import packages the toolchain can resolve even when the app's own dependencies are not
|
|
7
|
+
* installed yet (e.g. first-party plugins during `forge new --config`).
|
|
8
|
+
*/
|
|
9
|
+
export declare function registerConfigAliases(aliases: Record<string, string>): void;
|
|
10
|
+
/** Load a forge.config.* file (TypeScript supported) and return its default export. */
|
|
11
|
+
export declare function loadConfig(configPath: string): Promise<AppDefinition>;
|
|
12
|
+
export declare function loadAndCompile(configPath: string): Promise<AppIR>;
|
package/dist/load.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { createJiti } from "jiti";
|
|
5
|
+
import { compile } from "./compile.js";
|
|
6
|
+
export const CONFIG_FILES = ["forge.config.ts", "forge.config.mts", "forge.config.js", "forge.config.mjs"];
|
|
7
|
+
export function findConfig(cwd) {
|
|
8
|
+
for (const file of CONFIG_FILES) {
|
|
9
|
+
const candidate = resolve(cwd, file);
|
|
10
|
+
if (existsSync(candidate))
|
|
11
|
+
return candidate;
|
|
12
|
+
}
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
const extraAliases = {};
|
|
16
|
+
/**
|
|
17
|
+
* Let configs import packages the toolchain can resolve even when the app's own dependencies are not
|
|
18
|
+
* installed yet (e.g. first-party plugins during `forge new --config`).
|
|
19
|
+
*/
|
|
20
|
+
export function registerConfigAliases(aliases) {
|
|
21
|
+
Object.assign(extraAliases, aliases);
|
|
22
|
+
}
|
|
23
|
+
/** Load a forge.config.* file (TypeScript supported) and return its default export. */
|
|
24
|
+
export async function loadConfig(configPath) {
|
|
25
|
+
// Resolve @tekki-forge/core from the toolchain so configs load even before the app's dependencies are installed.
|
|
26
|
+
const core = fileURLToPath(import.meta.resolve("@tekki-forge/core"));
|
|
27
|
+
const jiti = createJiti(import.meta.url, { moduleCache: false, fsCache: false, interopDefault: true, alias: { ...extraAliases, "@tekki-forge/core": core } });
|
|
28
|
+
const mod = await jiti.import(resolve(configPath));
|
|
29
|
+
const config = mod.default ?? mod;
|
|
30
|
+
if (!config || typeof config !== "object" || !("entities" in config)) {
|
|
31
|
+
throw new Error(`${configPath} must default-export defineApp({ ... })`);
|
|
32
|
+
}
|
|
33
|
+
return config;
|
|
34
|
+
}
|
|
35
|
+
export async function loadAndCompile(configPath) {
|
|
36
|
+
return compile(await loadConfig(configPath));
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=load.js.map
|
package/dist/load.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load.js","sourceRoot":"","sources":["../src/load.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAGvC,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,iBAAiB,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,kBAAkB,CAAC,CAAC;AAE3G,MAAM,UAAU,UAAU,CAAC,GAAW;IACpC,KAAK,MAAM,IAAI,IAAI,YAAY,EAAE,CAAC;QAChC,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QACrC,IAAI,UAAU,CAAC,SAAS,CAAC;YAAE,OAAO,SAAS,CAAC;IAC9C,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,MAAM,YAAY,GAA2B,EAAE,CAAC;AAEhD;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CAAC,OAA+B;IACnE,MAAM,CAAC,MAAM,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;AACvC,CAAC;AAED,uFAAuF;AACvF,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,UAAkB;IACjD,iHAAiH;IACjH,MAAM,IAAI,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,mBAAmB,CAAC,CAAC,CAAC;IACrE,MAAM,IAAI,GAAG,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,WAAW,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,cAAc,EAAE,IAAI,EAAE,KAAK,EAAE,EAAE,GAAG,YAAY,EAAE,mBAAmB,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC;IAC9J,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,MAAM,CAA8C,OAAO,CAAC,UAAU,CAAC,CAAC,CAAC;IAChG,MAAM,MAAM,GAAI,GAAmC,CAAC,OAAO,IAAK,GAAqB,CAAC;IACtF,IAAI,CAAC,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,CAAC,CAAC,UAAU,IAAI,MAAM,CAAC,EAAE,CAAC;QACrE,MAAM,IAAI,KAAK,CAAC,GAAG,UAAU,yCAAyC,CAAC,CAAC;IAC1E,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,UAAkB;IACrD,OAAO,OAAO,CAAC,MAAM,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC;AAC/C,CAAC"}
|
package/dist/naming.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare function pluralize(word: string): string;
|
|
2
|
+
export declare function singularize(word: string): string;
|
|
3
|
+
export declare function words(input: string): string[];
|
|
4
|
+
export declare const snakeCase: (s: string) => string;
|
|
5
|
+
export declare const kebabCase: (s: string) => string;
|
|
6
|
+
export declare const camelCase: (s: string) => string;
|
|
7
|
+
export declare const pascalCase: (s: string) => string;
|
|
8
|
+
export declare const titleCase: (s: string) => string;
|
|
9
|
+
export declare const lowerFirst: (s: string) => string;
|
|
10
|
+
export declare const isPascalCase: (s: string) => boolean;
|
|
11
|
+
export declare const isCamelCase: (s: string) => boolean;
|
package/dist/naming.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const IRREGULAR = {
|
|
2
|
+
person: "people",
|
|
3
|
+
child: "children",
|
|
4
|
+
man: "men",
|
|
5
|
+
woman: "women",
|
|
6
|
+
status: "statuses",
|
|
7
|
+
data: "data",
|
|
8
|
+
};
|
|
9
|
+
export function pluralize(word) {
|
|
10
|
+
const lower = word.toLowerCase();
|
|
11
|
+
const irregular = IRREGULAR[lower];
|
|
12
|
+
if (irregular)
|
|
13
|
+
return matchCase(word, irregular);
|
|
14
|
+
if (/[^aeiou]y$/i.test(word))
|
|
15
|
+
return word.slice(0, -1) + "ies";
|
|
16
|
+
if (/(s|x|z|ch|sh)$/i.test(word))
|
|
17
|
+
return word + "es";
|
|
18
|
+
return word + "s";
|
|
19
|
+
}
|
|
20
|
+
function matchCase(source, target) {
|
|
21
|
+
return source[0] === source[0].toUpperCase() ? target[0].toUpperCase() + target.slice(1) : target;
|
|
22
|
+
}
|
|
23
|
+
const IRREGULAR_SINGULAR = Object.fromEntries(Object.entries(IRREGULAR).map(([s, p]) => [p, s]));
|
|
24
|
+
export function singularize(word) {
|
|
25
|
+
const lower = word.toLowerCase();
|
|
26
|
+
const irregular = IRREGULAR_SINGULAR[lower];
|
|
27
|
+
if (irregular && irregular !== lower)
|
|
28
|
+
return matchCase(word, irregular);
|
|
29
|
+
if (/[^aeiou]ies$/i.test(word))
|
|
30
|
+
return word.slice(0, -3) + "y";
|
|
31
|
+
if (/(ss|x|z|ch|sh)es$/i.test(word))
|
|
32
|
+
return word.slice(0, -2);
|
|
33
|
+
if (/s$/i.test(word) && !/(ss|us|is)$/i.test(word))
|
|
34
|
+
return word.slice(0, -1);
|
|
35
|
+
return word;
|
|
36
|
+
}
|
|
37
|
+
export function words(input) {
|
|
38
|
+
return input
|
|
39
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
40
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
|
|
41
|
+
.split(/[^A-Za-z0-9]+/)
|
|
42
|
+
.filter(Boolean);
|
|
43
|
+
}
|
|
44
|
+
export const snakeCase = (s) => words(s).map((w) => w.toLowerCase()).join("_");
|
|
45
|
+
export const kebabCase = (s) => words(s).map((w) => w.toLowerCase()).join("-");
|
|
46
|
+
export const camelCase = (s) => words(s)
|
|
47
|
+
.map((w, i) => (i === 0 ? w.toLowerCase() : w[0].toUpperCase() + w.slice(1).toLowerCase()))
|
|
48
|
+
.join("");
|
|
49
|
+
export const pascalCase = (s) => words(s)
|
|
50
|
+
.map((w) => w[0].toUpperCase() + w.slice(1))
|
|
51
|
+
.join("");
|
|
52
|
+
export const titleCase = (s) => words(s)
|
|
53
|
+
.map((w, i) => (i === 0 ? w[0].toUpperCase() + w.slice(1) : w.toLowerCase()))
|
|
54
|
+
.join(" ");
|
|
55
|
+
export const lowerFirst = (s) => s[0].toLowerCase() + s.slice(1);
|
|
56
|
+
export const isPascalCase = (s) => /^[A-Z][A-Za-z0-9]*$/.test(s);
|
|
57
|
+
export const isCamelCase = (s) => /^[a-z][A-Za-z0-9]*$/.test(s);
|
|
58
|
+
//# sourceMappingURL=naming.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"naming.js","sourceRoot":"","sources":["../src/naming.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { NotificationRule } from "@tekki-forge/core";
|
|
2
|
+
import type { EntityIR, NotificationRuleIR } from "./ir.js";
|
|
3
|
+
/** Placeholders like {number} or {customer} in notification texts. */
|
|
4
|
+
export declare function placeholders(text: string): string[];
|
|
5
|
+
export declare function compileNotifications(rules: readonly NotificationRule[] | undefined, entities: EntityIR[], roles: string[], issue: (path: string, message: string) => void): NotificationRuleIR[];
|