Skip to main content

validateManifestV2

Validate a v2 Conduction app manifest against the v2 JSON Schema. The schema validation uses a pre-compiled standalone Ajv validator (generated at build time from src/schemas/app-manifest-v2.schema.json), plus runtime post-checks the schema cannot express (cross-field arithmetic, $id uniqueness, @resolve: sentinel path rejection).

The v1 validator (validateManifest) stays hand-rolled for backward compatibility. The two coexist behind the $schema dispatch in validateManifest() — most consumers just call validateManifest(manifest) and get the correct path automatically.

CSP compliance​

Nextcloud enforces a strict Content Security Policy that does not allow unsafe-eval. Ajv's default mode JIT-compiles schema validators using new Function(), which triggers an EvalError and prevents the Vue app from mounting in any v2-adopting app.

To fix this, the v2 schema validator is pre-compiled at build time using Ajv's standalone code generation (ajv/dist/standalone). The generator script (scripts/build-validators.js) is invoked automatically via npm run prebuild and npm run pretest. The output (src/utils/validateManifestV2.compiled.js) is a plain JS module containing static code — no new Function() anywhere at runtime.

The compiled file is gitignored and regenerated on every install cycle. If you see an import error for validateManifestV2.compiled.js, run npm run build:validators once.

Signature​

import { validateManifestV2 } from '@conduction/nextcloud-vue'

const { valid, errors } = validateManifestV2(manifest)
ArgumentTypeDescription
manifestobjectThe v2 manifest object to validate. MUST declare a $schema field pointing to the v2 schema URL (the dispatcher in validateManifest() enforces this; calling validateManifestV2 directly skips the dispatch).

Return value​

KeyTypeDescription
validbooleantrue when errors is empty.
errorsstring[]Bracket-path error messages matching the v1 validator's format, e.g. '/pages/0/widgets/2: gridX + gridWidth (8 + 6 = 14) exceeds 12 for slot "body"'.

Rules enforced​

Schema-level (via Ajv, see app-manifest-v2.schema.json):

  • $schema, version, menu, pages are required at the top level.
  • version matches the semver pattern.
  • pages[].type is one of the 11 closed-enum values: index | detail | dashboard | logs | settings | chat | files | form | wiki | map | custom.
  • pages[].widgets[] (when present) entries follow the unified widgetEntry shape: widgetKey, slot, gridX, gridY, gridWidth, gridHeight, optional props, tabGroup, dataSource, _note.
  • slot is either one of body | sidebar | header-actions | footer | modal or matches the pattern ^(tab|section):.+.
  • Per-slot grid constraints: sidebar requires gridWidth: 1; header-actions requires gridY: 0.
  • pages[].actions[] (when present) entries have a type field in handler | open-modal | open-page | navigate. Missing type defaults to handler (via Ajv useDefaults: true, preserving v1.3.0 backward compatibility).
  • type: "custom" pages MUST include a _note field documenting why decomposition was not feasible.
  • All v1.3.0 features carry forward unchanged in the schema (dataSource, @resolve: sentinels, dynamic menus, named-view sidebar, cardComponent, settings tabs[], runtime, dependencies).

Post-schema (runtime checks):

  • pages[].id MUST be unique across the array.
  • For every widgets[] entry: gridX + gridWidth ≤ 12 (only enforced on 12-column slots — body, header-actions, footer, modal, tab:*, section:*).
  • @resolve:<key> sentinels are REJECTED on registry-key and router-invariant paths: pages[].id, pages[].route, pages[].component, pages[].headerComponent, pages[].actionsComponent, pages[].slots.*, menu[].id, menu[].route, dependencies[], version. Mirrors v1's behaviour.
  • stats-block widgets: exactly one of dataSource | props.entries[].
  • Wave-2 endpoint binding (schema v2.14.0, $defs/endpointSource): exactly one data binding per widget — stat/delta content.source | content.endpointSource (an editor-seeded EMPTY source blob doesn't count), chart dataSource | props.endpointSource, object-table props.source | props.endpointSource. Enforced on BOTH the v2 pages[].widgets[] grid and the legacy pages[].config.widgets[] dashboard catalog. See useEndpointSource.
  • Wave-3 vocabulary (schema v2.15.0): the chart dataSource.aggregate OBJECT form ({ groupBy, metric, sumField?, topN?, otherBucket?, labelResolve? }) + sibling drilldown ({ route, filterParam }); four new $defs/action types (open-form requires schema, api-call requires url, toggle requires writeUrl, refresh) each guarded by an if/then required-field branch; the shared $defs/visibleWhen predicate (endpoint | source | local field, op enum, value) reusable on actions; and typed props.content for the workspace-filter and kb-search catalog widgets. Free-form keys still pass (additionalProperties: true); the new branches type + document the load-bearing ones.
  • api-call payload + download (schema v2.20.0): payload (JSON body, @-tokens resolve RECURSIVELY at any nesting depth — objects/arrays of objects, e.g. { dataRefs: [{ id: '@objectId' }] }) alongside the legacy params (shallow, one level deep); download (boolean, request a blob response and trigger a browser file download) and filename (token-interpolated fallback filename). All three are additive/optional — existing params-only actions are unaffected. See dispatchAction.

Dispatch contract​

validateManifest() reads manifest.$schema:

  • ends with /app-manifest-v2.schema.json → calls validateManifestV2()
  • absent or matches v1 URL → existing v1 path
  • present but matches neither → falls back to v1 + console.warn once per module load

Consumers should call validateManifest() in most cases. validateManifestV2() is exported for explicit v2-only paths (e.g., the codemod CLI, the hydra gate-manifest-validates gate).

See also​