Skip to content

Commit b0b0822

Browse files
authored
fix(manifest): select the schema variant from the manifest own $schema (#136)
`tests/validate-manifest.js` hardcoded the v1 app-manifest schema while `src/manifest.json` declares v2: "$schema": ".../app-manifest-v2.schema.json" The v1 schema sets `additionalProperties: false` on `$defs.widgetDef` and predates the `content` / `icon` / `integrationId` widget keys that `CnDetailPage` actually renders, so it reported errors against a manifest that renders correctly and passes its own declared schema with zero errors. The validator now picks the schema variant from the manifest itself, as openconnector and scholiq already did. Ported verbatim from portaliq#59, where the same defect was diagnosed against a live browser: the pages render their widgets fully, and the migration the errors seemed to demand would have blanked every icon in the app (`icon` holds an MDI component name, neither `iconClass` nor `iconUrl`). Also drops the two hardcoded `/tmp/worktrees/...` schema candidates, which cannot resolve on a CI runner. This repo is the scaffold every new app copies, so the stale validator reproduces the same false failure in each one.
1 parent 5f5f92d commit b0b0822

1 file changed

Lines changed: 51 additions & 21 deletions

File tree

tests/validate-manifest.js

Lines changed: 51 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
// Copyright (C) 2026 Conduction B.V.
44
//
55
// validate-manifest.js — schema-validates src/manifest.json against the
6-
// @conduction/nextcloud-vue v1.1.0 app-manifest schema using Ajv.
6+
// @conduction/nextcloud-vue app-manifest schema using Ajv.
77
//
88
// Usage:
99
// node tests/validate-manifest.js
@@ -12,16 +12,19 @@
1212
// 0 — manifest validates against the schema with zero errors
1313
// 1 — manifest fails validation (or schema/manifest cannot be loaded)
1414
//
15-
// Schema lookup order (first hit wins):
16-
// 1. Env var APP_MANIFEST_SCHEMA — explicit absolute path to a schema JSON
17-
// 2. node_modules/@conduction/nextcloud-vue/src/schemas/app-manifest.schema.json
18-
// 3. ../nextcloud-vue/src/schemas/app-manifest.schema.json (sibling worktree)
19-
// 4. /tmp/worktrees/nextcloud-vue-manifest-v1/src/schemas/app-manifest.schema.json (v1.2.0 consolidation worktree)
20-
// 5. /tmp/worktrees/nextcloud-vue-page-type-extensions/src/schemas/app-manifest.schema.json (v1.1.0 fallback)
15+
// Schema VARIANT is chosen from the manifest's own `$schema` — a manifest
16+
// pointing at `app-manifest-v2.schema.json` is validated against the v2
17+
// schema, anything else against v1. Hardcoding v1 here (as this script did
18+
// until 2026-08) validated a v2 manifest against the v1 schema, whose
19+
// `$defs.widgetDef` sets `additionalProperties: false` and predates the
20+
// `content` / `icon` / `integrationId` widget keys that CnDetailPage actually
21+
// renders — reporting errors against a manifest that renders correctly and
22+
// passes its own declared schema with zero errors.
2123
//
22-
// The fourth / fifth options exist because the v1.x schema is not yet
23-
// released to npm; the consolidated `manifest-v1` worktree carries the
24-
// canonical v1.2.0 source. Once published, options 1 and 2 take over.
24+
// Schema lookup order (first hit wins), for the selected variant:
25+
// 1. Env var APP_MANIFEST_SCHEMA — explicit absolute path to a schema JSON
26+
// 2. node_modules/@conduction/nextcloud-vue/src/schemas/<variant>
27+
// 3. ../nextcloud-vue/src/schemas/<variant> (sibling worktree)
2528

2629
'use strict'
2730

@@ -32,16 +35,34 @@ const REPO_ROOT = path.resolve(__dirname, '..')
3235

3336
const MANIFEST_PATH = path.join(REPO_ROOT, 'src', 'manifest.json')
3437

35-
const SCHEMA_CANDIDATES = [
36-
process.env.APP_MANIFEST_SCHEMA,
37-
path.join(REPO_ROOT, 'node_modules', '@conduction', 'nextcloud-vue', 'src', 'schemas', 'app-manifest.schema.json'),
38-
path.join(REPO_ROOT, '..', 'nextcloud-vue', 'src', 'schemas', 'app-manifest.schema.json'),
39-
'/tmp/worktrees/nextcloud-vue-manifest-v1/src/schemas/app-manifest.schema.json',
40-
'/tmp/worktrees/nextcloud-vue-page-type-extensions/src/schemas/app-manifest.schema.json',
41-
].filter(Boolean)
38+
/**
39+
* Determine whether the manifest is v2 (points to the v2 $schema URL).
40+
*
41+
* @param {object} manifest Parsed manifest object.
42+
* @return {boolean} True when the manifest targets the v2 schema.
43+
*/
44+
function isV2Manifest(manifest) {
45+
return typeof manifest.$schema === 'string' && manifest.$schema.includes('app-manifest-v2')
46+
}
47+
48+
/**
49+
* Build the ordered list of schema file candidates for a given manifest.
50+
* V2 manifests prefer the v2 schema file; v1 manifests prefer the v1 file.
51+
*
52+
* @param {object} manifest Parsed manifest object.
53+
* @return {string[]} Candidate paths (env override first, then node_modules, then sibling worktree).
54+
*/
55+
function schemaCandidates(manifest) {
56+
const schemaFile = isV2Manifest(manifest) ? 'app-manifest-v2.schema.json' : 'app-manifest.schema.json'
57+
return [
58+
process.env.APP_MANIFEST_SCHEMA,
59+
path.join(REPO_ROOT, 'node_modules', '@conduction', 'nextcloud-vue', 'src', 'schemas', schemaFile),
60+
path.join(REPO_ROOT, '..', 'nextcloud-vue', 'src', 'schemas', schemaFile),
61+
].filter(Boolean)
62+
}
4263

43-
function findSchemaPath() {
44-
for (const candidate of SCHEMA_CANDIDATES) {
64+
function findSchemaPath(manifest) {
65+
for (const candidate of schemaCandidates(manifest)) {
4566
try {
4667
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
4768
return candidate
@@ -98,7 +119,13 @@ function structuralLint(manifest) {
98119
}
99120
if (!Array.isArray(manifest.menu)) errors.push('top-level: menu (array) is required')
100121
if (!Array.isArray(manifest.pages)) errors.push('top-level: pages (array) is required')
101-
const allowedTypes = new Set(['index', 'detail', 'dashboard', 'logs', 'settings', 'chat', 'files', 'custom'])
122+
// Mirrors $defs/page/properties/type in app-manifest-v2.schema.json — every
123+
// entry is a type CnAppRoot actually renders. Do NOT widen this to make a
124+
// manifest pass; widen it only once the renderer has gained the type.
125+
const allowedTypes = new Set([
126+
'chat', 'custom', 'dashboard', 'detail', 'files', 'form', 'index',
127+
'logs', 'map', 'roadmap', 'search', 'settings', 'wiki',
128+
])
102129
const seenIds = new Set()
103130
for (let i = 0; i < (manifest.pages || []).length; i++) {
104131
const page = manifest.pages[i]
@@ -136,7 +163,10 @@ function main() {
136163
console.log(`[validate-manifest] manifest.version: ${manifest.version}`)
137164
console.log(`[validate-manifest] pages: ${(manifest.pages || []).length}`)
138165

139-
const schemaPath = findSchemaPath()
166+
console.log(`[validate-manifest] manifest.$schema: ${manifest.$schema || '(unset)'}`)
167+
console.log(`[validate-manifest] schema variant: ${isV2Manifest(manifest) ? 'v2' : 'v1'}`)
168+
169+
const schemaPath = findSchemaPath(manifest)
140170
if (!schemaPath) {
141171
console.warn('[validate-manifest] no schema candidate resolved; falling back to structural lint.')
142172
const errors = structuralLint(manifest)

0 commit comments

Comments
 (0)