Skip to content

feat(vue3): migrate the app template from Vue 2 to Vue 3 - #103

Merged
rubenvdlinde merged 8 commits into
developmentfrom
feat/vue-3-migration
Jul 31, 2026
Merged

feat(vue3): migrate the app template from Vue 2 to Vue 3#103
rubenvdlinde merged 8 commits into
developmentfrom
feat/vue-3-migration

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

Migrates the app template from Vue 2 to Vue 3. This repo is what new Conduction apps are generated from, so every day it stayed on Vue 2 the fleet could mint new Vue 2 apps.

Results

Suite Before After
npm run lint 0 errors / 0 warnings — but src only, and 0 of 21 vue/no-deprecated-* rules active 0 errors / 0 warnings over src and tests, 21/21 deprecation rules active
npm run build webpack OK (Vue 2) webpack OK, 0 errors, 3 warnings (1 nc-vue library defect + 2 pre-existing asset-size hints)
Playwright chromium 0 tests (the only spec file is the docs-capture skeleton, which this project ignores; no auth wiring at all) 8 passed, 0 failed
npm run check:specs 5 PASS 5 PASS
l10n parity gate (#99) 36/36 36/36

Verified against a disposable, isolated instance on :8096 (docker inspect … Mounts = volume; the shared nextcloud container reports 39 bind mounts). Deployed bundle SHA checked against the on-disk build on every run.

Dependencies

vue 2.7→3.5.40 · vue-router 3→4.6.4 · @nextcloud/vue 8→9.9.0 · @conduction/nextcloud-vue 1.0.0-beta.2132.1.0-vue3.10 (the vue3 dist-tag) · vue-loader 15→17 · @nextcloud/webpack-vue-config 6→7.

Three traps worth naming:

  • overrides pinned @nextcloud/vue: ^8.39.0. Bumping dependencies alone would have silently dragged the whole tree back onto the Vue 2 line.
  • @nextcloud/dialogs@6.4.2 still declares peer vue@^2.7.16 despite the higher major, and hard-fails the install. 7.4.1 is the framework-agnostic line.
  • Undeclared peers. nc-vue lists gridstack, @vueuse/core, axe-core, dexie, dompurify, marked, @nextcloud/capabilities as peers and no consumer declared any of them — they resolved by accident from a hoisted node_modules. @nextcloud/webpack-vue-config@7 additionally require()s terser-webpack-plugin without declaring it. All now declared.

Rendering fix

@nextcloud/vue@9 and @nextcloud/dialogs@7 ship an exports map and no main, so the existing '@nextcloud/vue$': path.resolve(…/node_modules/@nextcloud/vue) webpack aliases became unresolvable — 233 Can't resolve '@nextcloud/vue' errors. Removed; resolve.modules already provided the single-instance pin those aliases were buying.

Pre-existing bugs fixed on the way through

  1. openspec/schemas was a committed mode-120000 symlink to ../../../../../../hydra/openspec/schemas. It dangles in every checkout on this box, nothing references it, and it made docker cp of the app refuse outright (invalid symlink) — the app could not be deployed to a container at all. Same failure class as openconnector's committed node_modules symlink.
  2. Two phantom manifest widgetKeys. Settings declared version-info both as a v1 config.sections widget (works) and as a v2 widgetKey (no such key in the v2 BUILT_IN_WIDGETS map); ExampleDetail used object-data where the library's key is data. Both rendered .cn-unknown-widget — "Widget unavailable" — with zero console output.
  3. tests/ was never linted (npm run lint was eslint src), and the eslint node-CLI override was an explicit file list, so the checker added by i18n: full European language coverage (36 locales) + parity gate #99 fell outside it. Fixing that surfaced two real findings: a dead reviverPathStack binding, and lifecycleStates() kept alive by a void lifecycleStates suppression while never being called — the inline check it was meant to replace only inspected t.to, and only when states{} existed, so an undeclared or array from state went unreported. Now wired up correctly.
  4. EmailField used the Vue 2 value+@input model contract, which is not what v-model binds in Vue 3.

Tests

The default chromium project had zero specs. Added tests/e2e/app-shell.spec.ts (7 tests) plus a shared one-time login. Every assertion covers a failure mode that is silent under Vue 3 — root props reaching App, no .cn-unknown-widget, nav icons measured (an unregistered icon name renders nothing, not a fallback), dashboard widgets measured for non-zero width (missing gridstack CSS lays items out 0 px wide and toBeVisible() does not catch it), the Vue Router 4 catch-all, and the separate admin-settings entry point.

Two of these were proven red before green:

Shared-instance safety. playwright.config.ts had NEXTCLOUD_URL || 'http://localhost:8080' and the Newman collection defaulted base_url to the same. :8080 is the shared dev container, which bind-mounts real host checkouts. PLAYWRIGHT_BASE_URL is now authoritative with no fallback, and the config throws on a :8080 base URL.

Template hygiene

README, project.md, docs/ and openspec/config.yaml all described the template as "Vue 2.7" — text every generated app inherits. openspec/specs/template-manifest-v1/spec.md carried a normative requirement that main.js call new Vue(...).$mount('#content'), which the code now contradicts; updated to createApp(...).mount('#content'). Archived changes under openspec/changes/ are history and left alone. Added an End-to-end tests section, since the config now refuses to guess a base URL and a new app author would otherwise hit an unexplained throw.

Outstanding PRs handled first

Known residuals

  • nc-vue defect (not fixed here): dist/esm/composables/cnFormFieldRenderer.js uses a CommonJS require('@nextcloud/vue') inside an ESM dist file. @nextcloud/vue@9's exports map has no require condition, so this can never resolve — it emits a build warning in every consumer and, because it is wrapped in try/catch, permanently forces NcTextArea = null with no runtime error.
  • 2 webpack asset-size hints (advisory, pre-existing in kind).
  • PHP suites unchanged: this change touches no PHP and no composer.json.

This is the template new Conduction apps are generated from, so every day
it stays on Vue 2 the fleet can mint new Vue 2 apps.

Dependencies
  - vue 2.7 -> 3.5, vue-router 3 -> 4, @nextcloud/vue 8 -> 9
  - @conduction/nextcloud-vue 1.0.0-beta.213 -> 2.1.0-vue3.10 (vue3 dist-tag)
  - @nextcloud/dialogs 3 -> 7. NOT 6: despite the higher major, 6.4.2 still
    declares `peer vue@^2.7.16` and hard-fails the install. 7.x is the
    framework-agnostic line.
  - declare the nc-vue peers no consumer was declaring: gridstack, @vueuse/core,
    axe-core, dexie, dompurify, marked, @nextcloud/capabilities. They used to
    resolve by accident from a hoisted node_modules outside the repo.
  - vue-loader 15 -> 17, drop vue-template-compiler, add @vue/compiler-sfc,
    @nextcloud/webpack-vue-config 6 -> 7 (the first line declaring Vue 3), and
    terser-webpack-plugin, which webpack-vue-config@7 requires but does not
    declare.
  - the `overrides` block pinned @nextcloud/vue to ^8.39.0 and would have
    silently dragged the whole tree back to the Vue 2 line.

Bootstrap
  - new Vue()/$mount -> createApp().mount() in main.js, settings.js and
    exampleWidget.js; Vue.extend + propsData -> createApp(Comp, props).
  - Vue.mixin is global in Vue 2 and per-app in Vue 3; the t/n helpers now
    register on each app instance. The old exampleWidget.js registered the
    mixin globally from inside the per-widget callback, leaking one copy per
    mount.
  - PiniaVuePlugin dropped (Vue 3 uses app.use(pinia)).
  - new VueRouter({mode:'history'}) -> createRouter({history:createWebHistory}).
  - the `path: '*'` catch-all is not a route in Vue Router 4; it silently never
    matches. Now `/:pathMatch(.*)*`.
  - Vue.observable -> reactive for the objectSidebarState provide/inject channel.

Rendering
  - the `@nextcloud/vue$` and `@nextcloud/dialogs` webpack aliases pointed at a
    bare package directory. v9/v7 ship an `exports` map and NO `main`, so those
    aliases cannot resolve and produced 233 "Can't resolve '@nextcloud/vue'"
    errors. resolve.modules already provided the single-instance pin.

Pre-existing bugs fixed on the way through
  - main.js never called registerBuiltinDashboardWidgets(), so webpack
    tree-shook the entire built-in widget catalogue and the Settings page
    rendered "Widget unavailable: version-info". Present on development too.
  - openspec/schemas was a committed mode-120000 symlink to
    ../../../../../../hydra/openspec/schemas. It dangles in every checkout on
    this box, nothing references it, and it made `docker cp` of the app refuse
    outright ("invalid symlink"), so the app could not be deployed to a
    container at all.
  - EmailField used the Vue 2 `value` + `@input` model contract, which is not
    what `v-model` binds in Vue 3.

Lint
  - spread `conductionVue3Fixes` from @conduction/nextcloud-vue/eslint last
    over the `@nextcloud` preset. That preset resolves eslint-plugin-vue's
    Vue 2 rules: 0 of the 21 `vue/no-deprecated-*` rules were active, so Vue 2
    idioms would survive a migration silently. Now 21/21 active.
Before this change the default `chromium` Playwright project had ZERO
specs: the only spec file was the journeydoc docs-capture skeleton, which
the project explicitly ignores, and whose two tests only take screenshots.
There was also no auth wiring at all, so every `page.goto` landed on the
login form.

Config
  - PLAYWRIGHT_BASE_URL is now authoritative with NO fallback, and the
    config throws on a :8080 base URL outright. The old
    `NEXTCLOUD_URL || 'http://localhost:8080'` default silently pointed
    local runs at the SHARED dev container.
  - tests/integration/*.postman_collection.json defaulted `base_url` to
    the same shared container; it now has no default.
  - one-time login in tests/e2e/auth.setup.ts, shared via storageState, so
    no spec carries credentials or re-logs-in per test.

Suite (tests/e2e/app-shell.spec.ts) — every assertion covers a failure mode
that is SILENT under Vue 3:
  - CnAppRoot actually mounts, and App receives its `manifest` prop. Vue 3
    moved root props from `render: h => h(App, { props })` to the second
    argument of createApp; getting it wrong passes a prop named "props".
  - each manifest page renders its own content.
  - no widget resolves to `.cn-unknown-widget`.
  - nav icons are measured, not just present: an unregistered icon name
    renders NOTHING rather than a fallback glyph.
  - dashboard widgets are measured for non-zero width — missing the
    gridstack stylesheet lays every item out 0px wide with no error, which
    toBeVisible() does not catch.
  - the catch-all route redirects: Vue Router 4 removed bare `path: '*'`,
    and an unmigrated catch-all never matches, leaving <main> empty.
  - the admin panel mounts from its separate webpack entry point.

Manifest: two phantom v2 widgetKeys
  The suite immediately failed on `.cn-unknown-widget`, which turned out
  NOT to be a bootstrap problem but two keys that no registry provides:
    - Settings declared `version-info` BOTH as a v1 `config.sections`
      widget (which works) and as a v2 `widgetKey` (which cannot — the
      v2 BUILT_IN_WIDGETS map has no such key). Removed the duplicate.
    - ExampleDetail used `object-data`; the library's key is `data`.
  Both were pre-existing and inherited by every generated app.

Uses the shared @conduction/nextcloud-vue/testing/playwright helpers —
seedFirstVisitOverlaysSeen rather than per-test dismissal, which races
the mount.
Every app generated from this repo inherits this text. README, project.md,
docs/ (intro, landing page, docusaurus config) and openspec/config.yaml all
still said 'Vue 2.7'.

openspec/specs/template-manifest-v1/spec.md carried a NORMATIVE requirement
that main.js call 'new Vue(...).$mount(#content)'. That is now the opposite of
what the template does, so it is updated to createApp(...).mount('#content')
rather than left to contradict the code. Archived changes under
openspec/changes/ are history and are left alone.

Adds an End-to-end tests section: PLAYWRIGHT_BASE_URL is now mandatory and
:8080 is rejected, so a new app author would otherwise hit a config throw with
no documentation explaining why.
`npm run lint` only ran `eslint src`, so nothing under tests/ was ever
checked. The eslint config's node-CLI override was also an explicit FILE
LIST, so tests/l10n/check-l10n-parity.js (added by #99) fell outside it and
would have failed the moment anyone widened the glob.

  - lint script -> `eslint src tests --max-warnings 0`
  - node-CLI override: explicit file list -> `tests/**/*.js` glob
  - `n/no-unpublished-import` off for tests/ (devDependencies by definition)
  - filled in the JSDoc the autofix stubbed out rather than leaving bare
    `@param name` lines

Two real findings, not style:
  - tests/validate-json-strict.js declared `reviverPathStack` for a
    reviver-based approach that was never built. Dead binding, removed.
  - tests/validate-register.js kept `lifecycleStates()` alive with a
    `void lifecycleStates` suppression and never called it. The inline
    check it was meant to replace only inspected `t.to`, and only when
    `states{}` was present — an undeclared `from` state, or an array
    `from`, was silently unreported. Now wired up properly: pass it just
    the transitions block so it yields the REFERENCED states (passing the
    whole lifecycle would fold the declared states in and make the
    comparison vacuous). validate-register still reports 0 warnings on the
    template's own register.

Result: 0 errors and 0 warnings across src/ and tests/.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/nextcloud-app-template @ 0f91809

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 100/100
npm ❌ 1/737 denied
PHPUnit ⏭️
Newman ⏭️
Playwright ⏭️

❌ Denied npm licenses

Package Version License
vue3-apexcharts 1.10.0 Custom: https://apexcharts.com/media/vue-apexcharts.png

Quality workflow — 2026-07-31 13:57 UTC

Download the full PDF report from the workflow artifacts.

… nc-vue exactly

Two separate lockfile faults, both of which passed local checks and only
failed in CI.

1. npm version skew. `engines` pins npm ^10 / node ^20, and CI honours it.
   The lockfile here was generated with npm 11 / node 22, which records
   unmet peers differently: npm 10 then refused it with
   `npm ci can only install packages when your package.json and
   package-lock.json are in sync — Missing: pinia@4.0.2 from lock file`.
   The demand comes from vue-router@5 nested inside @nextcloud/vue@9, which
   peer-requires `pinia ^3.0.4 || ^4.0.2` while @conduction/nextcloud-vue
   peer-requires `pinia ^2.0.0`. npm 11 elided that conflict; npm 10 wants
   it materialised. Regenerated with `npx npm@10`, and verified with a
   `npm@10 ci --dry-run` rather than the npm 11 one that reported "up to
   date" against an already-populated node_modules.

   Note this is the same failure class already sitting on `development`
   (`Missing: text-table@0.2.0`), so `npm ci` is currently broken there too.

2. `^2.1.0-vue3.10` is not a pin. A caret range over a prerelease DOES
   float: `2.1.0-vue3.11` was published mid-session and a later
   `npm install` silently picked it up, moving the `vue3` dist-tag out from
   under the build. Pinned exactly to `2.1.0-vue3.10` so the template
   resolves reproducibly instead of tracking whatever was published last.

Re-verified against the regenerated tree: lint 0/0, stylelint clean,
check:specs 5 PASS, l10n parity 36/36, webpack 0 errors, e2e 8 passed.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/nextcloud-app-template @ ffe90f6

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 100/100
npm ❌ 1/738 denied
PHPUnit ⏭️
Newman ⏭️
Playwright ⏭️

❌ Denied npm licenses

Package Version License
vue3-apexcharts 1.10.0 Custom: https://apexcharts.com/media/vue-apexcharts.png

Quality workflow — 2026-07-31 14:07 UTC

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/nextcloud-app-template @ 06c3004

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 100/100
npm ❌ 1/738 denied
PHPUnit ⏭️
Newman ⏭️
Playwright ⏭️

❌ Denied npm licenses

Package Version License
vue3-apexcharts 1.10.0 Custom: https://apexcharts.com/media/vue-apexcharts.png

Quality workflow — 2026-07-31 14:15 UTC

Download the full PDF report from the workflow artifacts.

`quality / License (npm)` was failing, and it was MY regression, not part of
the pre-existing set inherited from development. Reproduced locally against
the reusable workflow's own logic (ConductionNL/.github quality.yml, job
"License (npm)"):

    DENIED: vue3-apexcharts (1.10.0) uses
            'Custom: https://apexcharts.com/media/vue-apexcharts.png'

This is a TRUE POSITIVE, not an allowlist gap. vue3-apexcharts relicensed at
exactly 1.9.0:

    1.8.0   MIT
    1.9.0   "see LICENSE in LICENSE"   <-- relicensed here
    1.10.0  "see LICENSE in LICENSE"

Its LICENSE file is a proprietary dual-license: free only for organisations
under $2M USD annual revenue, a paid Commercial License at or above that, and
a paid OEM/Redistribution License for "no-code dashboards, developer
platforms, embedded BI tools, white-labeled apps or SDKs". Those are
revenue-capped field-of-use restrictions, which an EUPL-1.2 app cannot carry
downstream — and redistributing it inside a component library is squarely the
OEM case. It is not compatible with this repo's licence, so suppressing it
with a .license-overrides.json entry would have been wrong.

It arrived transitively: @conduction/nextcloud-vue@2.1.0-vue3.10 declares
`"vue3-apexcharts": "~1.10.0"` as a hard dependency, so the range sits
ENTIRELY inside the proprietary window and a fresh consumer gets proprietary
code by default. The Vue 2 line pulled `vue-apexcharts` instead, which is why
this appeared only on the Vue 3 bump.

Pinned via npm `overrides` to `<1.9.0`, matching the mitigation already in
openconnector's package.json. This is a real fix rather than a downgrade
hack: 1.8.0 is MIT and its peers are `vue >=3.0.0` + `apexcharts >=4.0.0`,
both satisfied here (vue 3.5.40, apexcharts 4.7.0 — the core `apexcharts`
package is still MIT at 4.7.0; only the Vue wrapper relicensed).

Verified: license check 0 violations (was 1), vue3-apexcharts resolves to
1.8.0 / MIT, npm@10 ci clean, lint 0/0, stylelint clean, check:specs 5 PASS,
l10n parity 36/36, webpack 0 errors, e2e 8 passed.

NOTE FOR nc-vue: the per-consumer override is a workaround every app must
remember independently. nc-vue should pin or drop the dependency itself.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/nextcloud-app-template @ f29a475

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
composer ✅ 100/100
npm ✅ 738/738
PHPUnit ⏭️
Newman ⏭️
Playwright ⏭️

Quality workflow — 2026-07-31 14:40 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde

Copy link
Copy Markdown
Contributor Author

quality / License (npm) — fixed (it was mine, not inherited)

I previously reported the residual set as "5 failures, identical to development". That was wrong twice over: I characterised the set while License (npm) was still pending, and I omitted Quality Report entirely. Corrected below.

Root cause — a true positive, not an allowlist gap

Reproduced locally against the reusable workflow's own check_license logic (ConductionNL/.githubquality.yml, job License (npm)):

DENIED: vue3-apexcharts (1.10.0) uses 'Custom: https://apexcharts.com/media/vue-apexcharts.png'
TOTAL VIOLATIONS: 1

vue3-apexcharts relicensed at exactly 1.9.0:

version license field
1.8.0 MIT
1.9.0 see LICENSE in LICENSE
1.10.0 see LICENSE in LICENSE

Its LICENSE is a proprietary dual-license: free only under $2M USD annual revenue, paid Commercial above that, and a paid OEM/Redistribution licence for "no-code dashboards, developer platforms, embedded BI tools, white-labeled apps or SDKs". Those are revenue-capped field-of-use restrictions that an EUPL-1.2 app cannot pass downstream, and shipping it inside a component library is squarely the OEM case. An allowlist/override entry would have suppressed a real finding.

It arrived transitively: @conduction/nextcloud-vue@2.1.0-vue3.10 declares "vue3-apexcharts": "~1.10.0" as a hard dependency — the range sits entirely inside the proprietary window, so a fresh consumer gets proprietary code by default. The Vue 2 line pulled vue-apexcharts instead, which is why this surfaced only on the Vue 3 bump.

Fix

npm overrides pin to <1.9.0, matching the mitigation already present in openconnector. A real fix, not a downgrade hack: 1.8.0 is MIT, its peers are vue >=3.0.0 + apexcharts >=4.0.0, both satisfied (vue 3.5.40, apexcharts 4.7.0). The core apexcharts package is still MIT at 4.7.0 — only the Vue wrapper relicensed.

Verified after the pin: vue3-apexcharts@1.8.0 license: MIT, license check 0 violations, npm@10 ci clean, lint 0/0, stylelint clean, check:specs 5 PASS, l10n parity 36/36, webpack 0 errors, e2e 8 passed. quality / License (npm) now passes in CI.

Residual set — now verified identical

Same workflow (Code Quality), both sides, completed runs:

development (cf4c54bc)          PR #103 (ca1f51fd)
  Features Check                  Features Check
  PHP Quality (phpcs)             PHP Quality (phpcs)
  PHP Quality (phpmd)             PHP Quality (phpmd)
  PHP Quality (psalm)             PHP Quality (psalm)
  Quality Report                  Quality Report
  Security (composer)             Security (composer)

diff -> IDENTICAL (6 vs 6)

Quality Report is a pure aggregator (needs: [all jobs] + a final "Gate — fail when any upstream job failed" step), so it fails iff one of the other five does. This PR touches zero PHP or composer files.

Two items for fleet policy (not changed here)

  1. nc-vue should pin or drop vue3-apexcharts itself. ~1.10.0 means every consumer on the Vue 3 line inherits a proprietary dependency and must independently remember the same override.
  2. The shared check_license does not handle SPDX AND. It splits on OR only, so MIT AND Zlib fails even though both halves are on the allowlist — which is why this repo carries manual .license-overrides.json entries for pako and sha.js. Splitting on AND and requiring all parts to be allowed would be strictly correct and would remove those overrides fleet-wide.

@rubenvdlinde
rubenvdlinde merged commit b988ee1 into development Jul 31, 2026
19 of 25 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant