@fayz-ai/core 0.1.6
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/app/render.d.ts +16 -0
- package/dist/app/render.d.ts.map +1 -0
- package/dist/blocks/index.d.ts +25 -0
- package/dist/blocks/index.d.ts.map +1 -0
- package/dist/chunk-2NTHNWEH.cjs +32 -0
- package/dist/chunk-2NTHNWEH.cjs.map +1 -0
- package/dist/chunk-5EAZOMVP.js +1609 -0
- package/dist/chunk-5EAZOMVP.js.map +1 -0
- package/dist/chunk-5TKIZVVJ.cjs +16 -0
- package/dist/chunk-5TKIZVVJ.cjs.map +1 -0
- package/dist/chunk-7RTUHSKY.js +3 -0
- package/dist/chunk-7RTUHSKY.js.map +1 -0
- package/dist/chunk-AXTWKF5X.cjs +38 -0
- package/dist/chunk-AXTWKF5X.cjs.map +1 -0
- package/dist/chunk-EGWWQ7AT.cjs +355 -0
- package/dist/chunk-EGWWQ7AT.cjs.map +1 -0
- package/dist/chunk-KAYQFSR6.js +298 -0
- package/dist/chunk-KAYQFSR6.js.map +1 -0
- package/dist/chunk-LSVBDYFQ.cjs +1639 -0
- package/dist/chunk-LSVBDYFQ.cjs.map +1 -0
- package/dist/chunk-SNQW6TRF.cjs +658 -0
- package/dist/chunk-SNQW6TRF.cjs.map +1 -0
- package/dist/chunk-WKZVSKEU.js +26 -0
- package/dist/chunk-WKZVSKEU.js.map +1 -0
- package/dist/chunk-Y2C2FBQP.js +35 -0
- package/dist/chunk-Y2C2FBQP.js.map +1 -0
- package/dist/chunk-YSBXGHAM.js +643 -0
- package/dist/chunk-YSBXGHAM.js.map +1 -0
- package/dist/data/archetype.d.ts +13 -0
- package/dist/data/archetype.d.ts.map +1 -0
- package/dist/data/cached.d.ts +19 -0
- package/dist/data/cached.d.ts.map +1 -0
- package/dist/data/fayz-api.d.ts +19 -0
- package/dist/data/fayz-api.d.ts.map +1 -0
- package/dist/data/index.cjs +32 -0
- package/dist/data/index.cjs.map +1 -0
- package/dist/data/index.d.ts +9 -0
- package/dist/data/index.d.ts.map +1 -0
- package/dist/data/index.js +3 -0
- package/dist/data/index.js.map +1 -0
- package/dist/data/mock.d.ts +7 -0
- package/dist/data/mock.d.ts.map +1 -0
- package/dist/data/resolve.d.ts +22 -0
- package/dist/data/resolve.d.ts.map +1 -0
- package/dist/data/supabase.d.ts +18 -0
- package/dist/data/supabase.d.ts.map +1 -0
- package/dist/data/types.d.ts +23 -0
- package/dist/data/types.d.ts.map +1 -0
- package/dist/entity/index.cjs +28 -0
- package/dist/entity/index.cjs.map +1 -0
- package/dist/entity/index.d.ts +3 -0
- package/dist/entity/index.d.ts.map +1 -0
- package/dist/entity/index.js +3 -0
- package/dist/entity/index.js.map +1 -0
- package/dist/entity/registry.d.ts +20 -0
- package/dist/entity/registry.d.ts.map +1 -0
- package/dist/events/index.d.ts +16 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/i18n/index.cjs +44 -0
- package/dist/i18n/index.cjs.map +1 -0
- package/dist/i18n/index.d.ts +21 -0
- package/dist/i18n/index.d.ts.map +1 -0
- package/dist/i18n/index.js +3 -0
- package/dist/i18n/index.js.map +1 -0
- package/dist/i18n/shell-translations.d.ts +2 -0
- package/dist/i18n/shell-translations.d.ts.map +1 -0
- package/dist/index.cjs +911 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +596 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/cache.d.ts +21 -0
- package/dist/lib/cache.d.ts.map +1 -0
- package/dist/lib/csv.d.ts +14 -0
- package/dist/lib/csv.d.ts.map +1 -0
- package/dist/lib/format.d.ts +7 -0
- package/dist/lib/format.d.ts.map +1 -0
- package/dist/manifest/index.d.ts +80 -0
- package/dist/manifest/index.d.ts.map +1 -0
- package/dist/plugin/createSafeDataProvider.d.ts +14 -0
- package/dist/plugin/createSafeDataProvider.d.ts.map +1 -0
- package/dist/plugin/index.cjs +32 -0
- package/dist/plugin/index.cjs.map +1 -0
- package/dist/plugin/index.d.ts +2 -0
- package/dist/plugin/index.d.ts.map +1 -0
- package/dist/plugin/index.js +3 -0
- package/dist/plugin/index.js.map +1 -0
- package/dist/plugin/runtime.d.ts +31 -0
- package/dist/plugin/runtime.d.ts.map +1 -0
- package/dist/registry/index.d.ts +88 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/router/index.cjs +16 -0
- package/dist/router/index.cjs.map +1 -0
- package/dist/router/index.d.ts +8 -0
- package/dist/router/index.d.ts.map +1 -0
- package/dist/router/index.js +3 -0
- package/dist/router/index.js.map +1 -0
- package/dist/runtime/index.cjs +16 -0
- package/dist/runtime/index.cjs.map +1 -0
- package/dist/runtime/index.d.ts +3 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +3 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/oauth.d.ts +96 -0
- package/dist/runtime/oauth.d.ts.map +1 -0
- package/dist/tenant/index.d.ts +3 -0
- package/dist/tenant/index.d.ts.map +1 -0
- package/dist/types/auth.d.ts +27 -0
- package/dist/types/auth.d.ts.map +1 -0
- package/dist/types/billing.d.ts +17 -0
- package/dist/types/billing.d.ts.map +1 -0
- package/dist/types/crud.d.ts +107 -0
- package/dist/types/crud.d.ts.map +1 -0
- package/dist/types/entities.d.ts +134 -0
- package/dist/types/entities.d.ts.map +1 -0
- package/dist/types/i18n.d.ts +8 -0
- package/dist/types/i18n.d.ts.map +1 -0
- package/dist/types/index.cjs +4 -0
- package/dist/types/index.cjs.map +1 -0
- package/dist/types/index.d.ts +10 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/org.d.ts +96 -0
- package/dist/types/org.d.ts.map +1 -0
- package/dist/types/permissions.d.ts +25 -0
- package/dist/types/permissions.d.ts.map +1 -0
- package/dist/types/plugins.d.ts +314 -0
- package/dist/types/plugins.d.ts.map +1 -0
- package/dist/types/theme.d.ts +16 -0
- package/dist/types/theme.d.ts.map +1 -0
- package/package.json +98 -0
- package/src/app/render.tsx +69 -0
- package/src/blocks/index.tsx +68 -0
- package/src/data/archetype.ts +210 -0
- package/src/data/cached.ts +55 -0
- package/src/data/fayz-api.ts +200 -0
- package/src/data/index.ts +8 -0
- package/src/data/mock.ts +84 -0
- package/src/data/resolve.ts +108 -0
- package/src/data/supabase.ts +169 -0
- package/src/data/types.ts +20 -0
- package/src/entity/index.ts +2 -0
- package/src/entity/registry.ts +42 -0
- package/src/events/index.tsx +82 -0
- package/src/i18n/index.ts +250 -0
- package/src/i18n/shell-translations.ts +1391 -0
- package/src/index.ts +132 -0
- package/src/lib/cache.ts +118 -0
- package/src/lib/csv.ts +54 -0
- package/src/lib/format.ts +58 -0
- package/src/manifest/app-manifest.schema.json +103 -0
- package/src/manifest/index.ts +394 -0
- package/src/plugin/createSafeDataProvider.ts +30 -0
- package/src/plugin/index.ts +1 -0
- package/src/plugin/runtime.ts +313 -0
- package/src/registry/index.ts +163 -0
- package/src/router/index.ts +29 -0
- package/src/runtime/index.ts +15 -0
- package/src/runtime/oauth.ts +219 -0
- package/src/tenant/index.ts +18 -0
- package/src/types/auth.ts +26 -0
- package/src/types/billing.ts +17 -0
- package/src/types/crud.ts +108 -0
- package/src/types/entities.ts +142 -0
- package/src/types/i18n.ts +7 -0
- package/src/types/index.ts +10 -0
- package/src/types/org.ts +94 -0
- package/src/types/permissions.ts +32 -0
- package/src/types/plugins.ts +363 -0
- package/src/types/theme.ts +17 -0
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import type { EntityDef } from '../types/crud'
|
|
2
|
+
import type { LocaleConfig } from '../types/i18n'
|
|
3
|
+
import type { PermissionsConfig } from '../types/permissions'
|
|
4
|
+
import type { BillingConfig } from '../types/billing'
|
|
5
|
+
import type { BlockNode } from '../blocks'
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// AppManifest — THE app, as data.
|
|
9
|
+
//
|
|
10
|
+
// This is the canonical artifact every generated repo commits as
|
|
11
|
+
// `app.manifest.json` and that `fayz upgrade` migrates across the fleet. It is
|
|
12
|
+
// strictly JSON-serializable: no functions, no components — only ids resolved
|
|
13
|
+
// against the registries (see ../registry). `createSaasApp`/`createStorefrontApp`
|
|
14
|
+
// become thin sugar that builds a manifest of this shape.
|
|
15
|
+
//
|
|
16
|
+
// Versioning: bump CURRENT_MANIFEST_VERSION and register a migration whenever
|
|
17
|
+
// the shape changes. The single biggest fleet-maintenance lever — getting it
|
|
18
|
+
// right is why W1 is the "point of no return".
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
export const CURRENT_MANIFEST_VERSION = 2
|
|
22
|
+
|
|
23
|
+
export type BackendProvider = 'supabase' | 'fayz-api' | 'fayz-shop' | 'mock' | 'custom'
|
|
24
|
+
|
|
25
|
+
export interface BackendRef {
|
|
26
|
+
provider: BackendProvider
|
|
27
|
+
/** Provider project ref / URL. Secrets stay in env/platform storage. */
|
|
28
|
+
projectRef?: string
|
|
29
|
+
url?: string
|
|
30
|
+
/** Registered adapter id when provider is 'custom'. */
|
|
31
|
+
adapterId?: string
|
|
32
|
+
/** Public, JSON-safe provider options. Never store secrets here. */
|
|
33
|
+
options?: Record<string, unknown>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** How a manifest page produces its body — exactly one of blocks/entity/component. */
|
|
37
|
+
export interface PageManifest {
|
|
38
|
+
path: string
|
|
39
|
+
label?: string
|
|
40
|
+
icon?: string
|
|
41
|
+
section?: 'main' | 'secondary' | 'settings'
|
|
42
|
+
/** A block tree (pure data). */
|
|
43
|
+
blocks?: BlockNode[]
|
|
44
|
+
/** An entity key → rendered as a CRUD page by the scaffold. */
|
|
45
|
+
entity?: string
|
|
46
|
+
/** A registered page-component id, e.g. 'custom:HarvestBoard'. */
|
|
47
|
+
component?: string
|
|
48
|
+
/** Optional permission gate: feature + action. */
|
|
49
|
+
permission?: { feature: string; action: string }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface PluginRef {
|
|
53
|
+
/** Plugin id, resolved to an installed @fayz-ai/plugin-* at build time. */
|
|
54
|
+
id: string
|
|
55
|
+
/** Pure-data config passed to the plugin (labels, kinds, module flags…). */
|
|
56
|
+
config?: Record<string, unknown>
|
|
57
|
+
enabled?: boolean
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface SurfaceManifest {
|
|
61
|
+
/** Registered scaffold id: 'admin' | 'storefront' | 'portal' | custom. */
|
|
62
|
+
scaffold: string
|
|
63
|
+
plugins?: PluginRef[]
|
|
64
|
+
pages?: PageManifest[]
|
|
65
|
+
/** Surface-specific declarative options (announcement, footer, nav, shipping,
|
|
66
|
+
* catalog seed, onboarding, …) — kept open so scaffolds evolve without a
|
|
67
|
+
* manifest-version bump for additive fields. */
|
|
68
|
+
options?: Record<string, unknown>
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface AppManifest {
|
|
72
|
+
manifestVersion: number
|
|
73
|
+
id: string
|
|
74
|
+
name: string
|
|
75
|
+
backend?: BackendRef
|
|
76
|
+
locale?: LocaleConfig
|
|
77
|
+
/** Theme tokens — SaasTheme or StorefrontTheme depending on surface; kept
|
|
78
|
+
* loose at the core layer to avoid coupling core to surface packages. */
|
|
79
|
+
theme?: Record<string, unknown>
|
|
80
|
+
/** Keyed by surface name ('admin', 'storefront', …). At least one required. */
|
|
81
|
+
surfaces: Record<string, SurfaceManifest>
|
|
82
|
+
/** App-defined entity extensions (vertical-specific tables/fields). */
|
|
83
|
+
entities?: EntityDef[]
|
|
84
|
+
permissions?: PermissionsConfig
|
|
85
|
+
billing?: BillingConfig
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Migration runner — applies sequential migrations from a manifest's version
|
|
90
|
+
// up to CURRENT. Each migration is keyed by the version it upgrades FROM.
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
94
|
+
type AnyManifest = Record<string, any>
|
|
95
|
+
export type ManifestMigration = (m: AnyManifest) => AnyManifest
|
|
96
|
+
|
|
97
|
+
const migrations = new Map<number, ManifestMigration>()
|
|
98
|
+
const supportedBackendProviders: BackendProvider[] = ['supabase', 'fayz-api', 'fayz-shop', 'mock', 'custom']
|
|
99
|
+
const supportedPageSections = new Set(['main', 'secondary', 'settings'])
|
|
100
|
+
const allowedManifestKeys = new Set([
|
|
101
|
+
'manifestVersion',
|
|
102
|
+
'id',
|
|
103
|
+
'name',
|
|
104
|
+
'backend',
|
|
105
|
+
'locale',
|
|
106
|
+
'theme',
|
|
107
|
+
'permissions',
|
|
108
|
+
'billing',
|
|
109
|
+
'entities',
|
|
110
|
+
'surfaces',
|
|
111
|
+
])
|
|
112
|
+
const allowedBackendKeys = new Set(['provider', 'projectRef', 'url', 'adapterId', 'options'])
|
|
113
|
+
const allowedSurfaceKeys = new Set(['scaffold', 'options', 'plugins', 'pages'])
|
|
114
|
+
const allowedPluginRefKeys = new Set(['id', 'config', 'enabled'])
|
|
115
|
+
const allowedPageKeys = new Set(['path', 'label', 'icon', 'section', 'blocks', 'entity', 'component', 'permission'])
|
|
116
|
+
const allowedPermissionKeys = new Set(['feature', 'action'])
|
|
117
|
+
const allowedBlockKeys = new Set(['type', 'id', 'props', 'children'])
|
|
118
|
+
const manifestIdPattern = /^[a-z0-9][a-z0-9-]*$/
|
|
119
|
+
|
|
120
|
+
/** Register a migration that upgrades a manifest FROM `fromVersion` to fromVersion+1. */
|
|
121
|
+
export function registerManifestMigration(fromVersion: number, fn: ManifestMigration): void {
|
|
122
|
+
migrations.set(fromVersion, fn)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Bring any manifest up to CURRENT_MANIFEST_VERSION, or throw if it was built
|
|
126
|
+
* for a newer SDK than this runtime understands. */
|
|
127
|
+
export function migrateManifest(input: AnyManifest): AppManifest {
|
|
128
|
+
let m = { ...input }
|
|
129
|
+
let version = typeof m.manifestVersion === 'number' ? m.manifestVersion : 1
|
|
130
|
+
if (version > CURRENT_MANIFEST_VERSION) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`Manifest version ${version} is newer than this SDK supports (${CURRENT_MANIFEST_VERSION}). Upgrade @fayz-ai/app-runtime.`,
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
while (version < CURRENT_MANIFEST_VERSION) {
|
|
136
|
+
const migrate = migrations.get(version)
|
|
137
|
+
if (!migrate) {
|
|
138
|
+
throw new Error(`No manifest migration registered from version ${version}.`)
|
|
139
|
+
}
|
|
140
|
+
m = migrate(m)
|
|
141
|
+
version += 1
|
|
142
|
+
m.manifestVersion = version
|
|
143
|
+
}
|
|
144
|
+
return m as AppManifest
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Validate the structural invariants the JSON Schema can't fully express.
|
|
148
|
+
* Returns a list of human-readable problems (empty = valid). */
|
|
149
|
+
export function validateManifest(m: AppManifest): string[] {
|
|
150
|
+
const problems: string[] = []
|
|
151
|
+
const manifest = m as unknown as Record<string, unknown>
|
|
152
|
+
|
|
153
|
+
addUnsupportedKeys(manifest, allowedManifestKeys, 'manifest', problems)
|
|
154
|
+
|
|
155
|
+
if (!Number.isInteger(manifest.manifestVersion)) {
|
|
156
|
+
problems.push('manifest.manifestVersion must be an integer')
|
|
157
|
+
} else if (manifest.manifestVersion !== CURRENT_MANIFEST_VERSION) {
|
|
158
|
+
problems.push(`manifest.manifestVersion must be ${CURRENT_MANIFEST_VERSION}`)
|
|
159
|
+
}
|
|
160
|
+
if (typeof manifest.id !== 'string' || !manifest.id.trim()) problems.push('manifest.id is required')
|
|
161
|
+
else if (!manifestIdPattern.test(manifest.id.trim())) {
|
|
162
|
+
problems.push('manifest.id must match /^[a-z0-9][a-z0-9-]*$/')
|
|
163
|
+
}
|
|
164
|
+
if (typeof manifest.name !== 'string' || !manifest.name.trim()) problems.push('manifest.name is required')
|
|
165
|
+
|
|
166
|
+
validateLooseObject(manifest.locale, 'manifest.locale', problems)
|
|
167
|
+
validateLooseObject(manifest.theme, 'manifest.theme', problems)
|
|
168
|
+
validateLooseObject(manifest.permissions, 'manifest.permissions', problems)
|
|
169
|
+
validateLooseObject(manifest.billing, 'manifest.billing', problems)
|
|
170
|
+
if (manifest.entities !== undefined) {
|
|
171
|
+
if (!Array.isArray(manifest.entities)) {
|
|
172
|
+
problems.push('manifest.entities must be an array')
|
|
173
|
+
} else {
|
|
174
|
+
manifest.entities.forEach((entity, index) => {
|
|
175
|
+
if (!isRecord(entity)) problems.push(`manifest.entities[${index}] must be an object`)
|
|
176
|
+
})
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (manifest.backend !== undefined) {
|
|
181
|
+
if (!isRecord(manifest.backend)) {
|
|
182
|
+
problems.push('manifest.backend must be an object')
|
|
183
|
+
} else {
|
|
184
|
+
addUnsupportedKeys(manifest.backend, allowedBackendKeys, 'manifest.backend', problems)
|
|
185
|
+
const provider = manifest.backend.provider
|
|
186
|
+
if (typeof provider !== 'string' || !supportedBackendProviders.includes(provider as BackendProvider)) {
|
|
187
|
+
problems.push(`manifest.backend.provider "${String(provider)}" is not supported`)
|
|
188
|
+
}
|
|
189
|
+
validateOptionalNonEmptyString(manifest.backend.projectRef, 'manifest.backend.projectRef', problems)
|
|
190
|
+
validateOptionalNonEmptyString(manifest.backend.url, 'manifest.backend.url', problems)
|
|
191
|
+
if (provider === 'custom' && !isNonEmptyString(manifest.backend.adapterId)) {
|
|
192
|
+
problems.push('manifest.backend.adapterId is required when provider is "custom"')
|
|
193
|
+
} else if (provider !== 'custom') {
|
|
194
|
+
validateOptionalNonEmptyString(manifest.backend.adapterId, 'manifest.backend.adapterId', problems)
|
|
195
|
+
}
|
|
196
|
+
validateLooseObject(manifest.backend.options, 'manifest.backend.options', problems)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!isRecord(manifest.surfaces) || Object.keys(manifest.surfaces).length === 0) {
|
|
201
|
+
problems.push('manifest.surfaces must declare at least one surface')
|
|
202
|
+
} else {
|
|
203
|
+
for (const [name, surface] of Object.entries(manifest.surfaces)) {
|
|
204
|
+
validateSurfaceManifest(name, surface, problems)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return problems
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function validateSurfaceManifest(name: string, surface: unknown, problems: string[]): void {
|
|
212
|
+
if (!isRecord(surface)) {
|
|
213
|
+
problems.push(`surface "${name}" must be an object`)
|
|
214
|
+
return
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
addUnsupportedKeys(surface, allowedSurfaceKeys, `surface "${name}"`, problems)
|
|
218
|
+
if (!isNonEmptyString(surface.scaffold)) problems.push(`surface "${name}" is missing a scaffold id`)
|
|
219
|
+
validateLooseObject(surface.options, `surface "${name}".options`, problems)
|
|
220
|
+
|
|
221
|
+
if (surface.pages !== undefined) {
|
|
222
|
+
if (!Array.isArray(surface.pages)) {
|
|
223
|
+
problems.push(`surface "${name}".pages must be an array`)
|
|
224
|
+
} else {
|
|
225
|
+
const pagePaths = new Set<string>()
|
|
226
|
+
for (const [index, page] of surface.pages.entries()) {
|
|
227
|
+
validatePageManifest(name, index, page, pagePaths, problems)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (surface.plugins !== undefined) {
|
|
233
|
+
if (!Array.isArray(surface.plugins)) {
|
|
234
|
+
problems.push(`surface "${name}".plugins must be an array`)
|
|
235
|
+
} else {
|
|
236
|
+
const pluginIds = new Set<string>()
|
|
237
|
+
for (const [index, plugin] of surface.plugins.entries()) {
|
|
238
|
+
validatePluginRef(name, index, plugin, pluginIds, problems)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function validatePageManifest(
|
|
245
|
+
surfaceName: string,
|
|
246
|
+
index: number,
|
|
247
|
+
page: unknown,
|
|
248
|
+
pagePaths: Set<string>,
|
|
249
|
+
problems: string[],
|
|
250
|
+
): void {
|
|
251
|
+
const pageLabel = `surface "${surfaceName}" page #${index + 1}`
|
|
252
|
+
if (!isRecord(page)) {
|
|
253
|
+
problems.push(`${pageLabel} must be an object`)
|
|
254
|
+
return
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
addUnsupportedKeys(page, allowedPageKeys, pageLabel, problems)
|
|
258
|
+
|
|
259
|
+
if (!isNonEmptyString(page.path)) {
|
|
260
|
+
problems.push(`${pageLabel} has an empty path`)
|
|
261
|
+
} else {
|
|
262
|
+
const normalizedPath = page.path.trim()
|
|
263
|
+
if (pagePaths.has(normalizedPath)) {
|
|
264
|
+
problems.push(`surface "${surfaceName}" declares duplicate page path "${normalizedPath}"`)
|
|
265
|
+
} else {
|
|
266
|
+
pagePaths.add(normalizedPath)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (page.label !== undefined && typeof page.label !== 'string') problems.push(`${pageLabel}.label must be a string`)
|
|
271
|
+
if (page.icon !== undefined && typeof page.icon !== 'string') problems.push(`${pageLabel}.icon must be a string`)
|
|
272
|
+
if (page.section !== undefined && (typeof page.section !== 'string' || !supportedPageSections.has(page.section))) {
|
|
273
|
+
problems.push(`${pageLabel}.section must be one of main, secondary, settings`)
|
|
274
|
+
}
|
|
275
|
+
if (page.entity !== undefined && !isNonEmptyString(page.entity)) problems.push(`${pageLabel}.entity must be a non-empty string`)
|
|
276
|
+
if (page.component !== undefined && !isNonEmptyString(page.component)) {
|
|
277
|
+
problems.push(`${pageLabel}.component must be a non-empty string`)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const kinds = [page.blocks, page.entity, page.component].filter((x) => x != null)
|
|
281
|
+
if (kinds.length !== 1) {
|
|
282
|
+
problems.push(`${pageLabel} must set exactly one of blocks/entity/component`)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (page.permission !== undefined) {
|
|
286
|
+
validatePermission(page.permission, `${pageLabel}.permission`, problems)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (page.blocks !== undefined) {
|
|
290
|
+
if (!Array.isArray(page.blocks)) {
|
|
291
|
+
problems.push(`${pageLabel}.blocks must be an array`)
|
|
292
|
+
} else {
|
|
293
|
+
page.blocks.forEach((block, blockIndex) => {
|
|
294
|
+
validateBlockManifest(block, `${pageLabel}.blocks[${blockIndex}]`, problems)
|
|
295
|
+
})
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function validatePluginRef(
|
|
301
|
+
surfaceName: string,
|
|
302
|
+
index: number,
|
|
303
|
+
plugin: unknown,
|
|
304
|
+
pluginIds: Set<string>,
|
|
305
|
+
problems: string[],
|
|
306
|
+
): void {
|
|
307
|
+
const pluginLabel = `surface "${surfaceName}" plugin #${index + 1}`
|
|
308
|
+
if (!isRecord(plugin)) {
|
|
309
|
+
problems.push(`${pluginLabel} must be an object`)
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
addUnsupportedKeys(plugin, allowedPluginRefKeys, pluginLabel, problems)
|
|
314
|
+
|
|
315
|
+
if (!isNonEmptyString(plugin.id)) {
|
|
316
|
+
problems.push(`${pluginLabel} has an empty id`)
|
|
317
|
+
} else {
|
|
318
|
+
const normalizedId = plugin.id.trim()
|
|
319
|
+
if (pluginIds.has(normalizedId)) {
|
|
320
|
+
problems.push(`surface "${surfaceName}" declares duplicate plugin id "${normalizedId}"`)
|
|
321
|
+
} else {
|
|
322
|
+
pluginIds.add(normalizedId)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
validateLooseObject(plugin.config, `${pluginLabel}.config`, problems)
|
|
327
|
+
if (plugin.enabled !== undefined && typeof plugin.enabled !== 'boolean') {
|
|
328
|
+
problems.push(`${pluginLabel}.enabled must be a boolean`)
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function validatePermission(permission: unknown, path: string, problems: string[]): void {
|
|
333
|
+
if (!isRecord(permission)) {
|
|
334
|
+
problems.push(`${path} must be an object`)
|
|
335
|
+
return
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
addUnsupportedKeys(permission, allowedPermissionKeys, path, problems)
|
|
339
|
+
if (!isNonEmptyString(permission.feature)) problems.push(`${path}.feature is required`)
|
|
340
|
+
if (!isNonEmptyString(permission.action)) problems.push(`${path}.action is required`)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function validateBlockManifest(block: unknown, path: string, problems: string[]): void {
|
|
344
|
+
if (!isRecord(block)) {
|
|
345
|
+
problems.push(`${path} must be an object`)
|
|
346
|
+
return
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
addUnsupportedKeys(block, allowedBlockKeys, path, problems)
|
|
350
|
+
if (!isNonEmptyString(block.type)) problems.push(`${path}.type is required`)
|
|
351
|
+
validateLooseObject(block.props, `${path}.props`, problems)
|
|
352
|
+
if (block.id !== undefined && typeof block.id !== 'string') problems.push(`${path}.id must be a string`)
|
|
353
|
+
|
|
354
|
+
if (block.children !== undefined) {
|
|
355
|
+
if (!Array.isArray(block.children)) {
|
|
356
|
+
problems.push(`${path}.children must be an array`)
|
|
357
|
+
} else {
|
|
358
|
+
block.children.forEach((child, index) => validateBlockManifest(child, `${path}.children[${index}]`, problems))
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function addUnsupportedKeys(
|
|
364
|
+
value: Record<string, unknown>,
|
|
365
|
+
allowedKeys: Set<string>,
|
|
366
|
+
path: string,
|
|
367
|
+
problems: string[],
|
|
368
|
+
): void {
|
|
369
|
+
for (const key of Object.keys(value)) {
|
|
370
|
+
if (!allowedKeys.has(key)) {
|
|
371
|
+
problems.push(`${path}.${key} is not part of AppManifest v2`)
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function validateLooseObject(value: unknown, path: string, problems: string[]): void {
|
|
377
|
+
if (value !== undefined && !isRecord(value)) {
|
|
378
|
+
problems.push(`${path} must be an object`)
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function validateOptionalNonEmptyString(value: unknown, path: string, problems: string[]): void {
|
|
383
|
+
if (value !== undefined && !isNonEmptyString(value)) {
|
|
384
|
+
problems.push(`${path} must be a non-empty string`)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
389
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function isNonEmptyString(value: unknown): value is string {
|
|
393
|
+
return typeof value === 'string' && value.trim().length > 0
|
|
394
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { getSupabaseClientOptional } from '../data/supabase'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* createSafeDataProvider — the standard lazy provider resolver every plugin
|
|
6
|
+
* repeated by hand. Returns a Proxy that, on first method call, resolves to the
|
|
7
|
+
* Supabase-backed provider when a Supabase client is configured, otherwise the
|
|
8
|
+
* mock provider. Resolution is deferred so importing a plugin never forces a
|
|
9
|
+
* Supabase connection, and memoized so it only happens once.
|
|
10
|
+
*
|
|
11
|
+
* const provider = options?.dataProvider ?? createSafeDataProvider(
|
|
12
|
+
* () => createSupabaseCrmProvider(providerOptions),
|
|
13
|
+
* () => createMockCrmProvider(),
|
|
14
|
+
* )
|
|
15
|
+
*/
|
|
16
|
+
export function createSafeDataProvider<T extends object>(
|
|
17
|
+
makeSupabase: () => T,
|
|
18
|
+
makeMock: () => T,
|
|
19
|
+
): T {
|
|
20
|
+
let resolved: T | null = null
|
|
21
|
+
const get = (): T => {
|
|
22
|
+
if (!resolved) {
|
|
23
|
+
resolved = getSupabaseClientOptional() ? makeSupabase() : makeMock()
|
|
24
|
+
}
|
|
25
|
+
return resolved
|
|
26
|
+
}
|
|
27
|
+
return new Proxy({} as T, {
|
|
28
|
+
get: (_target, prop) => (...args: any[]) => (get() as any)[prop](...args),
|
|
29
|
+
})
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { definePlugin, resolvePluginRuntime, getWidgetsForZone, PluginRuntimeProvider, usePluginRuntime, usePluginRuntimeOptional } from './runtime'
|