@conduction/nextcloud-vue 2.2.0-vue3.13 → 2.2.0-vue3.14
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/package.json +2 -1
- package/scripts/.jsdoc-baselines.json +254 -0
- package/scripts/build-validators.js +174 -0
- package/scripts/check-css-entry.js +118 -0
- package/scripts/check-docs.js +724 -0
- package/scripts/check-integration-build.js +180 -0
- package/scripts/check-integration-parity.js +540 -0
- package/scripts/check-jsdoc.js +230 -0
- package/scripts/check-peer-consistency.js +161 -0
- package/scripts/generate-nl-icons.mjs +98 -0
- package/scripts/precommit-regenerate-features.sh +33 -0
- package/scripts/precommit-regenerate-partials.sh +68 -0
- package/scripts/sync-l10n-ellipsis.js +46 -0
- package/scripts/update-fleet-manifest-fixtures.js +116 -0
- package/scripts/vue3-compile-sweep.cjs +77 -0
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Integration registry parity gate.
|
|
5
|
+
*
|
|
6
|
+
* Per AD-11/AD-13 of the `pluggable-integration-registry` umbrella,
|
|
7
|
+
* every registered integration must declare BOTH a sidebar `tab`
|
|
8
|
+
* component AND a `widget` component (the widget can be a thin shell
|
|
9
|
+
* around the tab's data for MVP parity, but it must exist). The
|
|
10
|
+
* registry throws at `register()` time when one is missing — this
|
|
11
|
+
* gate catches the same condition statically, before merge, for the
|
|
12
|
+
* descriptors shipped in this repo's `src/integrations/`. This is the
|
|
13
|
+
* HARD half of the gate (exit 1 on failure).
|
|
14
|
+
*
|
|
15
|
+
* ADR-066 extension — server↔JS leaf parity (WARN-only, bake-in epoch).
|
|
16
|
+
* A leaf has two faces (see OpenRegister `LeafDescriptor`): a server-side
|
|
17
|
+
* descriptor contributed via `RegisterLeafProvidersEvent` (discoverable in
|
|
18
|
+
* the `openregister.integrations.leaves` capability) and a JS
|
|
19
|
+
* `registerIntegration({ id })` that mounts the tab + widget on
|
|
20
|
+
* `window.OCA.OpenRegister.integrations`. The descriptor `id` MUST equal
|
|
21
|
+
* the JS registration id (ADR-019 parity). When only one side exists you
|
|
22
|
+
* get a PHANTOM render surface: a `render-surface` descriptor discoverable
|
|
23
|
+
* server-side whose JS widget never registered (renders nothing), or a JS
|
|
24
|
+
* registration with no server descriptor (invisible to the capability).
|
|
25
|
+
* This gate cross-references the two WITHIN the repo it runs against
|
|
26
|
+
* (`process.cwd()`), flagging orphans both ways. It is WARN-only: it never
|
|
27
|
+
* changes the exit code, matching the fleet's gate-introduction pattern
|
|
28
|
+
* (introduce as a warning, promote to blocking after a bake-in epoch once
|
|
29
|
+
* the fleet is clean). See the deferred follow-up notes in
|
|
30
|
+
* {@link crossReferenceServerLeaves}.
|
|
31
|
+
*
|
|
32
|
+
* Run via `npm run check:integration-parity` (wired into the
|
|
33
|
+
* Code Quality CI workflow and the pre-commit hook). The cross-ref phase
|
|
34
|
+
* only activates when the repo it runs against carries server-side leaf
|
|
35
|
+
* descriptors (a `lib/**` PHP `new LeafDescriptor(...)`), so it is a no-op
|
|
36
|
+
* for this JS-only library's own CI and only speaks up inside a consuming
|
|
37
|
+
* app repo (hermiq, openconnector, …) that ships both faces.
|
|
38
|
+
*
|
|
39
|
+
* Exit codes:
|
|
40
|
+
* 0 — every descriptor is parity-complete (WARN-only cross-ref findings
|
|
41
|
+
* do NOT change this)
|
|
42
|
+
* 1 — at least one descriptor is missing `tab` or `widget`, or
|
|
43
|
+
* carries a malformed `id` / `label`
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
'use strict'
|
|
47
|
+
|
|
48
|
+
const path = require('path')
|
|
49
|
+
const fs = require('fs')
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Run the parity gate: the HARD tab/widget check on this repo's built-in
|
|
53
|
+
* descriptors, then the WARN-only ADR-066 server↔JS cross-ref against the
|
|
54
|
+
* target repo (`process.cwd()`). Calls `process.exit()` with the HARD
|
|
55
|
+
* result — the cross-ref findings never change it.
|
|
56
|
+
*
|
|
57
|
+
* @return {void}
|
|
58
|
+
*/
|
|
59
|
+
function main() {
|
|
60
|
+
// --- HARD half: tab/widget parity on THIS repo's built-in descriptors. --
|
|
61
|
+
const failures = []
|
|
62
|
+
|
|
63
|
+
// The built-in registrations are the only descriptors that live in
|
|
64
|
+
// this repo. Leaf changes in other repos register their own and run
|
|
65
|
+
// their own copy of this gate (and the hydra quality gate enforces it
|
|
66
|
+
// cross-repo). Importing the module gives us the normalised array
|
|
67
|
+
// without spinning up Vue.
|
|
68
|
+
let builtinIntegrations
|
|
69
|
+
try {
|
|
70
|
+
// eslint-disable-next-line global-require, import/no-unresolved
|
|
71
|
+
builtinIntegrations = require(path.resolve(__dirname, '../src/integrations/builtin/index.js')).builtinIntegrations
|
|
72
|
+
} catch (e) {
|
|
73
|
+
// Fall back to a source scan if the module can't be required in
|
|
74
|
+
// this environment (e.g. ESM-only toolchains). We look for the
|
|
75
|
+
// per-id descriptor files and verify each names a `tab:` and
|
|
76
|
+
// `widget:` key. This is coarser but never throws.
|
|
77
|
+
const dir = path.resolve(__dirname, '../src/integrations/builtin')
|
|
78
|
+
for (const file of fs.readdirSync(dir)) {
|
|
79
|
+
if (file === 'index.js' || !file.endsWith('.js')) {
|
|
80
|
+
continue
|
|
81
|
+
}
|
|
82
|
+
const src = fs.readFileSync(path.join(dir, file), 'utf8')
|
|
83
|
+
// A mount-mode descriptor satisfies parity via mount+unmount
|
|
84
|
+
// instead of tab+widget (openregister#2127).
|
|
85
|
+
if (/renderMode\s*:\s*['"]mount['"]/.test(src)) {
|
|
86
|
+
if (!/\bmount\s*:/.test(src)) {
|
|
87
|
+
failures.push(`${file}: renderMode 'mount' but no \`mount:\` key found`)
|
|
88
|
+
}
|
|
89
|
+
if (!/\bunmount\s*:/.test(src)) {
|
|
90
|
+
failures.push(`${file}: renderMode 'mount' but no \`unmount:\` key found`)
|
|
91
|
+
}
|
|
92
|
+
continue
|
|
93
|
+
}
|
|
94
|
+
if (!/\btab\s*:/.test(src)) {
|
|
95
|
+
failures.push(`${file}: no \`tab:\` key found`)
|
|
96
|
+
}
|
|
97
|
+
if (!/\bwidget\s*:/.test(src)) {
|
|
98
|
+
failures.push(`${file}: no \`widget:\` key found`)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (Array.isArray(builtinIntegrations)) {
|
|
104
|
+
for (const d of builtinIntegrations) {
|
|
105
|
+
const label = d && typeof d.id === 'string' && d.id !== '' ? d.id : '(unknown)'
|
|
106
|
+
if (typeof d.id !== 'string' || d.id === '') {
|
|
107
|
+
failures.push(`${label}: missing or empty \`id\``)
|
|
108
|
+
}
|
|
109
|
+
if (typeof d.label !== 'string' || d.label === '') {
|
|
110
|
+
failures.push(`${label}: missing or empty \`label\``)
|
|
111
|
+
}
|
|
112
|
+
// Parity now accepts either render pair (openregister#2127, ADR-066):
|
|
113
|
+
// a `mount` descriptor is complete with mount+unmount; a `component`
|
|
114
|
+
// (default) descriptor keeps the tab+widget requirement.
|
|
115
|
+
if (d && d.renderMode === 'mount') {
|
|
116
|
+
if (typeof d.mount !== 'function') {
|
|
117
|
+
failures.push(`${label}: renderMode 'mount' missing required \`mount\` function`)
|
|
118
|
+
}
|
|
119
|
+
if (typeof d.unmount !== 'function') {
|
|
120
|
+
failures.push(`${label}: renderMode 'mount' missing required \`unmount\` function`)
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
if (d.tab === undefined || d.tab === null) {
|
|
124
|
+
failures.push(`${label}: missing required \`tab\` component`)
|
|
125
|
+
}
|
|
126
|
+
if (d.widget === undefined || d.widget === null) {
|
|
127
|
+
failures.push(`${label}: missing required \`widget\` component`)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// --- HARD half, part 2: barrel-export completeness. --------------------
|
|
134
|
+
// A parity-complete descriptor that no consumer can import is still dead.
|
|
135
|
+
// `flowIntegration` shipped for months in `builtinIntegrations[]`, with a
|
|
136
|
+
// bespoke tab and card, and was exported from neither `src/integrations/
|
|
137
|
+
// index.js` nor `src/index.js` — so `import { flowIntegration } from
|
|
138
|
+
// '@conduction/nextcloud-vue'` yielded `undefined` and registering it was a
|
|
139
|
+
// silent no-op. Nothing threw at any layer. This check makes that state a
|
|
140
|
+
// build failure instead of a support ticket.
|
|
141
|
+
failures.push(...checkBarrelExports())
|
|
142
|
+
|
|
143
|
+
report(failures)
|
|
144
|
+
|
|
145
|
+
// --- WARN half (ADR-066): server↔JS leaf parity on the target repo. -----
|
|
146
|
+
// Never changes the exit code (bake-in epoch). Runs against process.cwd()
|
|
147
|
+
// so the hydra gate — which cd's into the app repo before invoking this
|
|
148
|
+
// check via the app's wrapper — picks up the app's own PHP + JS.
|
|
149
|
+
try {
|
|
150
|
+
const { warnings, ran } = crossReferenceServerLeaves(process.cwd())
|
|
151
|
+
reportCrossRef(warnings, ran)
|
|
152
|
+
} catch (e) {
|
|
153
|
+
// A cross-ref scan must NEVER break the gate — it is advisory. Surface
|
|
154
|
+
// the reason and carry on with the hard-check exit code.
|
|
155
|
+
// eslint-disable-next-line no-console
|
|
156
|
+
console.error(` (server↔JS leaf cross-ref skipped: ${e && e.message})`)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
process.exit(failures.length === 0 ? 0 : 1)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Verify that every descriptor listed in `builtinIntegrations[]` is also a
|
|
164
|
+
* named export of BOTH public barrels — `src/integrations/index.js` and the
|
|
165
|
+
* package root `src/index.js`.
|
|
166
|
+
*
|
|
167
|
+
* The identifiers are read from the array literal in
|
|
168
|
+
* `src/integrations/builtin/index.js` rather than from the required module,
|
|
169
|
+
* because the array holds descriptor *objects* at runtime and the binding
|
|
170
|
+
* names (which are what a consumer imports) are only visible in the source.
|
|
171
|
+
*
|
|
172
|
+
* @return {string[]} One failure string per descriptor missing from a barrel.
|
|
173
|
+
*/
|
|
174
|
+
function checkBarrelExports() {
|
|
175
|
+
const failures = []
|
|
176
|
+
const root = path.resolve(__dirname, '..')
|
|
177
|
+
const builtinIndex = path.join(root, 'src', 'integrations', 'builtin', 'index.js')
|
|
178
|
+
|
|
179
|
+
let source
|
|
180
|
+
try {
|
|
181
|
+
source = fs.readFileSync(builtinIndex, 'utf8')
|
|
182
|
+
} catch {
|
|
183
|
+
// Nothing to check against — never turn a missing file into a false
|
|
184
|
+
// "everything is fine"; say so and let the tab/widget half stand.
|
|
185
|
+
return ['src/integrations/builtin/index.js is unreadable — barrel-export check skipped']
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const arrayMatch = source.match(/export\s+const\s+builtinIntegrations\s*=\s*\[([\s\S]*?)\]/)
|
|
189
|
+
if (!arrayMatch) {
|
|
190
|
+
return ['could not locate the `builtinIntegrations` array literal — barrel-export check skipped']
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const names = arrayMatch[1]
|
|
194
|
+
.split('\n')
|
|
195
|
+
.map((line) => line.replace(/\/\/.*$/, '').trim().replace(/,$/, ''))
|
|
196
|
+
.filter((name) => /^[A-Za-z_$][\w$]*$/.test(name))
|
|
197
|
+
|
|
198
|
+
if (names.length === 0) {
|
|
199
|
+
return ['`builtinIntegrations` array parsed as empty — barrel-export check skipped']
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const barrels = [
|
|
203
|
+
['src/integrations/index.js', path.join(root, 'src', 'integrations', 'index.js')],
|
|
204
|
+
['src/index.js', path.join(root, 'src', 'index.js')],
|
|
205
|
+
]
|
|
206
|
+
|
|
207
|
+
for (const [label, file] of barrels) {
|
|
208
|
+
let barrel
|
|
209
|
+
try {
|
|
210
|
+
barrel = fs.readFileSync(file, 'utf8')
|
|
211
|
+
} catch {
|
|
212
|
+
failures.push(`${label} is unreadable — cannot verify integration exports`)
|
|
213
|
+
continue
|
|
214
|
+
}
|
|
215
|
+
// Strip comments BEFORE matching. The first cut of this check searched
|
|
216
|
+
// the raw file and passed while `flowIntegration` was absent from every
|
|
217
|
+
// export statement — the docblock explaining the bug mentioned the name,
|
|
218
|
+
// and a bare substring search cannot tell prose from an export. Prose
|
|
219
|
+
// restating a symbol is not the symbol.
|
|
220
|
+
const code = barrel
|
|
221
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
222
|
+
.replace(/(^|[^:])\/\/.*$/gm, '$1')
|
|
223
|
+
for (const name of names) {
|
|
224
|
+
if (!new RegExp(`\\b${name}\\b`).test(code)) {
|
|
225
|
+
failures.push(
|
|
226
|
+
`${name} is in builtinIntegrations[] but is not exported from ${label} — `
|
|
227
|
+
+ 'consumers importing it get `undefined` and register nothing, silently',
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return failures
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Recursively collect files under `root` that match `test(filename)`, up to
|
|
238
|
+
* `maxDepth` levels deep. Skips `node_modules`, `vendor`, `.git`, and `dist`.
|
|
239
|
+
* Returns [] when `root` does not exist. Never throws on an unreadable dir.
|
|
240
|
+
*
|
|
241
|
+
* @param {string} root The directory to walk.
|
|
242
|
+
* @param {(name: string) => boolean} test Filename predicate.
|
|
243
|
+
* @param {number} [maxDepth] Maximum recursion depth (default 6).
|
|
244
|
+
*
|
|
245
|
+
* @return {string[]} Absolute paths of matching files.
|
|
246
|
+
*/
|
|
247
|
+
function collectFiles(root, test, maxDepth = 6) {
|
|
248
|
+
const out = []
|
|
249
|
+
if (!fs.existsSync(root)) {
|
|
250
|
+
return out
|
|
251
|
+
}
|
|
252
|
+
const skip = new Set(['node_modules', 'vendor', '.git', 'dist', 'build'])
|
|
253
|
+
const walk = (dir, depth) => {
|
|
254
|
+
if (depth > maxDepth) {
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
let entries
|
|
258
|
+
try {
|
|
259
|
+
entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
260
|
+
} catch (e) {
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
for (const ent of entries) {
|
|
264
|
+
if (ent.isDirectory()) {
|
|
265
|
+
if (!skip.has(ent.name)) {
|
|
266
|
+
walk(path.join(dir, ent.name), depth + 1)
|
|
267
|
+
}
|
|
268
|
+
} else if (ent.isFile() && test(ent.name)) {
|
|
269
|
+
out.push(path.join(dir, ent.name))
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
walk(root, 0)
|
|
274
|
+
return out
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Extract the server-side render-surface leaf descriptors declared in a
|
|
279
|
+
* repo's PHP (`lib/**`). Finds every `new LeafDescriptor( … )` constructor
|
|
280
|
+
* call, reads its `id:` argument (a string literal, or a `self::CONST`
|
|
281
|
+
* resolved from a `const CONST = '...'` in the same file), and records
|
|
282
|
+
* whether the descriptor's `kinds:` array contains `KIND_RENDER_SURFACE`
|
|
283
|
+
* (either the `LeafDescriptor::KIND_RENDER_SURFACE` constant or the
|
|
284
|
+
* literal `'render-surface'`).
|
|
285
|
+
*
|
|
286
|
+
* Deliberately regex-based: this must run in a plain Node CI step with no
|
|
287
|
+
* PHP toolchain. It is a static heuristic, hence WARN-only.
|
|
288
|
+
*
|
|
289
|
+
* @param {string} repoRoot The repo root to scan.
|
|
290
|
+
*
|
|
291
|
+
* @return {Array<{id: string, renderSurface: boolean, renderMode: string, file: string}>} The
|
|
292
|
+
* discovered descriptors (`renderMode` is `'mount'` or `'component'`).
|
|
293
|
+
*/
|
|
294
|
+
function collectServerDescriptors(repoRoot) {
|
|
295
|
+
const descriptors = []
|
|
296
|
+
const phpFiles = collectFiles(path.join(repoRoot, 'lib'), (n) => n.endsWith('.php'))
|
|
297
|
+
// The `id:` value inside a `new LeafDescriptor(` argument list — a
|
|
298
|
+
// single/double-quoted literal or a `self::CONST` / `static::CONST`.
|
|
299
|
+
const idRe = /\bid:\s*(?:'([^']+)'|"([^"]+)"|(?:self|static)::([A-Z0-9_]+))/
|
|
300
|
+
for (const file of phpFiles) {
|
|
301
|
+
let src
|
|
302
|
+
try {
|
|
303
|
+
src = fs.readFileSync(file, 'utf8')
|
|
304
|
+
} catch (e) {
|
|
305
|
+
continue
|
|
306
|
+
}
|
|
307
|
+
if (!src.includes('new LeafDescriptor(')) {
|
|
308
|
+
continue
|
|
309
|
+
}
|
|
310
|
+
// Constant table for `self::CONST` id resolution within the file.
|
|
311
|
+
const consts = {}
|
|
312
|
+
const constRe = /\bconst\s+([A-Z0-9_]+)\s*=\s*(?:'([^']+)'|"([^"]+)")/g
|
|
313
|
+
let cm
|
|
314
|
+
while ((cm = constRe.exec(src)) !== null) {
|
|
315
|
+
consts[cm[1]] = cm[2] || cm[3]
|
|
316
|
+
}
|
|
317
|
+
// Walk each constructor call as a bounded window (the argument list
|
|
318
|
+
// up to a reasonable length — descriptors are short value objects).
|
|
319
|
+
let idx = 0
|
|
320
|
+
while ((idx = src.indexOf('new LeafDescriptor(', idx)) !== -1) {
|
|
321
|
+
const window = src.slice(idx, idx + 1200)
|
|
322
|
+
const m = idRe.exec(window)
|
|
323
|
+
let id = null
|
|
324
|
+
if (m) {
|
|
325
|
+
id = m[1] || m[2] || (m[3] ? consts[m[3]] : null)
|
|
326
|
+
}
|
|
327
|
+
const renderSurface = /KIND_RENDER_SURFACE/.test(window)
|
|
328
|
+
|| /'render-surface'|"render-surface"/.test(window)
|
|
329
|
+
// renderMode carried on the descriptor (openregister#2127): a
|
|
330
|
+
// `RENDER_MODE_MOUNT` constant or a `renderMode: 'mount'` literal.
|
|
331
|
+
const renderMode = /RENDER_MODE_MOUNT/.test(window)
|
|
332
|
+
|| /renderMode\s*:\s*'mount'|renderMode\s*:\s*"mount"/.test(window)
|
|
333
|
+
? 'mount'
|
|
334
|
+
: 'component'
|
|
335
|
+
if (id) {
|
|
336
|
+
descriptors.push({ id, renderSurface, renderMode, file: path.relative(repoRoot, file) })
|
|
337
|
+
}
|
|
338
|
+
idx += 'new LeafDescriptor('.length
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
return descriptors
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Extract the JS integration registration ids declared in a repo's
|
|
346
|
+
* `src/**` — every `registerIntegration({ id: '...' })` CALL site (the
|
|
347
|
+
* `export function registerIntegration` DEFINITION in the shared library is
|
|
348
|
+
* excluded). These are the ids mounted on `window.OCA.OpenRegister.integrations`.
|
|
349
|
+
*
|
|
350
|
+
* @param {string} repoRoot The repo root to scan.
|
|
351
|
+
*
|
|
352
|
+
* @return {Array<{id: string, renderMode: string, file: string}>} The discovered
|
|
353
|
+
* registrations (`renderMode` is `'mount'` or `'component'`).
|
|
354
|
+
*/
|
|
355
|
+
function collectJsRegistrations(repoRoot) {
|
|
356
|
+
const regs = []
|
|
357
|
+
const jsFiles = collectFiles(
|
|
358
|
+
path.join(repoRoot, 'src'),
|
|
359
|
+
(n) => n.endsWith('.js') || n.endsWith('.ts') || n.endsWith('.vue'),
|
|
360
|
+
)
|
|
361
|
+
// `registerIntegration(` followed (within a small window) by `id: '...'`.
|
|
362
|
+
// The negative lookbehind on `function ` excludes the library definition.
|
|
363
|
+
const callRe = /registerIntegration\s*\(/g
|
|
364
|
+
const idRe = /\bid:\s*(?:'([^']+)'|"([^"]+)"|`([^`]+)`)/
|
|
365
|
+
for (const file of jsFiles) {
|
|
366
|
+
let src
|
|
367
|
+
try {
|
|
368
|
+
src = fs.readFileSync(file, 'utf8')
|
|
369
|
+
} catch (e) {
|
|
370
|
+
continue
|
|
371
|
+
}
|
|
372
|
+
let cm
|
|
373
|
+
while ((cm = callRe.exec(src)) !== null) {
|
|
374
|
+
const before = src.slice(Math.max(0, cm.index - 20), cm.index)
|
|
375
|
+
if (/function\s+$/.test(before)) {
|
|
376
|
+
continue // the `export function registerIntegration(` definition
|
|
377
|
+
}
|
|
378
|
+
const window = src.slice(cm.index, cm.index + 400)
|
|
379
|
+
const m = idRe.exec(window)
|
|
380
|
+
const id = m ? (m[1] || m[2] || m[3]) : null
|
|
381
|
+
// renderMode declared on the JS registration (openregister#2127).
|
|
382
|
+
const renderMode = /renderMode\s*:\s*'mount'|renderMode\s*:\s*"mount"|renderMode\s*:\s*`mount`/.test(window)
|
|
383
|
+
? 'mount'
|
|
384
|
+
: 'component'
|
|
385
|
+
if (id) {
|
|
386
|
+
regs.push({ id, renderMode, file: path.relative(repoRoot, file) })
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return regs
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Cross-reference server-side render-surface leaf descriptors against JS
|
|
395
|
+
* `registerIntegration` ids within one repo, producing advisory warnings
|
|
396
|
+
* for orphans both ways (ADR-066).
|
|
397
|
+
*
|
|
398
|
+
* Scoped pragmatically (WARN-first): the cross-ref only runs when the repo
|
|
399
|
+
* carries at least one server-side `LeafDescriptor` (i.e. it is a consuming
|
|
400
|
+
* app repo that ships both faces, not this JS-only library). This keeps the
|
|
401
|
+
* check silent for the nextcloud-vue library's own CI (whose built-in
|
|
402
|
+
* registrations correlate to PHP descriptors that live in the consuming
|
|
403
|
+
* apps, not here) and avoids false positives on repos that only own one
|
|
404
|
+
* face.
|
|
405
|
+
*
|
|
406
|
+
* DEFERRED (documented follow-up, not implemented in this pass):
|
|
407
|
+
* - Correlating this library's own `builtinIntegrations` against the PHP
|
|
408
|
+
* descriptors that live in EACH consuming app (a true cross-repo join);
|
|
409
|
+
* today each app runs this gate against its own tree.
|
|
410
|
+
* - Reading the `openregister.integrations.leaves` capability payload at
|
|
411
|
+
* runtime and asserting it against the JS registry live (this static
|
|
412
|
+
* pass approximates it from the PHP source).
|
|
413
|
+
* - Promoting the WARN findings to a hard failure once the fleet bakes in
|
|
414
|
+
* clean (flip `reportCrossRef` to push into `failures`).
|
|
415
|
+
*
|
|
416
|
+
* @param {string} repoRoot The repo root to scan (usually `process.cwd()`).
|
|
417
|
+
*
|
|
418
|
+
* @return {{ran: boolean, warnings: string[]}} Whether the cross-ref ran
|
|
419
|
+
* (server descriptors present) and any advisory warnings.
|
|
420
|
+
*/
|
|
421
|
+
function crossReferenceServerLeaves(repoRoot) {
|
|
422
|
+
const descriptors = collectServerDescriptors(repoRoot)
|
|
423
|
+
if (descriptors.length === 0) {
|
|
424
|
+
// No server-side leaf face in this repo — nothing to correlate.
|
|
425
|
+
return { ran: false, warnings: [] }
|
|
426
|
+
}
|
|
427
|
+
const registrations = collectJsRegistrations(repoRoot)
|
|
428
|
+
const jsIds = new Set(registrations.map((r) => r.id))
|
|
429
|
+
const phpIds = new Set(descriptors.map((d) => d.id))
|
|
430
|
+
const jsModeById = new Map(registrations.map((r) => [r.id, r.renderMode]))
|
|
431
|
+
const warnings = []
|
|
432
|
+
|
|
433
|
+
// renderMode cross-layer correlation (openregister#2127 / ADR-066): for a
|
|
434
|
+
// render-surface leaf present on both sides, the server descriptor's
|
|
435
|
+
// renderMode MUST equal the JS registration's under the shared id.
|
|
436
|
+
for (const d of descriptors) {
|
|
437
|
+
if (!d.renderSurface || !jsModeById.has(d.id)) {
|
|
438
|
+
continue
|
|
439
|
+
}
|
|
440
|
+
const jsMode = jsModeById.get(d.id)
|
|
441
|
+
if (d.renderMode !== jsMode) {
|
|
442
|
+
warnings.push(
|
|
443
|
+
`render-surface leaf "${d.id}" (${d.file}) declares renderMode `
|
|
444
|
+
+ `"${d.renderMode}" server-side but "${jsMode}" in its JS `
|
|
445
|
+
+ 'registration — renderMode MUST match across layers (ADR-066).',
|
|
446
|
+
)
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Phantom render surface: a render-surface descriptor discoverable in the
|
|
451
|
+
// capability whose JS widget never registered.
|
|
452
|
+
for (const d of descriptors) {
|
|
453
|
+
if (d.renderSurface && !jsIds.has(d.id)) {
|
|
454
|
+
warnings.push(
|
|
455
|
+
`render-surface leaf descriptor "${d.id}" (${d.file}) has NO matching `
|
|
456
|
+
+ 'registerIntegration({ id }) in src/** — phantom render surface (the '
|
|
457
|
+
+ 'capability advertises a tab/widget that never mounts).',
|
|
458
|
+
)
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
// Orphan JS: a registration with no server descriptor of any kind — the
|
|
462
|
+
// widget mounts but the leaf is invisible to the capability.
|
|
463
|
+
for (const r of registrations) {
|
|
464
|
+
if (!phpIds.has(r.id)) {
|
|
465
|
+
warnings.push(
|
|
466
|
+
`registerIntegration id "${r.id}" (${r.file}) has NO matching server-side `
|
|
467
|
+
+ 'LeafDescriptor in lib/** — orphan JS registration (mounts on '
|
|
468
|
+
+ 'window.OCA.OpenRegister.integrations but is not discoverable via the '
|
|
469
|
+
+ 'openregister.integrations.leaves capability).',
|
|
470
|
+
)
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
return { ran: true, warnings }
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Print the hard-check result to stdout/stderr.
|
|
478
|
+
*
|
|
479
|
+
* @param {string[]} list Failure messages (empty when all good).
|
|
480
|
+
*
|
|
481
|
+
* @return {void}
|
|
482
|
+
*/
|
|
483
|
+
function report(list) {
|
|
484
|
+
if (list.length === 0) {
|
|
485
|
+
// eslint-disable-next-line no-console
|
|
486
|
+
console.log('✓ integration parity: every registered integration has both a tab and a widget')
|
|
487
|
+
return
|
|
488
|
+
}
|
|
489
|
+
// eslint-disable-next-line no-console
|
|
490
|
+
console.error('✗ integration parity gate failed:')
|
|
491
|
+
for (const f of list) {
|
|
492
|
+
// eslint-disable-next-line no-console
|
|
493
|
+
console.error(` - ${f}`)
|
|
494
|
+
}
|
|
495
|
+
// eslint-disable-next-line no-console
|
|
496
|
+
console.error('\nEvery integration registered on window.OCA.OpenRegister.integrations')
|
|
497
|
+
// eslint-disable-next-line no-console
|
|
498
|
+
console.error('must declare BOTH a `tab` and a `widget` component (AD-11/AD-13).')
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Print the WARN-only server↔JS cross-ref result. Never fails the build.
|
|
503
|
+
*
|
|
504
|
+
* @param {string[]} warnings Advisory warning messages.
|
|
505
|
+
* @param {boolean} ran Whether the cross-ref actually ran (server leaves present).
|
|
506
|
+
*
|
|
507
|
+
* @return {void}
|
|
508
|
+
*/
|
|
509
|
+
function reportCrossRef(warnings, ran) {
|
|
510
|
+
if (!ran) {
|
|
511
|
+
return
|
|
512
|
+
}
|
|
513
|
+
if (warnings.length === 0) {
|
|
514
|
+
// eslint-disable-next-line no-console
|
|
515
|
+
console.log('✓ server↔JS leaf parity (ADR-066): every render-surface descriptor has a JS registration and vice-versa')
|
|
516
|
+
return
|
|
517
|
+
}
|
|
518
|
+
// eslint-disable-next-line no-console
|
|
519
|
+
console.warn('⚠ server↔JS leaf parity (ADR-066) — advisory (WARN-only, does not fail the gate):')
|
|
520
|
+
for (const w of warnings) {
|
|
521
|
+
// eslint-disable-next-line no-console
|
|
522
|
+
console.warn(` - ${w}`)
|
|
523
|
+
}
|
|
524
|
+
// eslint-disable-next-line no-console
|
|
525
|
+
console.warn('\nThe server LeafDescriptor id MUST equal the JS registerIntegration id (ADR-019 / ADR-066).')
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// Run the gate only on direct invocation (`node check-integration-parity.js`),
|
|
529
|
+
// never on `require()` — so unit tests can import the helpers below without the
|
|
530
|
+
// CLI calling process.exit() and killing the test runner.
|
|
531
|
+
if (require.main === module) {
|
|
532
|
+
main()
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Exported for unit testing.
|
|
536
|
+
module.exports = {
|
|
537
|
+
collectServerDescriptors,
|
|
538
|
+
collectJsRegistrations,
|
|
539
|
+
crossReferenceServerLeaves,
|
|
540
|
+
}
|