@conduction/nextcloud-vue 2.2.0-vue3.13 → 2.2.0-vue3.15

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conduction/nextcloud-vue",
3
- "version": "2.2.0-vue3.13",
3
+ "version": "2.2.0-vue3.15",
4
4
  "description": "Shared Vue component library for Conduction Nextcloud apps — complements @nextcloud/vue with higher-level components, OpenRegister integration, and NL Design System support",
5
5
  "license": "EUPL-1.2",
6
6
  "author": "Conduction B.V. <info@conduction.nl>",
@@ -24,7 +24,9 @@
24
24
  "src/",
25
25
  "css/",
26
26
  "eslint/",
27
+ "stylelint/",
27
28
  "l10n/",
29
+ "scripts/",
28
30
  "testing/",
29
31
  "webpack/"
30
32
  ],
@@ -0,0 +1,254 @@
1
+ {
2
+ "CnActionButtons": 1,
3
+ "CnActionsBar": 1,
4
+ "CnActionsMenu": 1,
5
+ "CnAdminSettingsShell": 1,
6
+ "CnAdvancedFormDialog": 0.65,
7
+ "CnAiCompanion": 1,
8
+ "CnAnalyticsReportCreate": 1,
9
+ "CnAnalyticsReportPicker": 1,
10
+ "CnAppLoading": 0.67,
11
+ "CnAppNav": 1,
12
+ "CnAppRoot": 0.92,
13
+ "CnAuditTrailCard": 0.92,
14
+ "CnAuditTrailWidget": 1,
15
+ "CnAuditTrailWidgetForm": 1,
16
+ "CnBannerWidget": 1,
17
+ "CnBannerWidgetForm": 1,
18
+ "CnBodySections": 1,
19
+ "CnBookmarkCreate": 1,
20
+ "CnBookmarkPicker": 1,
21
+ "CnCalendarEventCreate": 1,
22
+ "CnCalendarEventPicker": 1,
23
+ "CnCalendarWidget": 1,
24
+ "CnCalendarWidgetForm": 1,
25
+ "CnCard": 0.59,
26
+ "CnCardGrid": 0.54,
27
+ "CnCellRenderer": 1,
28
+ "CnChartWidget": 1,
29
+ "CnChartWidgetForm": 1,
30
+ "CnChatPage": 0.69,
31
+ "CnCollectivePageCreate": 1,
32
+ "CnCollectivePagePicker": 1,
33
+ "CnColorPicker": 1,
34
+ "CnCommandPalette": 1,
35
+ "CnConfigurationCard": 0.17,
36
+ "CnContactCreate": 1,
37
+ "CnContactPicker": 1,
38
+ "CnContainerWidget": 1,
39
+ "CnContainerWidgetForm": 1,
40
+ "CnContextMenu": 1,
41
+ "CnCopyDialog": 0.82,
42
+ "CnCospendCreate": 1,
43
+ "CnCospendPicker": 1,
44
+ "CnCredentials": 1,
45
+ "CnDashboardGrid": 0.94,
46
+ "CnDashboardPage": 0.94,
47
+ "CnDashTileWidget": 1,
48
+ "CnDashTileWidgetForm": 1,
49
+ "CnDataMatrix": 1,
50
+ "CnDataTable": 1,
51
+ "CnDateRangePicker": 1,
52
+ "CnDeckCardCreate": 1,
53
+ "CnDeckCardPicker": 1,
54
+ "CnDeleteDialog": 0.82,
55
+ "CnDeltaWidget": 1,
56
+ "CnDeltaWidgetForm": 1,
57
+ "CnDependencyMissing": 1,
58
+ "CnDetailCard": 0.5,
59
+ "CnDetailGrid": 1,
60
+ "CnDetailPage": 1,
61
+ "CnDividerWidget": 1,
62
+ "CnDividerWidgetForm": 1,
63
+ "CnEmailPicker": 1,
64
+ "CnExportWizard": 1,
65
+ "CnFacetSidebar": 0.78,
66
+ "CnFeaturesAndRoadmapLink": 1,
67
+ "CnFeaturesAndRoadmapPage": 1,
68
+ "CnFeaturesAndRoadmapSidebar": 1,
69
+ "CnFeaturesAndRoadmapView": 1,
70
+ "CnFeaturesTab": 1,
71
+ "CnFederationStatus": 1,
72
+ "CnFieldPicker": 1,
73
+ "CnFileManager": 1,
74
+ "CnFilesCard": 1,
75
+ "CnFilesPage": 0.72,
76
+ "CnFilesWidget": 1,
77
+ "CnFilesWidgetForm": 1,
78
+ "CnFilterBar": 0.75,
79
+ "CnFilterRowsEditor": 1,
80
+ "CnFkResolveCell": 1,
81
+ "CnFlowCanvas": 1,
82
+ "CnFlowOperationPicker": 1,
83
+ "CnFlowRunsWidget": 1,
84
+ "CnFlowRunsWidgetForm": 1,
85
+ "CnFolderSidebar": 1,
86
+ "CnFolderTree": 1,
87
+ "CnFormBuilder": 1,
88
+ "CnFormDialog": 0.72,
89
+ "CnFormPage": 1,
90
+ "CnGaugeWidget": 1,
91
+ "CnGaugeWidgetForm": 1,
92
+ "CnGraphCanvas": 1,
93
+ "CnHeaderWidget": 1,
94
+ "CnHeaderWidgetForm": 1,
95
+ "CnIcon": 1,
96
+ "CnIconBrowser": 1,
97
+ "CnIconPicker": 1,
98
+ "CnImageWidget": 1,
99
+ "CnImageWidgetForm": 1,
100
+ "CnIndexPage": 0.72,
101
+ "CnIndexSidebar": 0.69,
102
+ "CnInfoWidget": 1,
103
+ "CnIntegrationCard": 1,
104
+ "CnIntegrationTab": 1,
105
+ "CnIntegrationWidget": 1,
106
+ "CnIntegrationWidgetGrid": 1,
107
+ "CnInteractionFormWidget": 1,
108
+ "CnItemCard": 0.5,
109
+ "CnJsonViewer": 1,
110
+ "CnKbSearchWidget": 1,
111
+ "CnKpiGrid": 0.67,
112
+ "CnLabelWidget": 1,
113
+ "CnLabelWidgetForm": 1,
114
+ "CnLeafMountHost": 1,
115
+ "CnLifecycleActions": 1,
116
+ "CnLinkButtonWidget": 1,
117
+ "CnLinkButtonWidgetForm": 1,
118
+ "CnLinksWidget": 1,
119
+ "CnLinksWidgetForm": 1,
120
+ "CnLockedBanner": 1,
121
+ "CnLogsPage": 0.63,
122
+ "CnMapPage": 1,
123
+ "CnMapPoiCreate": 1,
124
+ "CnMapPoiPicker": 1,
125
+ "CnMapWidget": 1,
126
+ "CnMapWidgetForm": 1,
127
+ "CnMarkdownEditor": 1,
128
+ "CnMassActionBar": 1,
129
+ "CnMassCopyDialog": 0.92,
130
+ "CnMassDeleteDialog": 0.92,
131
+ "CnMassExportDialog": 0.93,
132
+ "CnMassImportDialog": 0.87,
133
+ "CnMenuItemEditor": 1,
134
+ "CnMenuTreeNode": 1,
135
+ "CnMenuWidget": 1,
136
+ "CnMenuWidgetForm": 1,
137
+ "CnNcDashboardWidgetForm": 1,
138
+ "CnNcWidgetGridPicker": 1,
139
+ "CnNcWidgetWidget": 1,
140
+ "CnNewsWidget": 1,
141
+ "CnNewsWidgetForm": 1,
142
+ "CnNoteCard": 0.67,
143
+ "CnNotesCard": 0.8,
144
+ "CnNotificationPreferences": 1,
145
+ "CnObjectCalendar": 1,
146
+ "CnObjectCard": 0.7,
147
+ "CnObjectDataWidget": 0.73,
148
+ "CnObjectDataWidgetForm": 1,
149
+ "CnObjectGeoWidget": 1,
150
+ "CnObjectGeoWidgetForm": 1,
151
+ "CnObjectKanban": 1,
152
+ "CnObjectList": 1,
153
+ "CnObjectListWidget": 1,
154
+ "CnObjectListWidgetForm": 1,
155
+ "CnObjectMetadataModal": 1,
156
+ "CnObjectMetadataWidget": 0.92,
157
+ "CnObjectRow": 1,
158
+ "CnObjectSidebar": 0.75,
159
+ "CnOpenBuildEditButton": 1,
160
+ "CnOpenProjectCreate": 1,
161
+ "CnOpenProjectPicker": 1,
162
+ "CnPageHeader": 0.67,
163
+ "CnPageRenderer": 1,
164
+ "CnPageTreeNode": 1,
165
+ "CnPagination": 1,
166
+ "CnPeopleWidget": 1,
167
+ "CnPeopleWidgetForm": 1,
168
+ "CnPhotoAlbumCreate": 1,
169
+ "CnPhotoAlbumPicker": 1,
170
+ "CnPollCreate": 1,
171
+ "CnPollPicker": 1,
172
+ "CnProgressBar": 0.71,
173
+ "CnQuickFilterBar": 0.9,
174
+ "CnQuicklinksWidget": 1,
175
+ "CnQuicklinksWidgetForm": 1,
176
+ "CnRegisterMapping": 0.68,
177
+ "CnRegisterSchemaSelect": 1,
178
+ "CnRelatedCollections": 1,
179
+ "CnRelatedFiles": 1,
180
+ "CnRelatedObjectsWidget": 1,
181
+ "CnRelatedObjectsWidgetForm": 1,
182
+ "CnRelationshipGraph": 1,
183
+ "CnResourceSelect": 1,
184
+ "CnRichSubmitDialog": 1,
185
+ "CnRoadmapItem": 1,
186
+ "CnRoadmapTab": 1,
187
+ "CnRowActions": 0.8,
188
+ "CnSavedViewsControl": 1,
189
+ "CnSaveViewDialog": 1,
190
+ "CnSchemaFormDialog": 0.79,
191
+ "CnSearchPage": 1,
192
+ "CnSettingsCard": 0.83,
193
+ "CnSettingsPage": 0.64,
194
+ "CnSettingsSection": 0.71,
195
+ "CnSetupWizard": 1,
196
+ "CnShareCreate": 1,
197
+ "CnSignatureCapture": 1,
198
+ "CnSpendAnalyticsWidget": 1,
199
+ "CnSpendAnalyticsWidgetForm": 1,
200
+ "CnStatsBlock": 0.88,
201
+ "CnStatsBlockWidget": 1,
202
+ "CnStatsBlockWidgetForm": 1,
203
+ "CnStatsPanel": 0.33,
204
+ "CnStatusBadge": 0.71,
205
+ "CnStatWidget": 1,
206
+ "CnStatWidgetForm": 1,
207
+ "CnStructuredDocReview": 1,
208
+ "CnSuggestFeatureModal": 1,
209
+ "CnSummaryAggregates": 1,
210
+ "CnSupportDialog": 1,
211
+ "CnTabbedFormDialog": 0.7,
212
+ "CnTableWidget": 1,
213
+ "CnTabs": 1,
214
+ "CnTagsCard": 1,
215
+ "CnTalkRoomCreate": 1,
216
+ "CnTalkRoomPicker": 1,
217
+ "CnTasksCard": 0.91,
218
+ "CnTenantBadge": 1,
219
+ "CnTextTableEditor": 1,
220
+ "CnTextWidget": 1,
221
+ "CnTextWidgetForm": 1,
222
+ "CnThemePreview": 1,
223
+ "CnTileWidget": 1,
224
+ "CnTimelineStages": 0.78,
225
+ "CnTimelineView": 1,
226
+ "CnTimeTrackerCreate": 1,
227
+ "CnTimeTrackerPicker": 1,
228
+ "CnTranslatedBadge": 1,
229
+ "CnTreeView": 1,
230
+ "CnUserActionMenu": 1,
231
+ "CnVersionHistory": 1,
232
+ "CnVersionInfoCard": 0.72,
233
+ "CnVideoWidget": 1,
234
+ "CnVideoWidgetForm": 1,
235
+ "CnWalkthrough": 1,
236
+ "CnWidgetCardGrid": 1,
237
+ "CnWidgetEditCog": 1,
238
+ "CnWidgetFormRenderer": 1,
239
+ "CnWidgetGrid": 1,
240
+ "CnWidgetMapViewer": 1,
241
+ "CnWidgetObjectTable": 1,
242
+ "CnWidgetRefItem": 1,
243
+ "CnWidgetRenderer": 1,
244
+ "CnWidgetWrapper": 0.84,
245
+ "CnWikiPage": 1,
246
+ "CnWizardDialog": 1,
247
+ "CnWorkspaceFilterWidget": 1,
248
+ "CnXwikiPageCreate": 1,
249
+ "CnXwikiPagePicker": 1,
250
+ "__schema__": {
251
+ "generatedBy": "scripts/check-jsdoc.js --update",
252
+ "rule": "CI fails if any component score < baseline. New components require 1.0."
253
+ }
254
+ }
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env node
2
+ // SPDX-License-Identifier: EUPL-1.2
3
+ // Copyright (C) 2026 Conduction B.V.
4
+ //
5
+ // Pre-compiles the v2 manifest JSON Schema into a standalone JS module
6
+ // that doesn't require runtime `new Function()`. Nextcloud's CSP blocks
7
+ // unsafe-eval, so Ajv's default JIT compilation breaks Vue mount in v2
8
+ // apps (EvalError on every app boot). This generator runs at build time
9
+ // as a `prebuild` step to produce
10
+ // src/utils/validateManifestV2.compiled.js — a regular CJS module that
11
+ // imports Ajv's runtime helpers but never calls new Function().
12
+ //
13
+ // See ADR-036 and docs/utilities/validate-manifest-v2.md for rationale.
14
+
15
+ 'use strict'
16
+
17
+ const fs = require('fs')
18
+ const path = require('path')
19
+ // eslint-disable-next-line n/no-unpublished-require
20
+ const Ajv2020 = require('ajv/dist/2020')
21
+ // eslint-disable-next-line n/no-unpublished-require
22
+ const addFormats = require('ajv-formats')
23
+ // eslint-disable-next-line n/no-unpublished-require
24
+ const standaloneCode = require('ajv/dist/standalone')
25
+
26
+ // ajv and ajv-formats export default via CJS interop differently depending on
27
+ // bundler / Node version — unwrap .default when present.
28
+ const AjvClass = Ajv2020.default || Ajv2020
29
+ const addFormatsFunc = addFormats.default || addFormats
30
+ const standaloneCodeFunc = standaloneCode.default || standaloneCode
31
+
32
+ const schemaPath = path.resolve(__dirname, '../src/schemas/app-manifest-v2.schema.json')
33
+ const outPath = path.resolve(__dirname, '../src/utils/validateManifestV2.compiled.js')
34
+
35
+ const schema = JSON.parse(fs.readFileSync(schemaPath, 'utf-8'))
36
+
37
+ const ajv = new AjvClass({
38
+ code: { source: true, esm: false }, // CJS output for broadest bundler compat
39
+ useDefaults: true,
40
+ allErrors: true,
41
+ strict: false,
42
+ })
43
+ addFormatsFunc(ajv)
44
+
45
+ const validate = ajv.compile(schema)
46
+ let moduleCode = standaloneCodeFunc(ajv, validate)
47
+
48
+ // Inline ajv-formats so the standalone module has ZERO runtime dependency on
49
+ // ajv-formats. Ajv's standalone output emits lines like
50
+ // const formatN = require("ajv-formats/dist/formats").fullFormats.uri;
51
+ // That runtime require survives Node/Jest fine, but the rollup→webpack
52
+ // double-bundle every consumer app runs tree-shakes ajv-formats'
53
+ // `exports.fullFormats = {…}` assignment down to `{}`, so at runtime
54
+ // `{}.fullFormats.uri` throws `Cannot read properties of undefined (reading
55
+ // 'uri')` during app boot and white-screens the whole SPA (observed
56
+ // 2026-07-11 on openbuild). Replacing each reference with a self-contained
57
+ // inline definition, extracted from the installed ajv-formats at build time,
58
+ // removes the fragile require while preserving identical validation. The
59
+ // formats file is re-read on every regeneration, so this stays in sync.
60
+ const formatsSrc = fs.readFileSync(
61
+ require.resolve('ajv-formats/dist/formats'),
62
+ 'utf-8',
63
+ )
64
+ const fullFormats = require('ajv-formats/dist/formats').fullFormats
65
+
66
+ /**
67
+ * Read a full JavaScript regex literal (including flags) from source, starting
68
+ * at the opening `/`. Correctly skips escapes and `[…]` character classes so a
69
+ * bare `/` inside a class does not prematurely terminate the literal.
70
+ *
71
+ * @param {string} src The source text.
72
+ * @param {number} start Index of the opening `/`.
73
+ * @return {string|null} The literal `/…/flags`, or null if unterminated.
74
+ */
75
+ function readRegexLiteral(src, start) {
76
+ let i = start + 1
77
+ let inClass = false
78
+ for (; i < src.length; i++) {
79
+ const ch = src[i]
80
+ if (ch === '\\') {
81
+ i++
82
+ continue
83
+ }
84
+ if (ch === '[') {
85
+ inClass = true
86
+ } else if (ch === ']') {
87
+ inClass = false
88
+ } else if (ch === '/' && !inClass) {
89
+ break
90
+ } else if (ch === '\n') {
91
+ return null
92
+ }
93
+ }
94
+ if (i >= src.length) {
95
+ return null
96
+ }
97
+ let end = i + 1
98
+ while (end < src.length && /[a-z]/i.test(src[end])) {
99
+ end++
100
+ }
101
+ return src.slice(start, end)
102
+ }
103
+
104
+ /**
105
+ * Build a self-contained JS expression reproducing one ajv-formats format,
106
+ * with no runtime dependency on the ajv-formats package.
107
+ *
108
+ * @param {string} name The format name (e.g. "uri").
109
+ * @return {string} A JS expression that evaluates to the format value.
110
+ */
111
+ function inlineFormat(name) {
112
+ const value = fullFormats[name]
113
+ if (value === undefined) {
114
+ throw new Error(`build-validators: ajv-formats has no format "${name}" to inline`)
115
+ }
116
+ if (value instanceof RegExp) {
117
+ return value.toString()
118
+ }
119
+ if (typeof value === 'function') {
120
+ // A function format (e.g. uri) closes over module-level `const X = /…/;`
121
+ // regexes in formats.js. Inline exactly the ones it references inside an
122
+ // IIFE so the returned function is self-contained. Regex literals are
123
+ // read with a lexer-correct scanner (escapes + `[…]` classes) — a plain
124
+ // regex can't parse a regex literal that contains bare `/` inside a
125
+ // character class (ajv-formats' URI regex does).
126
+ const body = value.toString()
127
+ const deps = []
128
+ const declRe = /const ([A-Z0-9_]+) = \//g
129
+ let m
130
+ while ((m = declRe.exec(formatsSrc)) !== null) {
131
+ const name = m[1]
132
+ if (!new RegExp(`\\b${name}\\b`).test(body)) {
133
+ continue
134
+ }
135
+ const literal = readRegexLiteral(formatsSrc, m.index + m[0].length - 1)
136
+ if (literal) {
137
+ deps.push(`const ${name} = ${literal};`)
138
+ }
139
+ }
140
+ return `(function(){${deps.join('')}return ${body};})()`
141
+ }
142
+ throw new Error(`build-validators: cannot inline ajv-formats format "${name}" of type ${typeof value}`)
143
+ }
144
+
145
+ moduleCode = moduleCode.replace(
146
+ /require\("ajv-formats\/dist\/formats"\)\.fullFormats\.([A-Za-z0-9_-]+)/g,
147
+ (_match, name) => inlineFormat(name),
148
+ )
149
+
150
+ if (/require\("ajv-formats/.test(moduleCode)) {
151
+ throw new Error('build-validators: a require("ajv-formats…") reference survived inlining — the standalone validator is not self-contained')
152
+ }
153
+
154
+ const banner = `// SPDX-License-Identifier: EUPL-1.2
155
+ // Copyright (C) 2026 Conduction B.V.
156
+ //
157
+ // AUTOGENERATED by scripts/build-validators.js — DO NOT EDIT BY HAND.
158
+ // Run \`npm run build:validators\` to regenerate after editing
159
+ // src/schemas/app-manifest-v2.schema.json.
160
+ //
161
+ // This is a standalone Ajv-compiled validator. It does NOT use
162
+ // new Function() at runtime — required for Nextcloud's CSP which
163
+ // blocks unsafe-eval. The validator function is exported as the
164
+ // module's default export (CJS: module.exports).
165
+ //
166
+ // See ADR-036 and docs/utilities/validate-manifest-v2.md.
167
+
168
+ `
169
+
170
+ fs.writeFileSync(outPath, banner + moduleCode)
171
+
172
+ const kb = (moduleCode.length / 1024).toFixed(1)
173
+ // eslint-disable-next-line no-console
174
+ console.log(`build-validators: wrote ${path.relative(process.cwd(), outPath)} (${kb} KB)`)
@@ -0,0 +1,118 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Published-CSS-entry gate.
5
+ *
6
+ * THE BUG THIS GUARDS AGAINST
7
+ * ---------------------------
8
+ * `css/index.css` — the entry every consumer app imports — used to re-export
9
+ * `../src/css/index.css`, which holds only the UNSCOPED globals. Every
10
+ * component's scoped SFC style block lives exclusively in the built
11
+ * `dist/nextcloud-vue.css`, and the only reference to that file was the
12
+ * `import './nextcloud-vue.css'` banner atop `dist/esm/index.js`. webpack
13
+ * treats that barrel as side-effect-free (it is absent from
14
+ * package.json#sideEffects), so it skips the module — and the CSS import with
15
+ * it — whenever a consumer imports named components instead of the namespace.
16
+ *
17
+ * Net effect: dist-consuming apps rendered components carrying `data-v-*`
18
+ * scope attributes for which no rule was ever shipped. Nothing errored; the
19
+ * elements just silently lost every scoped declaration, so flex containers
20
+ * fell back to `display: block`. Observed on pipelinq, where the dashboard
21
+ * header stacked its action buttons underneath the title and 11 nc-vue
22
+ * components rendered entirely unstyled.
23
+ *
24
+ * WHAT THIS DOES
25
+ * --------------
26
+ * Follows the @import chain out of `css/index.css` exactly as a consumer's
27
+ * css-loader would, then asserts the invariant that was violated: EVERY
28
+ * `data-v-*` scope id baked into the built JS has at least one matching rule
29
+ * in the CSS reachable from that entry. Vue only emits a scope id for a
30
+ * component that actually has a scoped <style> block, so an id with no rules
31
+ * always means its styles were dropped somewhere between rollup and the
32
+ * published entry point.
33
+ *
34
+ * Runs after `npm run build` (dist/ must exist).
35
+ *
36
+ * Exit codes:
37
+ * 0 — every scope id rendered by the built JS is styled by the CSS entry
38
+ * 1 — at least one scope id would ship unstyled
39
+ */
40
+
41
+ const fs = require('fs')
42
+ const path = require('path')
43
+
44
+ const root = path.resolve(__dirname, '..')
45
+ const entry = path.join(root, 'css', 'index.css')
46
+
47
+ const SCOPE_ID = /data-v-[0-9a-f]{8}/g
48
+
49
+ /**
50
+ * Resolve a stylesheet and everything it @imports, css-loader style.
51
+ *
52
+ * @param {string} file Absolute path to a CSS file.
53
+ * @param {Set<string>} seen Guard against @import cycles.
54
+ * @return {string} The concatenated CSS of `file` and its transitive imports.
55
+ */
56
+ function readWithImports(file, seen = new Set()) {
57
+ if (seen.has(file)) {
58
+ return ''
59
+ }
60
+ seen.add(file)
61
+
62
+ if (!fs.existsSync(file)) {
63
+ console.error(`✗ CSS entry points at a file that does not exist: ${path.relative(root, file)}`)
64
+ console.error(' Did `npm run build` run first? dist/ is gitignored and built by rollup.')
65
+ process.exit(1)
66
+ }
67
+
68
+ const css = fs.readFileSync(file, 'utf8')
69
+ const imports = [...css.matchAll(/@import\s+['"]([^'"]+)['"]/g)].map((m) => m[1])
70
+
71
+ return imports.reduce(
72
+ (acc, spec) => acc + readWithImports(path.resolve(path.dirname(file), spec), seen),
73
+ css,
74
+ )
75
+ }
76
+
77
+ /**
78
+ * Collect every scope id the built JS will stamp onto rendered elements.
79
+ *
80
+ * @return {Set<string>} Scope ids such as `data-v-5266112e`.
81
+ */
82
+ function scopeIdsInBuiltJs() {
83
+ const ids = new Set()
84
+ const walk = (dir) => {
85
+ for (const dirent of fs.readdirSync(dir, { withFileTypes: true })) {
86
+ const full = path.join(dir, dirent.name)
87
+ if (dirent.isDirectory()) {
88
+ walk(full)
89
+ } else if (dirent.name.endsWith('.js')) {
90
+ for (const id of fs.readFileSync(full, 'utf8').match(SCOPE_ID) || []) {
91
+ ids.add(id)
92
+ }
93
+ }
94
+ }
95
+ }
96
+ walk(path.join(root, 'dist'))
97
+ return ids
98
+ }
99
+
100
+ const css = readWithImports(entry)
101
+ const styled = new Set(css.match(SCOPE_ID) || [])
102
+ const rendered = scopeIdsInBuiltJs()
103
+ const unstyled = [...rendered].filter((id) => !styled.has(id)).sort()
104
+
105
+ if (unstyled.length > 0) {
106
+ console.error(`✗ ${unstyled.length} scope id(s) are rendered by the built JS but carry no rule in css/index.css:`)
107
+ for (const id of unstyled) {
108
+ console.error(` ${id}`)
109
+ }
110
+ console.error('')
111
+ console.error(' Consumers importing @conduction/nextcloud-vue/css/index.css would render')
112
+ console.error(' these components with every scoped declaration missing (silently — no error).')
113
+ console.error(' css/index.css must @import the BUILT stylesheet (dist/nextcloud-vue.css),')
114
+ console.error(' which carries the scoped SFC styles as well as the unscoped globals.')
115
+ process.exit(1)
116
+ }
117
+
118
+ console.log(`✓ css/index.css styles all ${rendered.size} scope ids rendered by the built JS`)