chore: split frontend building into legacy Vue 2 and Vue 3

- Built the frontend in separate packages until we migrated everything
  to Vue 3.
- Separate logic into two packages controlled by main package.json

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
pull/55432/head
Ferdinand Thiessen 2025-10-19 18:36:22 +07:00
parent ab551c4c8a
commit f3383f9f90
No known key found for this signature in database
GPG Key ID: 45FAE7268762B400
65 changed files with 38368 additions and 21249 deletions

@ -38,7 +38,10 @@ updates:
# Main master npm frontend dependencies
- package-ecosystem: npm
directory: "/"
directories:
- "/"
- "/build/frontend"
- "/build/frontend-legacy"
schedule:
interval: weekly
day: saturday

9
.gitignore vendored

@ -11,7 +11,7 @@
/apps/inc.php
/assets
/.htaccess
/node_modules
node_modules/
/translationfiles
/translationtool.phar
@ -55,10 +55,6 @@
/apps/files_external/3rdparty/irodsphp/prods/test*
/apps/files_external/tests/config.*.php
# apps modules
/apps/*/node_modules
# ignore themes except the example and the README
/themes/*
!/themes/example
@ -131,9 +127,6 @@ nbproject
# Tests
/tests/phpunit.xml
# Node Modules
/build/node_modules/
# nodejs
/build/bin
/build/lib/

@ -0,0 +1,50 @@
#!/bin/bash
# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: CC0-1.0
# This is a simple helper to execute npm COMMANDs in two directories
# we need this as we cannot use npm workspaces as they break with 2 versions of vue.
COMMAND=""
FRONTEND="$(dirname $0)/frontend"
FRONTEND_LEGACY="$(dirname $0)/frontend-legacy"
build_command() {
if [ "install" = "$1" ] || [ "ci" = "$1" ]; then
COMMAND=$@
elif [ "run" = "$1" ]; then
COMMAND="run --if-present ${@:2}"
else
COMMAND="run --if-present $@"
fi
}
run_parallel() {
npx concurrently \
"cd \"$FRONTEND\" && npm $COMMAND" \
"cd \"$FRONTEND_LEGACY\" && npm $COMMAND"
}
run_sequentially() {
echo -e "\e[1;34m>> Running 'npm $COMMAND' for Vue 3 based frontend\e[0m"
echo
pushd "$FRONTEND"
npm $COMMAND
popd
echo -e "\e[1;34m>> Running 'npm $COMMAND' for Vue 2 based frontend\e[0m"
echo
pushd "$FRONTEND_LEGACY"
npm $COMMAND
popd
}
if [ "--parallel" = "$1" ]; then
build_command ${@:2}
run_parallel
else
build_command $@
run_sequentially
fi

@ -45,7 +45,6 @@ $expectedFiles = [
'autotest-checkers.sh',
'autotest-external.sh',
'autotest.sh',
'babel.config.js',
'build',
'codecov.yml',
'composer.json',
@ -86,10 +85,8 @@ $expectedFiles = [
'tsconfig.json',
'vendor-bin',
'version.php',
'vitest.config.mts',
'webpack.common.cjs',
'webpack.config.js',
'webpack.modules.cjs',
'vite.config.ts',
'vitest.config.ts',
'window.d.ts',
];
$actualFiles = [];

@ -0,0 +1,10 @@
version = 1
SPDX-PackageName = "nextcloud"
SPDX-PackageSupplier = "Nextcloud <info@nextcloud.com>"
SPDX-PackageDownloadLocation = "https://github.com/nextcloud/server"
[[annotations]]
path = ["package.json", "package-lock.json", "tsconfig.json"]
precedence = "aggregate"
SPDX-FileCopyrightText = "2025 Nextcloud GmbH and Nextcloud contributors"
SPDX-License-Identifier = "AGPL-3.0-or-later"

@ -0,0 +1,18 @@
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
export function getCurrentUser() {
return {
uid: 'test',
displayName: 'Test',
isAdmin: false,
}
}
export function getRequestToken() {
return 'test-token-1234'
}
export function onRequestTokenUpdate() {}

@ -0,0 +1,18 @@
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
export default {
interceptors: {
response: {
use: () => {},
},
request: {
use: () => {},
},
},
get: async () => ({ status: 200, data: {} }),
delete: async () => ({ status: 200, data: {} }),
post: async () => ({ status: 200, data: {} }),
head: async () => ({ status: 200, data: {} }),
}

@ -0,0 +1,23 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import type { Capabilities } from '../../apps/files/src/types.ts'
export function getCapabilities(): Capabilities {
return {
files: {
bigfilechunking: true,
blacklisted_files: [],
forbidden_filename_basenames: [],
forbidden_filename_characters: [],
forbidden_filename_extensions: [],
forbidden_filenames: [],
undelete: true,
version_deletion: true,
version_labeling: true,
versioning: true,
},
}
}

@ -0,0 +1,13 @@
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { vi } from 'vitest'
export const showMessage = vi.fn()
export const showSuccess = vi.fn()
export const showWarning = vi.fn()
export const showInfo = vi.fn()
export const showError = vi.fn()
export const showUndo = vi.fn()

@ -0,0 +1,8 @@
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
export function loadState(app: string, key: string, fallback?: any) {
return fallback
}

@ -0,0 +1,5 @@
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
export default {}

@ -0,0 +1,5 @@
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
export default '<svg>SvgMock</svg>'

@ -0,0 +1,11 @@
/**
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
export function createClient() {}
export function getPatcher() {
return {
patch: () => {},
}
}

@ -0,0 +1,121 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import mime from 'mime'
import { basename } from 'node:path'
class FileSystemEntry {
private _isFile: boolean
private _fullPath: string
constructor(isFile: boolean, fullPath: string) {
this._isFile = isFile
this._fullPath = fullPath
}
get isFile() {
return !!this._isFile
}
get isDirectory() {
return !this.isFile
}
get name() {
return basename(this._fullPath)
}
}
export class FileSystemFileEntry extends FileSystemEntry {
private _contents: string
private _lastModified: number
constructor(fullPath: string, contents: string, lastModified = Date.now()) {
super(true, fullPath)
this._contents = contents
this._lastModified = lastModified
}
file(success: (file: File) => void) {
const lastModified = this._lastModified
// Faking the mime by using the file extension
const type = mime.getType(this.name) || ''
success(new File([this._contents], this.name, { lastModified, type }))
}
}
export class FileSystemDirectoryEntry extends FileSystemEntry {
private _entries: FileSystemEntry[]
constructor(fullPath: string, entries: FileSystemEntry[]) {
super(false, fullPath)
this._entries = entries || []
}
createReader() {
let read = false
return {
readEntries: (success: (entries: FileSystemEntry[]) => void) => {
if (read) {
return success([])
}
read = true
success(this._entries)
},
}
}
}
/**
* This mocks the File API's File class
* It will allow us to test the Filesystem API as well as the
* File API in the same test suite.
*/
export class DataTransferItem {
private _type: string
private _entry: FileSystemEntry
getAsEntry?: () => FileSystemEntry
constructor(type = '', entry: FileSystemEntry, isFileSystemAPIAvailable = true) {
this._type = type
this._entry = entry
// Only when the Files API is available we are
// able to get the entry
if (isFileSystemAPIAvailable) {
this.getAsEntry = () => this._entry
}
}
get kind() {
return 'file'
}
get type() {
return this._type
}
getAsFile(): File | null {
if (this._entry.isFile && this._entry instanceof FileSystemFileEntry) {
let file: File | null = null
this._entry.file((f) => {
file = f
})
return file
}
// The browser will return an empty File object if the entry is a directory
return new File([], this._entry.name, { type: '' })
}
}
export function fileSystemEntryToDataTransferItem(entry: FileSystemEntry, isFileSystemAPIAvailable = true): DataTransferItem {
return new DataTransferItem(
entry.isFile ? 'text/plain' : 'httpd/unix-directory',
entry,
isFileSystemAPIAvailable,
)
}

@ -0,0 +1,15 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
window.OC = {
...window.OC,
config: {
version: '32.0.0',
...(window.OC?.config ?? {}),
},
}
window.OCA = { ...window.OCA }
window.OCP = { ...window.OCP }
window._oc_webroot = ''

@ -0,0 +1,7 @@
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: CC0-1.0
*/
export function setup() {
process.env.TZ = 'UTC'
}

@ -0,0 +1,6 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: CC0-1.0
*/
import '@testing-library/jest-dom/vitest'
import 'core-js/stable/index.js'

@ -0,0 +1 @@
../../../apps/comments

@ -0,0 +1 @@
../../../apps/dashboard

@ -0,0 +1 @@
../../../apps/dav

@ -0,0 +1 @@
../../../apps/federatedfilesharing

@ -0,0 +1 @@
../../../apps/files

@ -0,0 +1 @@
../../../apps/files_external

@ -0,0 +1 @@
../../../apps/files_reminders

@ -0,0 +1 @@
../../../apps/files_sharing

@ -0,0 +1 @@
../../../apps/files_trashbin

@ -0,0 +1 @@
../../../apps/files_versions

@ -0,0 +1 @@
../../../apps/oauth2

@ -0,0 +1 @@
../../../apps/profile

@ -0,0 +1 @@
../../../apps/settings

@ -0,0 +1 @@
../../../apps/sharebymail

@ -0,0 +1 @@
../../../apps/systemtags

@ -0,0 +1 @@
../../../apps/theming

@ -0,0 +1 @@
../../../apps/twofactor_backupcodes

@ -0,0 +1 @@
../../../apps/updatenotification

@ -0,0 +1 @@
../../../apps/user_ldap

@ -0,0 +1 @@
../../../apps/user_status

@ -0,0 +1 @@
../../../apps/weather_status

@ -0,0 +1 @@
../../../apps/workflowengine

@ -0,0 +1,77 @@
/**
* SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { recommendedVue2 } from '@nextcloud/eslint-config'
import CypressEslint from 'eslint-plugin-cypress'
import { defineConfig } from 'eslint/config'
import * as globals from 'globals'
export default defineConfig([
{
linterOptions: {
reportUnusedDisableDirectives: 'error',
reportUnusedInlineConfigs: 'error',
},
},
...recommendedVue2,
{
name: 'server/custom-webpack-globals',
files: ['**/*.js', '**/*.ts', '**/*.vue'],
languageOptions: {
globals: {
PRODUCTION: 'readonly',
},
},
},
{
name: 'server/scripts-are-cjs',
files: [
'*.js',
'build/**/*.js',
'**/core/src/icons.cjs',
],
languageOptions: {
globals: {
...globals.es2023,
...globals.node,
},
},
rules: {
'no-console': 'off',
'jsdoc/require-jsdoc': 'off',
},
},
// Cypress setup
CypressEslint.configs.recommended,
{
name: 'server/cypress',
files: ['cypress/**', '**/*.cy.*'],
rules: {
'no-console': 'off',
'jsdoc/require-jsdoc': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-expressions': 'off',
},
},
// customer server ignore files
{
name: 'server/ignored-files',
ignores: [
'.devcontainer/',
'composer.json',
'**/*.php',
'3rdparty/',
'tests/', // PHP tests
'**/js/',
'**/l10n/', // all translations (config only ignored in root)
'**/vendor/', // different vendors
],
},
])

File diff suppressed because it is too large Load Diff

@ -0,0 +1,151 @@
{
"name": "nextcloud-ui-legacy",
"version": "1.0.0",
"private": true,
"description": "Nextcloud Server",
"license": "AGPL-3.0-or-later",
"author": "Nextcloud GmbH and Nextcloud contributors",
"scripts": {
"build": "webpack --node-env production --progress",
"dev": "webpack --node-env development --progress",
"lint": "eslint --suppressions-location ../eslint-baseline.json --no-error-on-unmatched-pattern ./apps/*/ ./core/",
"lint:fix": "eslint --suppressions-location ../eslint-baseline.json --fix --no-error-on-unmatched-pattern ./apps/*/ ./core/",
"test": "vitest run",
"test:coverage": "vitest run --coverage --reporter=default",
"test:update-snapshots": "vitest run --update",
"test:watch": "vitest watch",
"watch": "webpack --node-env development --progress --watch"
},
"browserslist": [
"extends @nextcloud/browserslist-config"
],
"overrides": {
"@vitejs/plugin-vue2": {
"vite": "^7"
}
},
"dependencies": {
"@chenfengyuan/vue-qrcode": "^1.0.2",
"@mdi/js": "^7.4.47",
"@mdi/svg": "^7.4.47",
"@nextcloud/auth": "^2.5.3",
"@nextcloud/axios": "^2.5.2",
"@nextcloud/browser-storage": "^0.5.0",
"@nextcloud/calendar-availability-vue": "^2.2.10",
"@nextcloud/capabilities": "^1.2.0",
"@nextcloud/dialogs": "^7.1.0",
"@nextcloud/event-bus": "^3.3.2",
"@nextcloud/files": "^3.12.0",
"@nextcloud/initial-state": "^3.0.0",
"@nextcloud/l10n": "^3.4.0",
"@nextcloud/logger": "^3.0.2",
"@nextcloud/moment": "^1.3.5",
"@nextcloud/password-confirmation": "^6.0.1",
"@nextcloud/paths": "^2.2.1",
"@nextcloud/router": "^3.0.1",
"@nextcloud/sharing": "^0.3.0",
"@nextcloud/upload": "^1.11.0",
"@nextcloud/vue": "^8.31.0",
"@simplewebauthn/browser": "^13.2.2",
"@vue/web-component-wrapper": "^1.3.0",
"@vueuse/components": "^11.3.0",
"@vueuse/core": "^11.3.0",
"@vueuse/integrations": "^11.3.0",
"backbone": "^1.6.1",
"blurhash": "^2.0.5",
"browserslist-useragent-regexp": "^4.1.3",
"camelcase": "^8.0.0",
"cancelable-promise": "^4.3.1",
"clipboard": "^2.0.11",
"color": "^5.0.2",
"core-js": "^3.45.0",
"crypto-browserify": "^3.12.1",
"davclient.js": "nextcloud-deps/davclient.js#59d7777d7fe290c5f1fd74a58e7eb529b63e153d",
"debounce": "^2.2.0",
"dompurify": "^3.2.7",
"escape-html": "^1.0.3",
"focus-trap": "^7.6.5",
"handlebars": "^4.7.8",
"is-svg": "^6.1.0",
"jquery": "~3.7",
"jquery-ui": "1.14.1",
"jquery-ui-dist": "^1.13.3",
"libphonenumber-js": "^1.12.23",
"lodash": "^4.17.21",
"marked": "^16.3.0",
"moment": "^2.30.1",
"moment-timezone": "^0.6.0",
"p-limit": "^7.1.1",
"p-queue": "^8.1.0",
"path": "^0.12.7",
"pinia": "^2.3.1",
"query-string": "^9.3.1",
"regenerator-runtime": "^0.14.1",
"select2": "3.5.1",
"snap.js": "^2.0.9",
"strengthify": "github:nextcloud/strengthify#0.5.9",
"throttle-debounce": "^5.0.2",
"underscore": "1.13.7",
"url-search-params-polyfill": "^8.2.5",
"v-click-outside": "^3.2.0",
"v-tooltip": "^2.1.3",
"vue": "^2.7.16",
"vue-click-outside": "^1.1.0",
"vue-cropperjs": "^4.2.0",
"vue-frag": "^1.4.3",
"vue-infinite-loading": "^2.4.5",
"vue-localstorage": "^0.6.2",
"vue-material-design-icons": "^5.3.1",
"vue-router": "^3.6.5",
"vuedraggable": "^2.24.3",
"vuex": "^3.6.2",
"vuex-router-sync": "^5.0.0",
"webdav": "^5.8.0"
},
"devDependencies": {
"@babel/node": "^7.28.0",
"@babel/plugin-transform-private-methods": "^7.27.1",
"@babel/preset-typescript": "^7.27.1",
"@codecov/webpack-plugin": "^1.9.1",
"@nextcloud/babel-config": "^1.2.0",
"@nextcloud/typings": "^1.9.1",
"@nextcloud/webpack-vue-config": "^6.3.0",
"@pinia/testing": "^0.1.7",
"@testing-library/jest-dom": "^6.6.4",
"@testing-library/user-event": "^14.6.1",
"@testing-library/vue": "^5.8.3",
"@types/dockerode": "^3.3.44",
"@types/wait-on": "^5.3.4",
"@vitejs/plugin-vue2": "^2.3.3",
"@vitest/coverage-v8": "^3.2.4",
"@vue/test-utils": "^1.3.5",
"@vue/tsconfig": "~0.5.1",
"@zip.js/zip.js": "^2.8.2",
"babel-loader-exclude-node-modules-except": "^1.2.1",
"babel-plugin-module-resolver": "^5.0.2",
"colord": "^2.9.3",
"exports-loader": "^5.0.0",
"file-loader": "^6.2.0",
"handlebars-loader": "^1.7.3",
"mime": "^4.1.0",
"msw": "^2.11.3",
"raw-loader": "^4.0.2",
"regextras": "^0.8.0",
"sass": "^1.93.2",
"sinon": "<= 5.0.7",
"typescript": "^5.9.2",
"vite": "^7.1.8",
"vitest": "^3.2.4",
"vue-loader": "^15.11.1",
"vue-template-compiler": "^2.7.16",
"wait-on": "^8.0.4",
"webpack": "^5.102.0",
"webpack-cli": "^6.0.1",
"webpack-merge": "^6.0.1",
"workbox-webpack-plugin": "^7.3.0"
},
"engines": {
"node": "^22.0.0",
"npm": "^10.5.0"
}
}

@ -0,0 +1,35 @@
{
"extends": "@vue/tsconfig/tsconfig.json",
"include": ["./apps/**/*.ts", "./apps/**/*.vue", "./core/**/*.ts", "./core/**/*.vue", "./*.d.ts"],
"exclude": ["./**/*.cy.ts"],
"compilerOptions": {
"lib": ["DOM", "ESNext"],
"types": ["node", "vue", "vue-router"],
"outDir": "./dist/",
"target": "ESNext",
"module": "ESNext",
// Set module resolution to bundler and `noEmit` to be able to set `allowImportingTsExtensions`, so we can import Typescript with .ts extension
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"noEmit": true,
// Allow ts to import js files
"allowJs": true,
"allowSyntheticDefaultImports": true,
"declaration": false,
"noImplicitAny": false,
"resolveJsonModule": true,
"strict": true,
},
"vueCompilerOptions": {
"target": 2.7
},
"ts-node": {
// these options are overrides used only by ts-node
// same as our --compilerOptions flag and our TS_NODE_COMPILER_OPTIONS environment variable
"compilerOptions": {
"moduleResolution": "node",
"module": "commonjs",
"verbatimModuleSyntax": false
}
}
}

@ -0,0 +1,66 @@
/**
* SPDX-FileCopyrightText: 2023-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: CC0-1.0
*/
import vue from '@vitejs/plugin-vue2'
import { exec } from 'node:child_process'
import { resolve } from 'node:path'
import { promisify } from 'node:util'
import { defaultExclude, defineConfig } from 'vitest/config'
const gitIgnore: string[] = []
// get all files ignored in the apps directory (e.g. if putting `view` app there).
try {
const execAsync = promisify(exec)
const { stdout } = await execAsync('git check-ignore apps/*', { cwd: __dirname })
gitIgnore.push(...stdout.split('\n').filter(Boolean))
// eslint-disable-next-line no-console
console.log('Git ignored files excluded from tests: ', gitIgnore)
} catch (error) {
// we can ignore error code 1 as this just means there are no ignored files
if (error && (typeof error !== 'object' || !('code' in error && error.code === 1))) {
// but otherwise something bad is happening and we should re-throw
throw error
}
}
export default defineConfig({
plugins: [vue()],
root: import.meta.dirname,
resolve: {
preserveSymlinks: true,
alias: {
vue$: resolve(__dirname, './node_modules/vue/dist/vue.js'),
},
},
test: {
include: ['./{apps,core}/**/*.{test,spec}.?(c|m)[jt]s?(x)'],
environment: 'jsdom',
environmentOptions: {
jsdom: {
url: 'http://nextcloud.local',
},
},
coverage: {
include: ['./apps/*/src/**', 'core/src/**'],
exclude: ['**.spec.*', '**.test.*', '**.cy.*', 'core/src/tests/**'],
provider: 'v8',
reporter: ['lcov', 'text'],
},
setupFiles: [
'./__tests__/mock-window.js',
'./__tests__/setup-testing-library.js',
],
exclude: [
...defaultExclude,
...gitIgnore,
],
globalSetup: './__tests__/setup-global.js',
server: {
deps: {
inline: true,
},
},
},
})

@ -11,10 +11,10 @@ const path = require('path')
const { VueLoaderPlugin } = require('vue-loader')
const webpack = require('webpack')
const WorkboxPlugin = require('workbox-webpack-plugin')
const WebpackSPDXPlugin = require('./build/WebpackSPDXPlugin.cjs')
const modules = require('./webpack.modules.cjs')
const WebpackSPDXPlugin = require('./WebpackSPDXPlugin.cjs')
const appVersion = readFileSync('./version.php').toString().match(/OC_Version.+\[([0-9]{2})/)?.[1] ?? 'unknown'
const appVersion = readFileSync(path.join(__dirname, '../../version.php')).toString().match(/OC_Version.+\[([0-9]{2})/)?.[1] ?? 'unknown'
const isDev = process.env.NODE_ENV === 'development'
const isTesting = process.env.TESTING === 'true'
@ -60,7 +60,7 @@ const config = {
entry: modulesToBuild(),
output: {
// Step away from the src folder and extract to the js folder
path: path.join(__dirname, 'dist'),
path: path.join(__dirname, '../../dist'),
// Let webpack determine automatically where it's located
publicPath: 'auto',
filename: '[name].js?v=[contenthash]',
@ -73,9 +73,7 @@ const config = {
const rel = path.relative(rootDir, info.absoluteResourcePath)
return `webpack:///nextcloud/${rel}`
},
clean: {
keep: /icons\.css/, // Keep static icons css
},
clean: false,
},
module: {
@ -182,7 +180,7 @@ const config = {
// Provide jQuery to jquery plugins as some are loaded before $ is exposed globally.
// We need to provide the path to node_moduels as otherwise npm link will fail due
// to tribute.js checking for jQuery in @nextcloud/vue
jQuery: path.resolve(path.join(__dirname, 'node_modules/jquery')),
jQuery: require.resolve('jquery'),
}),
new WorkboxPlugin.GenerateSW({
@ -256,7 +254,7 @@ const config = {
*/
'.js': ['.js', '.ts'],
},
symlinks: true,
symlinks: false,
fallback: {
fs: false,
},

@ -97,9 +97,6 @@ module.exports = {
'vue-settings-personal-webauthn': path.join(__dirname, 'apps/settings/src', 'main-personal-webauth.js'),
'declarative-settings-forms': path.join(__dirname, 'apps/settings/src', 'main-declarative-settings-forms.ts'),
},
sharebymail: {
'vue-settings-admin-sharebymail': path.join(__dirname, 'apps/sharebymail/src', 'main-admin.js'),
},
systemtags: {
init: path.join(__dirname, 'apps/systemtags/src', 'init.ts'),
admin: path.join(__dirname, 'apps/systemtags/src', 'admin.ts'),

@ -0,0 +1,10 @@
version = 1
SPDX-PackageName = "nextcloud"
SPDX-PackageSupplier = "Nextcloud <info@nextcloud.com>"
SPDX-PackageDownloadLocation = "https://github.com/nextcloud/server"
[[annotations]]
path = ["package.json", "package-lock.json", "tsconfig.json"]
precedence = "aggregate"
SPDX-FileCopyrightText = "2025 Nextcloud GmbH and Nextcloud contributors"
SPDX-License-Identifier = "AGPL-3.0-or-later"

File diff suppressed because it is too large Load Diff

@ -0,0 +1,31 @@
{
"name": "nextcloud-ui",
"version": "1.0.0",
"private": true,
"description": "Nextcloud Server Vue 3 UI",
"license": "AGPL-3.0-or-later",
"author": "Nextcloud GmbH and Nextcloud contributors",
"type": "module",
"scripts": {
"build": "vite build",
"dev": "NODE_ENV=development vite build --mode development",
"lint": "eslint --suppressions-location ../eslint-baseline.json --no-error-on-unmatched-pattern ./apps/*/ ./core/",
"lint:fix": "eslint --suppressions-location ../eslint-baseline.json --fix --no-error-on-unmatched-pattern ./apps/*/ ./core/",
"test": "vitest run --passWithNoTests",
"test:coverage": "vitest run --passWithNoTests --coverage --reporter=default",
"test:update-snapshots": "vitest run --update",
"test:watch": "vitest watch",
"watch": "NODE_ENV=development vite build --mode development --watch"
},
"browserslist": [
"extends @nextcloud/browserslist-config"
],
"devDependencies": {
"@nextcloud/vite-config": "^2.5.2",
"vite": "npm:rolldown-vite@^7.1.19"
},
"engines": {
"node": "^22.0.0",
"npm": "^10.5.0"
}
}

@ -0,0 +1,4 @@
{
"extends": "../../tsconfig.json",
"include": ["./apps", "./core"]
}

@ -0,0 +1,53 @@
/*!
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: CC0-1.0
*/
import { createAppConfig } from '@nextcloud/vite-config'
import { resolve } from 'node:path'
export default createAppConfig({
}, {
emptyOutputDirectory: {
additionalDirectories: [resolve(import.meta.dirname, '../..', 'dist')],
},
extractLicenseInformation: {
includeSourceMaps: true,
},
config: {
root: resolve(import.meta.dirname, '../..'),
resolve: {
preserveSymlinks: true,
},
build: {
outDir: 'dist',
rollupOptions: {
output: {
entryFileNames({ facadeModuleId }) {
const [, appId] = facadeModuleId!.match(/apps\/([^/]+)\//)!
return `${appId}-[name].mjs`
},
chunkFileNames: '[name]-[hash].chunk.mjs',
assetFileNames({ originalFileNames }) {
const [name] = originalFileNames
if (name) {
const [, appId] = name.match(/apps\/([^/]+)\//)!
return `${appId}-[name]-[hash][extname]`
}
return '[name]-[hash][extname]'
},
/* advancedChunks: {
groups: [{ name: 'common', test: /[\\/]node_modules[\\/]/ }],
// only include modules in the groups if they are used at least by 3 different chunks
minShareCount: 3,
// only include modules in the groups if they are smaller than 200kb on its own
maxModuleSize: 200 * 1024,
// define the groups output size (not too small but also not too big!)
minSize: 50 * 1024,
maxSize: 500 * 1024,
}, */
},
},
},
},
})

@ -1,9 +1,9 @@
/**
* SPDX-FileCopyrightText: 2023-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: CC0-1.0
*/
import vue from '@vitejs/plugin-vue2'
import vue from '@vitejs/plugin-vue'
import { exec } from 'node:child_process'
import { promisify } from 'node:util'
import { defaultExclude, defineConfig } from 'vitest/config'
@ -27,7 +27,7 @@ try {
export default defineConfig({
plugins: [vue()],
test: {
include: ['{apps,core}/**/*.{test,spec}.?(c|m)[jt]s?(x)'],
include: ['./apps/sharebymail/**/*.{test,spec}.?(c|m)[jt]s?(x)'],
environment: 'jsdom',
environmentOptions: {
jsdom: {

@ -12,13 +12,13 @@ npm run sass:icons
# Add licenses for source maps
if [ -d "dist" ]; then
for f in dist/*.js; do
for f in dist/*.js dist/*.mjs dist/*.css; do
# If license file and source map exists copy license for the source map
if [ -f "$f.license" ] && [ -f "$f.map" ]; then
# Remove existing link
[ -e "$f.map.license" ] || [ -L "$f.map.license" ] && rm "$f.map.license"
[ -L "$f.map.license" ] && rm "$f.map.license"
# Create a new link
ln -s "$(basename "$f.license")" "$f.map.license"
[ ! -e "$f.map.license" ] && ln -s "$(basename "$f.license")" "$f.map.license"
fi
done
echo "Copying licenses for sourcemaps done"

@ -11,7 +11,7 @@
@use 'fixes';
@use 'mobile';
@use 'tooltip';
@use 'public';
// If you include .css, it will be imported as url
@use '../../node_modules/@nextcloud/dialogs/dist/style' as *;
@use '../../node_modules/@nextcloud/password-confirmation/dist/style' as *;
@use 'public';

@ -159,6 +159,7 @@ export default defineConfig({
const ip = await startNextcloud(process.env.BRANCH, false, {
mounts,
exposePort: port,
forceRecreate: true,
})
// Setting container's IP as base Url
config.baseUrl = `http://localhost:${port}/index.php`

32689
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -12,142 +12,53 @@
},
"license": "AGPL-3.0-or-later",
"author": "Nextcloud GmbH and Nextcloud contributors",
"directories": {
"lib": "lib",
"test": "tests"
},
"type": "module",
"scripts": {
"build": "webpack --node-env production --progress",
"build": "build/demi.sh build",
"postbuild": "build/npm-post-build.sh",
"cypress": "cypress run --e2e",
"cypress:gui": "cypress open",
"cypress:version": "cypress version",
"dev": "webpack --node-env development --progress",
"lint": "eslint --suppressions-location build/eslint-baseline.json",
"lint:fix": "eslint --suppressions-location build/eslint-baseline.json --fix",
"dev": "build/demi.sh dev",
"postinstall": "build/demi.sh ci",
"lint": "eslint --suppressions-location build/eslint-baseline.json --no-error-on-unmatched-pattern ./cypress",
"postlint": "build/demi.sh lint",
"lint:fix": "build/demi.sh lint:fix",
"sass": "sass --style compressed --load-path core/css core/css/ $(for cssdir in $(find apps -mindepth 2 -maxdepth 2 -name \"css\"); do if ! $(git check-ignore -q $cssdir); then printf \"$cssdir \"; fi; done)",
"sass:icons": "babel-node core/src/icons.cjs",
"sass:icons": "node build/icons.mjs",
"sass:watch": "sass --watch --load-path core/css core/css/ $(for cssdir in $(find apps -mindepth 2 -maxdepth 2 -name \"css\"); do if ! $(git check-ignore -q $cssdir); then printf \"$cssdir \"; fi; done)",
"stylelint": "stylelint $(for appdir in $(ls apps); do if ! $(git check-ignore -q \"apps/$appdir\"); then printf \"'apps/$appdir/**/*.{scss,vue}' \"; fi; done) 'core/**/*.{scss,vue}'",
"stylelint:fix": "npm run stylelint -- --fix",
"test": "vitest run",
"test:coverage": "vitest run --coverage --reporter=default --reporter=junit --outputFile=test-report.junit.xml",
"test:update-snapshots": "vitest run --update",
"test:watch": "vitest watch",
"watch": "webpack --node-env development --progress --watch"
"test": "build/demi.sh test",
"test:coverage": "build/demi.sh test:coverage",
"test:update-snapshots": "build/demi.sh test:update-snapshots",
"test:watch": "build/demi.sh --parallel test:watch",
"watch": "build/demi.sh --parallel watch"
},
"browserslist": [
"extends @nextcloud/browserslist-config"
],
"overrides": {
"colors": "1.4.0"
},
"dependencies": {
"@chenfengyuan/vue-qrcode": "^1.0.2",
"@mdi/js": "^7.4.47",
"@mdi/svg": "^7.4.47",
"@nextcloud/auth": "^2.5.2",
"@nextcloud/axios": "^2.5.1",
"@nextcloud/browser-storage": "^0.5.0",
"@nextcloud/browserslist-config": "^3.1.0",
"@nextcloud/calendar-availability-vue": "^2.2.10",
"@nextcloud/capabilities": "^1.2.0",
"@nextcloud/axios": "^2.5.2",
"@nextcloud/dialogs": "^7.1.0",
"@nextcloud/event-bus": "^3.3.2",
"@nextcloud/files": "^3.12.0",
"@nextcloud/initial-state": "^3.0.0",
"@nextcloud/l10n": "^3.4.0",
"@nextcloud/logger": "^3.0.2",
"@nextcloud/moment": "^1.3.5",
"@nextcloud/password-confirmation": "^6.0.1",
"@nextcloud/paths": "^2.2.1",
"@nextcloud/router": "^3.0.1",
"@nextcloud/sharing": "^0.3.0",
"@nextcloud/upload": "^1.11.0",
"@nextcloud/vue": "^8.32.0",
"@simplewebauthn/browser": "^13.2.0",
"@vue/web-component-wrapper": "^1.3.0",
"@vueuse/components": "^11.3.0",
"@vueuse/core": "^11.3.0",
"@vueuse/integrations": "^11.3.0",
"backbone": "^1.6.1",
"blurhash": "^2.0.5",
"browserslist-useragent-regexp": "^4.1.3",
"camelcase": "^8.0.0",
"cancelable-promise": "^4.3.1",
"clipboard": "^2.0.11",
"color": "^5.0.2",
"core-js": "^3.45.0",
"crypto-browserify": "^3.12.1",
"davclient.js": "nextcloud-deps/davclient.js#59d7777d7fe290c5f1fd74a58e7eb529b63e153d",
"debounce": "^2.2.0",
"dompurify": "^3.2.7",
"escape-html": "^1.0.3",
"focus-trap": "^7.6.5",
"handlebars": "^4.7.8",
"is-svg": "^6.1.0",
"jquery": "~3.7",
"jquery-ui": "1.14.1",
"jquery-ui-dist": "^1.13.3",
"libphonenumber-js": "^1.12.24",
"lodash": "^4.17.21",
"marked": "^16.3.0",
"moment": "^2.30.1",
"moment-timezone": "^0.6.0",
"p-limit": "^7.1.1",
"p-queue": "^9.0.0",
"path": "^0.12.7",
"pinia": "^2.3.1",
"query-string": "^9.3.1",
"regenerator-runtime": "^0.14.1",
"select2": "3.5.1",
"snap.js": "^2.0.9",
"strengthify": "github:nextcloud/strengthify#0.5.9",
"throttle-debounce": "^5.0.2",
"underscore": "1.13.7",
"url-search-params-polyfill": "^8.2.5",
"v-click-outside": "^3.2.0",
"v-tooltip": "^2.1.3",
"vue": "^2.7.16",
"vue-click-outside": "^1.1.0",
"vue-cropperjs": "^4.2.0",
"vue-frag": "^1.4.3",
"vue-infinite-loading": "^2.4.5",
"vue-localstorage": "^0.6.2",
"vue-material-design-icons": "^5.3.1",
"vue-router": "^3.6.5",
"vuedraggable": "^2.24.3",
"vuex": "^3.6.2",
"vuex-router-sync": "^5.0.0",
"webdav": "^5.8.0"
"@nextcloud/vue": "^9.0.1",
"vue": "^3.5.22"
},
"devDependencies": {
"@babel/node": "^7.28.0",
"@babel/plugin-transform-private-methods": "^7.27.1",
"@babel/preset-typescript": "^7.27.1",
"@codecov/webpack-plugin": "^1.9.1",
"@nextcloud/babel-config": "^1.2.0",
"@nextcloud/browserslist-config": "^3.1.1",
"@nextcloud/e2e-test-server": "^0.4.0",
"@nextcloud/eslint-config": "^9.0.0-rc.5",
"@nextcloud/stylelint-config": "^3.1.0",
"@nextcloud/typings": "^1.9.1",
"@nextcloud/webpack-vue-config": "^6.3.0",
"@pinia/testing": "^0.1.7",
"@nextcloud/stylelint-config": "^3.1.1",
"@nextcloud/vite-config": "^2.5.2",
"@testing-library/cypress": "^10.1.0",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/user-event": "^14.6.1",
"@testing-library/vue": "^5.9.0",
"@types/dockerode": "^3.3.44",
"@types/wait-on": "^5.3.4",
"@vitejs/plugin-vue2": "^2.3.3",
"@vitest/coverage-v8": "^3.2.4",
"@vue/test-utils": "^1.3.5",
"@vue/tsconfig": "^0.5.1",
"@zip.js/zip.js": "^2.8.6",
"babel-loader-exclude-node-modules-except": "^1.2.1",
"babel-plugin-module-resolver": "^5.0.2",
"browserslist": "^4.26.3",
"colord": "^2.9.3",
"@vue/tsconfig": "^0.8.1",
"@zip.js/zip.js": "^2.8.8",
"concurrently": "^9.2.1",
"cypress": "^15.5.0",
"cypress-axe": "^1.7.0",
"cypress-if": "^1.13.2",
@ -155,32 +66,20 @@
"cypress-vite": "^1.8.0",
"cypress-wait-until": "^3.0.2",
"dockerode": "^4.0.9",
"eslint": "^9.36.0",
"eslint": "^9.38.0",
"eslint-plugin-cypress": "^5.2.0",
"eslint-plugin-no-only-tests": "^3.3.0",
"exports-loader": "^5.0.0",
"file-loader": "^6.2.0",
"handlebars-loader": "^1.7.3",
"jsdom": "^27.0.0",
"globals": "^16.4.0",
"jsdom": "^27.0.1",
"jsdom-testing-mocks": "^1.16.0",
"mime": "^4.1.0",
"msw": "^2.11.3",
"raw-loader": "^4.0.2",
"regextras": "^0.8.0",
"sass": "^1.93.2",
"stylelint": "^16.24.0",
"stylelint": "^16.25.0",
"stylelint-use-logical": "^2.1.2",
"ts-node": "^10.9.2",
"tslib": "^2.8.1",
"typescript": "^5.9.2",
"vite": "^7.1.11",
"vitest": "^3.2.4",
"vue-loader": "^15.11.1",
"vue-template-compiler": "^2.7.16",
"wait-on": "^8.0.4",
"webpack": "^5.102.1",
"webpack-cli": "^6.0.1",
"webpack-merge": "^6.0.1",
"workbox-webpack-plugin": "^7.3.0"
"wait-on": "^9.0.1"
},
"engines": {
"node": "^22.0.0",

@ -1,5 +1,8 @@
# SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: AGPL-3.0-or-later
# Used for performance testing, see .github/workflows/performance.yml
[[user]]
id = "test"
groups = ["test_group"]

@ -4,14 +4,12 @@
"exclude": ["./**/*.cy.ts"],
"compilerOptions": {
"lib": ["DOM", "ESNext"],
"types": ["node", "vue", "vue-router"],
"outDir": "./dist/",
"target": "ESNext",
"module": "ESNext",
// Set module resolution to bundler and `noEmit` to be able to set `allowImportingTsExtensions`, so we can import Typescript with .ts extension
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"noEmit": true,
"rewriteRelativeImportExtensions": true,
// Allow ts to import js files
"allowJs": true,
"allowSyntheticDefaultImports": true,
@ -21,15 +19,6 @@
"strict": true,
},
"vueCompilerOptions": {
"target": 2.7
"target": 3.6
},
"ts-node": {
// these options are overrides used only by ts-node
// same as our --compilerOptions flag and our TS_NODE_COMPILER_OPTIONS environment variable
"compilerOptions": {
"moduleResolution": "node",
"module": "commonjs",
"verbatimModuleSyntax": false
}
}
}

@ -0,0 +1,7 @@
/*!
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: CC0-1.0
*/
// stub - for the moment see build/frontend/vite.config.ts
export default {}

@ -0,0 +1,6 @@
/*!
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: CC0-1.0
*/
// temporary see build/frontend/vitest.config.mts