diff --git a/.gitattributes b/.gitattributes index 1bea4dc8f..c5b9ea10e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1 @@ -/build/commit_hash.js export-subst +/build/webpack.prod.conf.js export-subst diff --git a/.gitignore b/.gitignore index 01ffda9a8..0d5befd28 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,5 @@ test/e2e/reports selenium-debug.log .idea/ config/local.json -src/assets/emoji.json +static/emoji.json logs/ -__screenshots__/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 99c85dd36..de3beccca 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -50,15 +50,10 @@ test: APT_CACHE_DIR: apt-cache script: - mkdir -pv $APT_CACHE_DIR && apt-get -qq update + - apt install firefox-esr -y --no-install-recommends + - firefox --version - yarn - - yarn playwright install firefox - - yarn playwright install-deps - - yarn unit-ci - artifacts: - # When the test fails, upload screenshots for better context on why it fails - paths: - - test/**/__screenshots__ - when: on_failure + - yarn unit build: stage: build diff --git a/build/build.js b/build/build.js new file mode 100644 index 000000000..e84122888 --- /dev/null +++ b/build/build.js @@ -0,0 +1,43 @@ +// https://github.com/shelljs/shelljs +import('./check-versions.mjs').then(m => m.default()) +require('shelljs/global') +env.NODE_ENV = 'production' + +var path = require('path') +var config = require('../config') +var ora = require('ora') +var webpack = require('webpack') +var webpackConfig = require('./webpack.prod.conf') + +console.info( + ' Tip:\n' + + ' Built files are meant to be served over an HTTP server.\n' + + ' Opening index.html over file:// won\'t work.\n' +) + +var spinner = ora('building for production...') +spinner.start() + +var updateEmoji = require('./update-emoji').updateEmoji +updateEmoji() + +var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory) +rm('-rf', assetsPath) +mkdir('-p', assetsPath) +cp('-R', 'static/*', assetsPath) + +webpack(webpackConfig, function (err, stats) { + spinner.stop() + if (err) throw err + process.stdout.write(stats.toString({ + colors: true, + modules: false, + children: false, + chunks: false, + chunkModules: false + }) + '\n') + if (stats.hasErrors()) { + console.error('See above for errors.') + process.exit(1) + } +}) diff --git a/build/commit_hash.js b/build/commit_hash.js deleted file mode 100644 index c104af5d9..000000000 --- a/build/commit_hash.js +++ /dev/null @@ -1,18 +0,0 @@ -import childProcess from 'child_process' - -export const getCommitHash = (() => { - const subst = "$Format:%h$" - if(!subst.match(/Format:/)) { - return subst - } else { - try { - return childProcess - .execSync('git rev-parse --short HEAD') - .toString() - .trim() - } catch (e) { - console.error('Failed run git:', e) - return 'UNKNOWN' - } - } -}) diff --git a/build/copy_plugin.js b/build/copy_plugin.js deleted file mode 100644 index a783fe7ff..000000000 --- a/build/copy_plugin.js +++ /dev/null @@ -1,40 +0,0 @@ -import serveStatic from 'serve-static' -import { resolve } from 'node:path' -import { cp } from 'node:fs/promises' - -const getPrefix = s => { - const padEnd = s.endsWith('/') ? s : s + '/' - return padEnd.startsWith('/') ? padEnd : '/' + padEnd -} - -const copyPlugin = ({ inUrl, inFs }) => { - const prefix = getPrefix(inUrl) - const subdir = prefix.slice(1) - let copyTarget - const handler = serveStatic(inFs) - - return [{ - name: 'copy-plugin-serve', - apply: 'serve', - configureServer (server) { - server.middlewares.use(prefix, handler) - } - }, { - name: 'copy-plugin-build', - apply: 'build', - configResolved (config) { - copyTarget = resolve(config.root, config.build.outDir, subdir) - }, - closeBundle: { - order: 'post', - sequential: true, - async handler () { - console.log(`Copying '${inFs}' to ${copyTarget}...`) - await cp(inFs, copyTarget, { recursive: true }) - console.log('Done.') - } - } - }] -} - -export default copyPlugin diff --git a/build/dev-client.js b/build/dev-client.js new file mode 100644 index 000000000..18aa1e219 --- /dev/null +++ b/build/dev-client.js @@ -0,0 +1,9 @@ +/* eslint-disable */ +require('eventsource-polyfill') +var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') + +hotClient.subscribe(function (event) { + if (event.action === 'reload') { + window.location.reload() + } +}) diff --git a/build/dev-server.js b/build/dev-server.js new file mode 100644 index 000000000..8e3b3a12c --- /dev/null +++ b/build/dev-server.js @@ -0,0 +1,82 @@ +import('./check-versions.mjs').then(m => m.default()) +var config = require('../config') +if (!process.env.NODE_ENV) process.env.NODE_ENV = config.dev.env +var path = require('path') +var express = require('express') +var webpack = require('webpack') +var opn = require('opn') +var proxyMiddleware = require('http-proxy-middleware') +var webpackConfig = process.env.NODE_ENV === 'testing' + ? require('./webpack.prod.conf') + : require('./webpack.dev.conf') + +var updateEmoji = require('./update-emoji').updateEmoji +updateEmoji() + +// default port where dev server listens for incoming traffic +var port = process.env.PORT || config.dev.port +// Define HTTP proxies to your custom API backend +// https://github.com/chimurai/http-proxy-middleware +var proxyTable = config.dev.proxyTable + +var app = express() +var compiler = webpack(webpackConfig) + +var devMiddleware = require('webpack-dev-middleware')(compiler, { + publicPath: webpackConfig.output.publicPath, + writeToDisk: true, + stats: { + colors: true, + chunks: false + } +}) + +var hotMiddleware = require('webpack-hot-middleware')(compiler) + +// FIXME: The statement below gives error about hooks being required in webpack 5. +// force page reload when html-webpack-plugin template changes +// compiler.plugin('compilation', function (compilation) { +// compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) { +// // FIXME: This supposed to reload whole page when index.html is changed, +// // however now it reloads entire page on every breath, i suppose the order +// // of plugins changed or something. It's a minor thing and douesn't hurt +// // disabling it, constant reloads hurt much more + +// // hotMiddleware.publish({ action: 'reload' }) +// // cb() +// }) +// }) + +// proxy api requests +Object.keys(proxyTable).forEach(function (context) { + var options = proxyTable[context] + if (typeof options === 'string') { + options = { target: options } + } + options.pathFilter = context + app.use(proxyMiddleware.createProxyMiddleware(options)) +}) + +// handle fallback for HTML5 history API +app.use(require('connect-history-api-fallback')()) + +// serve webpack bundle output +app.use(devMiddleware) + +// enable hot-reload and state-preserving +// compilation error display +app.use(hotMiddleware) + +// serve pure static assets +var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory) +app.use(staticPath, express.static('./static')) + +module.exports = app.listen(port, function (err) { + if (err) { + console.error(err) + return + } + var uri = 'http://localhost:' + port + console.info('Listening at ' + uri + '\n') + // opn(uri) +}) diff --git a/build/service_worker_messages.js b/build/service_worker_messages.js deleted file mode 100644 index c078e8563..000000000 --- a/build/service_worker_messages.js +++ /dev/null @@ -1,30 +0,0 @@ -import { languages, langCodeToJsonName } from '../src/i18n/languages.js' -import { readFile } from 'node:fs/promises' -import { dirname, resolve } from 'node:path' -import { fileURLToPath } from 'node:url' - -const i18nDir = resolve( - dirname(dirname(fileURLToPath(import.meta.url))), - 'src/i18n' -) - -export const i18nFiles = languages.reduce((acc, lang) => { - const name = langCodeToJsonName(lang) - const file = resolve(i18nDir, name + '.json') - acc[lang] = file - return acc -}, {}) - -export const generateServiceWorkerMessages = async () => { - const msgArray = await Promise.all(Object.entries(i18nFiles).map(async ([lang, file]) => { - const fileContent = await readFile(file, 'utf-8') - const msg = { - notifications: JSON.parse(fileContent).notifications || {} - } - return [lang, msg] - })) - return msgArray.reduce((acc, [lang, msg]) => { - acc[lang] = msg - return acc - }, {}) -} diff --git a/build/sw_plugin.js b/build/sw_plugin.js deleted file mode 100644 index 90ab856ad..000000000 --- a/build/sw_plugin.js +++ /dev/null @@ -1,163 +0,0 @@ -import { fileURLToPath } from 'node:url' -import { dirname, resolve } from 'node:path' -import { readFile } from 'node:fs/promises' -import { build } from 'vite' -import * as esbuild from 'esbuild' -import { generateServiceWorkerMessages, i18nFiles } from './service_worker_messages.js' - -const getSWMessagesAsText = async () => { - const messages = await generateServiceWorkerMessages() - return `export default ${JSON.stringify(messages, undefined, 2)}` -} -const projectRoot = dirname(dirname(fileURLToPath(import.meta.url))) - -export const devSwPlugin = ({ - swSrc, - swDest, - transformSW, - alias -}) => { - const swFullSrc = resolve(projectRoot, swSrc) - const esbuildAlias = {} - Object.entries(alias).forEach(([source, dest]) => { - esbuildAlias[source] = dest.startsWith('/') ? projectRoot + dest : dest - }) - - return { - name: 'dev-sw-plugin', - apply: 'serve', - configResolved (conf) { - }, - resolveId (id) { - const name = id.startsWith('/') ? id.slice(1) : id - if (name === swDest) { - return swFullSrc - } - return null - }, - async load (id) { - if (id === swFullSrc) { - return readFile(swFullSrc, 'utf-8') - } - return null - }, - /** - * vite does not bundle the service worker - * during dev, and firefox does not support ESM as service worker - * https://bugzilla.mozilla.org/show_bug.cgi?id=1360870 - */ - async transform (code, id) { - if (id === swFullSrc && transformSW) { - const res = await esbuild.build({ - entryPoints: [swSrc], - bundle: true, - write: false, - outfile: 'sw-pleroma.js', - alias: esbuildAlias, - plugins: [{ - name: 'vite-like-root-resolve', - setup (b) { - b.onResolve( - { filter: new RegExp(/^\//) }, - args => ({ - path: resolve(projectRoot, args.path.slice(1)) - }) - ) - } - }, { - name: 'sw-messages', - setup (b) { - b.onResolve( - { filter: new RegExp('^' + swMessagesName + '$') }, - args => ({ - path: args.path, - namespace: 'sw-messages' - })) - b.onLoad( - { filter: /.*/, namespace: 'sw-messages' }, - async () => ({ - contents: await getSWMessagesAsText() - })) - } - }] - }) - const text = res.outputFiles[0].text - return text - } - } - } -} - -// Idea taken from -// https://github.com/vite-pwa/vite-plugin-pwa/blob/main/src/plugins/build.ts -// rollup does not support compiling to iife if we want to code-split; -// however, we must compile the service worker to iife because of browser support. -// Run another vite build just for the service worker targeting iife at -// the end of the build. -export const buildSwPlugin = ({ - swSrc, - swDest, -}) => { - let config - return { - name: 'build-sw-plugin', - enforce: 'post', - apply: 'build', - configResolved (resolvedConfig) { - config = { - define: resolvedConfig.define, - resolve: resolvedConfig.resolve, - plugins: [swMessagesPlugin()], - publicDir: false, - build: { - ...resolvedConfig.build, - lib: { - entry: swSrc, - formats: ['iife'], - name: 'sw_pleroma' - }, - emptyOutDir: false, - rollupOptions: { - output: { - entryFileNames: swDest - } - } - }, - configFile: false - } - }, - closeBundle: { - order: 'post', - sequential: true, - async handler () { - console.log('Building service worker for production') - await build(config) - } - } - } -} - -const swMessagesName = 'virtual:pleroma-fe/service_worker_messages' -const swMessagesNameResolved = '\0' + swMessagesName - -export const swMessagesPlugin = () => { - return { - name: 'sw-messages-plugin', - resolveId (id) { - if (id === swMessagesName) { - Object.values(i18nFiles).forEach(f => { - this.addWatchFile(f) - }) - return swMessagesNameResolved - } else { - return null - } - }, - async load (id) { - if (id === swMessagesNameResolved) { - return await getSWMessagesAsText() - } - return null - } - } -} diff --git a/build/update-emoji.js b/build/update-emoji.js index 5d578ba61..9f4b4e67a 100644 --- a/build/update-emoji.js +++ b/build/update-emoji.js @@ -1,23 +1,27 @@ -import emojis from '@kazvmoe-infra/unicode-emoji-json/data-by-group.json' with { type: 'json' } -import fs from 'fs' +module.exports = { + updateEmoji () { + const emojis = require('@kazvmoe-infra/unicode-emoji-json/data-by-group') + const fs = require('fs') -Object.keys(emojis) - .map(k => { - emojis[k].map(e => { - delete e.unicode_version - delete e.emoji_version - delete e.skin_tone_support_unicode_version - }) - }) + Object.keys(emojis) + .map(k => { + emojis[k].map(e => { + delete e.unicode_version + delete e.emoji_version + delete e.skin_tone_support_unicode_version + }) + }) -const res = {} -Object.keys(emojis) - .map(k => { - const groupId = k.replace('&', 'and').replace(/ /g, '-').toLowerCase() - res[groupId] = emojis[k] - }) + const res = {} + Object.keys(emojis) + .map(k => { + const groupId = k.replace('&', 'and').replace(/ /g, '-').toLowerCase() + res[groupId] = emojis[k] + }) -console.info('Updating emojis...') -fs.writeFileSync('src/assets/emoji.json', JSON.stringify(res)) -console.info('Done.') + console.info('Updating emojis...') + fs.writeFileSync('static/emoji.json', JSON.stringify(res)) + console.info('Done.') + } +} diff --git a/build/utils.js b/build/utils.js new file mode 100644 index 000000000..02fa2f724 --- /dev/null +++ b/build/utils.js @@ -0,0 +1,55 @@ +var path = require('path') +var config = require('../config') +var sass = require('sass') +var MiniCssExtractPlugin = require('mini-css-extract-plugin') + +exports.assetsPath = function (_path) { + var assetsSubDirectory = process.env.NODE_ENV === 'production' + ? config.build.assetsSubDirectory + : config.dev.assetsSubDirectory + return path.posix.join(assetsSubDirectory, _path) +} + +exports.cssLoaders = function (options) { + options = options || {} + + function generateLoaders (loaders) { + // Extract CSS when that option is specified + // (which is the case during production build) + if (options.extract) { + return [MiniCssExtractPlugin.loader].concat(loaders) + } else { + return ['vue-style-loader'].concat(loaders) + } + } + + // http://vuejs.github.io/vue-loader/configurations/extract-css.html + return [ + { + test: /\.(post)?css$/, + use: generateLoaders(['css-loader', 'postcss-loader']), + }, + { + test: /\.less$/, + use: generateLoaders(['css-loader', 'postcss-loader', 'less-loader']), + }, + { + test: /\.scss$/, + use: generateLoaders([ + 'css-loader', + 'postcss-loader', + { + loader: 'sass-loader', + options: { + api: 'modern' + } + } + ]) + } + ] +} + +// Generate loaders for standalone style files (outside of .vue) +exports.styleLoaders = function (options) { + return exports.cssLoaders(options) +} diff --git a/build/webpack.base.conf.js b/build/webpack.base.conf.js new file mode 100644 index 000000000..72b4ac0a6 --- /dev/null +++ b/build/webpack.base.conf.js @@ -0,0 +1,130 @@ +var path = require('path') +var config = require('../config') +var utils = require('./utils') +var projectRoot = path.resolve(__dirname, '../') +var ServiceWorkerWebpackPlugin = require('serviceworker-webpack5-plugin') +var CopyPlugin = require('copy-webpack-plugin'); +var { VueLoaderPlugin } = require('vue-loader') +var ESLintPlugin = require('eslint-webpack-plugin'); +var StylelintPlugin = require('stylelint-webpack-plugin'); + +var env = process.env.NODE_ENV +// check env & config/index.js to decide weither to enable CSS Sourcemaps for the +// various preprocessor loaders added to vue-loader at the end of this file +var cssSourceMapDev = (env === 'development' && config.dev.cssSourceMap) +var cssSourceMapProd = (env === 'production' && config.build.productionSourceMap) +var useCssSourceMap = cssSourceMapDev || cssSourceMapProd + +var now = Date.now() + +module.exports = { + entry: { + app: './src/main.js' + }, + output: { + path: config.build.assetsRoot, + publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath, + filename: '[name].js', + chunkFilename: '[name].js' + }, + optimization: { + splitChunks: { + chunks: 'all' + } + }, + resolve: { + extensions: ['.mjs', '.js', '.jsx', '.vue'], + modules: [ + path.join(__dirname, '../node_modules') + ], + alias: { + 'static': path.resolve(__dirname, '../static'), + 'src': path.resolve(__dirname, '../src'), + 'assets': path.resolve(__dirname, '../src/assets'), + 'components': path.resolve(__dirname, '../src/components'), + 'vue-i18n': 'vue-i18n/dist/vue-i18n.runtime.esm-bundler.js' + }, + fallback: { + 'querystring': require.resolve('querystring-es3'), + 'url': require.resolve('url/') + } + }, + module: { + noParse: /node_modules\/localforage\/dist\/localforage.js/, + rules: [ + { + enforce: 'post', + test: /\.(json5?|ya?ml)$/, // target json, json5, yaml and yml files + type: 'javascript/auto', + loader: '@intlify/vue-i18n-loader', + include: [ // Use `Rule.include` to specify the files of locale messages to be pre-compiled + path.resolve(__dirname, '../src/i18n') + ] + }, + { + test: /\.vue$/, + loader: 'vue-loader', + options: { + compilerOptions: { + isCustomElement(tag) { + if (tag === 'pinch-zoom') { + return true + } + return false + } + } + } + }, + { + test: /\.jsx?$/, + include: projectRoot, + exclude: /node_modules\/(?!tributejs)/, + use: 'babel-loader' + }, + { + test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, + type: 'asset', + generator: { + filename: utils.assetsPath('img/[name].[hash:7][ext]') + } + }, + { + test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, + type: 'asset', + generator: { + filename: utils.assetsPath('fonts/[name].[hash:7][ext]') + } + }, + { + test: /\.mjs$/, + include: /node_modules/, + type: 'javascript/auto' + } + ] + }, + plugins: [ + new ServiceWorkerWebpackPlugin({ + entry: path.join(__dirname, '..', 'src/sw.js'), + filename: 'sw-pleroma.js' + }), + new ESLintPlugin({ + formatter: require('eslint-formatter-friendly'), + overrideConfigFile: path.resolve(__dirname, '..', 'eslint.config.mjs'), + configType: 'flat' + }), + new StylelintPlugin({}), + new VueLoaderPlugin(), + // This copies Ruffle's WASM to a directory so that JS side can access it + new CopyPlugin({ + patterns: [ + { + from: "node_modules/@ruffle-rs/ruffle/**/*", + to: "static/ruffle/[name][ext]" + }, + ], + options: { + concurrency: 100, + }, + }) + ] +} diff --git a/build/webpack.dev.conf.js b/build/webpack.dev.conf.js new file mode 100644 index 000000000..2369e0e87 --- /dev/null +++ b/build/webpack.dev.conf.js @@ -0,0 +1,38 @@ +var config = require('../config') +var webpack = require('webpack') +var merge = require('webpack-merge') +var utils = require('./utils') +var baseWebpackConfig = require('./webpack.base.conf') +var HtmlWebpackPlugin = require('html-webpack-plugin') + +// add hot-reload related code to entry chunks +Object.keys(baseWebpackConfig.entry).forEach(function (name) { + baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name]) +}) + +module.exports = merge(baseWebpackConfig, { + module: { + rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap }) + }, + mode: 'development', + // eval-source-map is faster for development + devtool: 'eval-source-map', + plugins: [ + new webpack.DefinePlugin({ + 'process.env': config.dev.env, + 'COMMIT_HASH': JSON.stringify('DEV'), + 'DEV_OVERRIDES': JSON.stringify(config.dev.settings), + '__VUE_OPTIONS_API__': true, + '__VUE_PROD_DEVTOOLS__': false, + '__VUE_PROD_HYDRATION_MISMATCH_DETAILS__': false + }), + // https://github.com/glenjamin/webpack-hot-middleware#installation--usage + new webpack.HotModuleReplacementPlugin(), + // https://github.com/ampedandwired/html-webpack-plugin + new HtmlWebpackPlugin({ + filename: 'index.html', + template: 'index.html', + inject: true + }) + ] +}) diff --git a/build/webpack.prod.conf.js b/build/webpack.prod.conf.js new file mode 100644 index 000000000..4282e4bd0 --- /dev/null +++ b/build/webpack.prod.conf.js @@ -0,0 +1,105 @@ +var path = require('path') +var config = require('../config') +var utils = require('./utils') +var webpack = require('webpack') +var merge = require('webpack-merge') +var baseWebpackConfig = require('./webpack.base.conf') +var MiniCssExtractPlugin = require('mini-css-extract-plugin') +const CssMinimizerPlugin = require("css-minimizer-webpack-plugin") +var HtmlWebpackPlugin = require('html-webpack-plugin') +var env = process.env.NODE_ENV === 'testing' + ? require('../config/test.env') + : config.build.env + +let commitHash = (() => { + const subst = "$Format:%h$"; + if(!subst.match(/Format:/)) { + return subst; + } else { + return require('child_process') + .execSync('git rev-parse --short HEAD') + .toString(); + } +})(); + +var webpackConfig = merge(baseWebpackConfig, { + mode: 'production', + module: { + rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, extract: true }) + }, + devtool: config.build.productionSourceMap ? 'source-map' : false, + optimization: { + minimize: true, + splitChunks: { + chunks: 'all' + }, + minimizer: [ + `...`, + new CssMinimizerPlugin() + ] + }, + output: { + path: config.build.assetsRoot, + filename: utils.assetsPath('js/[name].[chunkhash].js'), + chunkFilename: utils.assetsPath('js/[name].[chunkhash].js') + }, + plugins: [ + // http://vuejs.github.io/vue-loader/workflow/production.html + new webpack.DefinePlugin({ + 'process.env': env, + 'COMMIT_HASH': JSON.stringify(commitHash), + 'DEV_OVERRIDES': JSON.stringify(undefined), + '__VUE_OPTIONS_API__': true, + '__VUE_PROD_DEVTOOLS__': false, + '__VUE_PROD_HYDRATION_MISMATCH_DETAILS__': false + }), + // extract css into its own file + new MiniCssExtractPlugin({ + filename: utils.assetsPath('css/[name].[contenthash].css') + }), + // generate dist index.html with correct asset hash for caching. + // you can customize output by editing /index.html + // see https://github.com/ampedandwired/html-webpack-plugin + new HtmlWebpackPlugin({ + filename: process.env.NODE_ENV === 'testing' + ? 'index.html' + : config.build.index, + template: 'index.html', + inject: true, + minify: { + removeComments: true, + collapseWhitespace: true, + removeAttributeQuotes: true, + ignoreCustomComments: [/server-generated-meta/] + // more options: + // https://github.com/kangax/html-minifier#options-quick-reference + } + }), + // split vendor js into its own file + // extract webpack runtime and module manifest to its own file in order to + // prevent vendor hash from being updated whenever app bundle is updated + // new webpack.optimize.SplitChunksPlugin({ + // name: ['app', 'vendor'] + // }), + ] +}) + +if (config.build.productionGzip) { + var CompressionWebpackPlugin = require('compression-webpack-plugin') + + webpackConfig.plugins.push( + new CompressionWebpackPlugin({ + asset: '[path].gz[query]', + algorithm: 'gzip', + test: new RegExp( + '\\.(' + + config.build.productionGzipExtensions.join('|') + + ')$' + ), + threshold: 10240, + minRatio: 0.8 + }) + ) +} + +module.exports = webpackConfig diff --git a/changelog.d/boot-improvements.change b/changelog.d/boot-improvements.change deleted file mode 100644 index 8007d2332..000000000 --- a/changelog.d/boot-improvements.change +++ /dev/null @@ -1 +0,0 @@ -Speed up initial boot. diff --git a/changelog.d/cover-image-path.change b/changelog.d/cover-image-path.change deleted file mode 100644 index 2c9c4d1f3..000000000 --- a/changelog.d/cover-image-path.change +++ /dev/null @@ -1 +0,0 @@ -BREAKING: static/img/nsfw.2958239.png is now static/img/nsfw.DepQPhG0.png, which may affect people who specify exactly this path as the cover image diff --git a/changelog.d/draft-save.fix b/changelog.d/draft-save.fix deleted file mode 100644 index 7e3097e04..000000000 --- a/changelog.d/draft-save.fix +++ /dev/null @@ -1 +0,0 @@ -Fix draft saving when auto-save is off diff --git a/changelog.d/migrate-bookmark-folders-store-pinia.skip b/changelog.d/migrate-bookmark-folders-store-pinia.skip deleted file mode 100644 index e69de29bb..000000000 diff --git a/changelog.d/no-non-esm-script.remove b/changelog.d/no-non-esm-script.remove deleted file mode 100644 index db63f90e2..000000000 --- a/changelog.d/no-non-esm-script.remove +++ /dev/null @@ -1 +0,0 @@ -BREAKING: drop support for browsers that do not support ` +
diff --git a/package.json b/package.json index 3e07e0a85..87f7317fb 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,10 @@ "author": "Pleroma contributors ", "private": false, "scripts": { - "dev": "node build/update-emoji.js && vite dev", - "build": "node build/update-emoji.js && vite build", - "unit": "node build/update-emoji.js && vitest --run", - "unit-ci": "node build/update-emoji.js && vitest --run --browser.headless", - "unit:watch": "node build/update-emoji.js && vitest", + "dev": "node build/dev-server.js", + "build": "node build/build.js", + "unit": "karma start test/unit/karma.conf.js --single-run", + "unit:watch": "karma start test/unit/karma.conf.js --single-run=false", "e2e": "node test/e2e/runner.js", "test": "yarn run unit && yarn run e2e", "stylelint": "yarn exec stylelint '**/*.scss' '**/*.vue'", @@ -28,7 +27,6 @@ "@ruffle-rs/ruffle": "0.1.0-nightly.2025.1.13", "@vuelidate/core": "2.0.3", "@vuelidate/validators": "2.0.4", - "@web3-storage/parse-link-header": "^3.1.0", "body-scroll-lock": "3.1.5", "chromatism": "3.0.0", "click-outside-vue3": "4.0.1", @@ -58,22 +56,23 @@ "@babel/plugin-transform-runtime": "7.26.9", "@babel/preset-env": "7.26.9", "@babel/register": "7.25.9", + "@intlify/vue-i18n-loader": "5.0.1", "@ungap/event-target": "0.2.4", - "@vitejs/plugin-vue": "^5.2.1", - "@vitejs/plugin-vue-jsx": "^4.1.1", - "@vitest/browser": "^3.0.7", - "@vitest/ui": "^3.0.7", "@vue/babel-helper-vue-jsx-merge-props": "1.4.0", "@vue/babel-plugin-jsx": "1.2.5", "@vue/compiler-sfc": "3.5.13", "@vue/test-utils": "2.4.6", "autoprefixer": "10.4.20", + "babel-loader": "9.2.1", "babel-plugin-lodash": "3.3.4", "chai": "4.5.0", "chalk": "5.4.1", "chromedriver": "133.0.2", "connect-history-api-fallback": "2.0.0", + "copy-webpack-plugin": "12.0.2", "cross-spawn": "7.0.6", + "css-loader": "7.1.2", + "css-minimizer-webpack-plugin": "7.0.0", "custom-event-polyfill": "1.0.7", "eslint": "9.20.1", "eslint-config-standard": "17.1.0", @@ -82,23 +81,38 @@ "eslint-plugin-n": "17.15.1", "eslint-plugin-promise": "7.2.1", "eslint-plugin-vue": "9.32.0", + "eslint-webpack-plugin": "4.2.0", "eventsource-polyfill": "0.9.6", "express": "4.21.2", "function-bind": "1.1.2", + "html-webpack-plugin": "5.6.3", "http-proxy-middleware": "3.0.3", "iso-639-1": "3.1.5", + "json-loader": "0.5.7", + "karma": "6.4.4", + "karma-coverage": "2.2.1", + "karma-firefox-launcher": "2.1.3", + "karma-mocha": "2.0.1", + "karma-mocha-reporter": "2.2.5", + "karma-sinon-chai": "2.0.2", + "karma-sourcemap-loader": "0.4.0", + "karma-spec-reporter": "0.0.36", + "karma-webpack": "5.0.1", "lodash": "4.17.21", + "mini-css-extract-plugin": "2.9.2", + "mocha": "11.1.0", "nightwatch": "2.6.25", "opn": "5.5.0", "ora": "0.4.1", - "playwright": "1.49.1", "postcss": "8.5.2", "postcss-html": "^1.5.0", + "postcss-loader": "7.3.4", "postcss-scss": "^4.0.6", "sass": "1.85.0", + "sass-loader": "13.3.3", "selenium-server": "3.141.59", "semver": "7.7.1", - "serve-static": "1.16.2", + "serviceworker-webpack5-plugin": "2.0.0", "shelljs": "0.8.5", "sinon": "15.2.0", "sinon-chai": "3.7.0", @@ -108,12 +122,14 @@ "stylelint-config-recommended-vue": "^1.4.0", "stylelint-config-standard": "29.0.0", "stylelint-rscss": "0.4.0", - "vite": "^6.1.0", - "vite-plugin-eslint2": "^5.0.3", - "vite-plugin-stylelint": "^6.0.0", - "vitest": "^3.0.7" + "stylelint-webpack-plugin": "^3.3.0", + "vue-loader": "17.4.2", + "vue-style-loader": "4.1.3", + "webpack": "5.97.1", + "webpack-dev-middleware": "3.7.3", + "webpack-hot-middleware": "2.26.1", + "webpack-merge": "0.20.0" }, - "type": "module", "engines": { "node": ">= 16.0.0" }, diff --git a/postcss.config.js b/postcss.config.js index 95ebbf2a6..88752c6cb 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,7 +1,5 @@ -import autoprefixer from 'autoprefixer' - -export default { +module.exports = { plugins: [ - autoprefixer + require('autoprefixer') ] } diff --git a/public/static/pleromatan_apology_fox_small.webp b/public/static/pleromatan_apology_fox_small.webp deleted file mode 100644 index eacbf3cbf..000000000 Binary files a/public/static/pleromatan_apology_fox_small.webp and /dev/null differ diff --git a/public/static/pleromatan_apology_small.webp b/public/static/pleromatan_apology_small.webp deleted file mode 100644 index e27e06d06..000000000 Binary files a/public/static/pleromatan_apology_small.webp and /dev/null differ diff --git a/src/App.scss b/src/App.scss index 34cb590a2..fa02ad929 100644 --- a/src/App.scss +++ b/src/App.scss @@ -931,7 +931,7 @@ option { #splash { pointer-events: none; - transition: opacity 0.5s; + transition: opacity 2s; opacity: 1; &.hidden { @@ -960,7 +960,7 @@ option { &.dead { animation-name: dead; - animation-duration: 0.5s; + animation-duration: 2s; animation-iteration-count: 1; transform: rotateX(90deg) rotateY(0) rotateZ(-45deg); } diff --git a/src/assets/pleromatan_apology.png b/src/assets/pleromatan_apology.png new file mode 120000 index 000000000..a7f6191f6 --- /dev/null +++ b/src/assets/pleromatan_apology.png @@ -0,0 +1 @@ +../../static/pleromatan_apology.png \ No newline at end of file diff --git a/src/assets/pleromatan_apology_fox.png b/src/assets/pleromatan_apology_fox.png new file mode 120000 index 000000000..b3db4af3f --- /dev/null +++ b/src/assets/pleromatan_apology_fox.png @@ -0,0 +1 @@ +../../static/pleromatan_apology_fox.png \ No newline at end of file diff --git a/src/boot/after_store.js b/src/boot/after_store.js index 8e27d7457..ada812739 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -230,16 +230,12 @@ const getStickers = async ({ store }) => { const getAppSecret = async ({ store }) => { const { state, commit } = store const { oauth, instance } = state - if (oauth.userToken) { - commit('setBackendInteractor', backendInteractorService(store.getters.getToken())) - } else { - return getOrCreateApp({ ...oauth, instance: instance.server, commit }) - .then((app) => getClientToken({ ...app, instance: instance.server })) - .then((token) => { - commit('setAppToken', token.access_token) - commit('setBackendInteractor', backendInteractorService(store.getters.getToken())) - }) - } + return getOrCreateApp({ ...oauth, instance: instance.server, commit }) + .then((app) => getClientToken({ ...app, instance: instance.server })) + .then((token) => { + commit('setAppToken', token.access_token) + commit('setBackendInteractor', backendInteractorService(store.getters.getToken())) + }) } const resolveStaffAccounts = ({ store, accounts }) => { @@ -381,9 +377,10 @@ const afterStoreSetup = async ({ pinia, store, storageError, i18n }) => { getInstanceConfig({ store }) ]).catch(e => Promise.reject(e)) + await store.dispatch('loadDrafts') + // Start fetching things that don't need to block the UI store.dispatch('fetchMutes') - store.dispatch('loadDrafts') useAnnouncementsStore().startFetchingAnnouncements() getTOS({ store }) getStickers({ store }) diff --git a/src/components/bookmark_folder_edit/bookmark_folder_edit.js b/src/components/bookmark_folder_edit/bookmark_folder_edit.js index bb96585bf..4ed440eb7 100644 --- a/src/components/bookmark_folder_edit/bookmark_folder_edit.js +++ b/src/components/bookmark_folder_edit/bookmark_folder_edit.js @@ -1,7 +1,5 @@ import EmojiPicker from '../emoji_picker/emoji_picker.vue' import apiService from '../../services/api/api.service' -import { useInterfaceStore } from 'src/stores/interface' -import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders' const BookmarkFolderEdit = { data () { @@ -54,18 +52,18 @@ const BookmarkFolderEdit = { this.emojiPickerExpanded = false }, updateFolder () { - useBookmarkFoldersStore().updateBookmarkFolder({ folderId: this.id, name: this.nameDraft, emoji: this.emojiDraft }) + this.$store.dispatch('setBookmarkFolder', { folderId: this.id, name: this.nameDraft, emoji: this.emojiDraft }) .then(() => { this.$router.push({ name: 'bookmark-folders' }) }) }, createFolder () { - useBookmarkFoldersStore().createBookmarkFolder({ name: this.nameDraft, emoji: this.emojiDraft }) + this.$store.dispatch('createBookmarkFolder', { name: this.nameDraft, emoji: this.emojiDraft }) .then(() => { this.$router.push({ name: 'bookmark-folders' }) }) .catch((e) => { - useInterfaceStore().pushGlobalNotice({ + this.$store.useInterfaceStore().pushGlobalNotice({ messageKey: 'bookmark_folders.error', messageArgs: [e.message], level: 'error' @@ -73,7 +71,7 @@ const BookmarkFolderEdit = { }) }, deleteFolder () { - useBookmarkFoldersStore().deleteBookmarkFolder({ folderId: this.id }) + this.$store.dispatch('deleteBookmarkFolder', { folderId: this.id }) this.$router.push({ name: 'bookmark-folders' }) } } diff --git a/src/components/bookmark_folders/bookmark_folders.js b/src/components/bookmark_folders/bookmark_folders.js index 096f3769d..9f1f1fed0 100644 --- a/src/components/bookmark_folders/bookmark_folders.js +++ b/src/components/bookmark_folders/bookmark_folders.js @@ -1,5 +1,4 @@ import BookmarkFolderCard from '../bookmark_folder_card/bookmark_folder_card.vue' -import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders' const BookmarkFolders = { data () { @@ -12,7 +11,7 @@ const BookmarkFolders = { }, computed: { bookmarkFolders () { - return useBookmarkFoldersStore().allFolders + return this.$store.state.bookmarkFolders.allFolders } }, methods: { diff --git a/src/components/bookmark_folders_menu/bookmark_folders_menu_content.js b/src/components/bookmark_folders_menu/bookmark_folders_menu_content.js index dc46b91b3..43db7df32 100644 --- a/src/components/bookmark_folders_menu/bookmark_folders_menu_content.js +++ b/src/components/bookmark_folders_menu/bookmark_folders_menu_content.js @@ -1,7 +1,6 @@ -import { mapState } from 'pinia' +import { mapState } from 'vuex' import NavigationEntry from 'src/components/navigation/navigation_entry.vue' import { getBookmarkFolderEntries } from 'src/components/navigation/filter.js' -import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders' export const BookmarkFoldersMenuContent = { props: [ @@ -11,7 +10,7 @@ export const BookmarkFoldersMenuContent = { NavigationEntry }, computed: { - ...mapState(useBookmarkFoldersStore, { + ...mapState({ folders: getBookmarkFolderEntries }) } diff --git a/src/components/navigation/filter.js b/src/components/navigation/filter.js index 12ab9266e..0974bc1e3 100644 --- a/src/components/navigation/filter.js +++ b/src/components/navigation/filter.js @@ -19,7 +19,7 @@ export const getListEntries = store => store.allLists.map(list => ({ iconLetter: list.title[0] })) -export const getBookmarkFolderEntries = store => store.allFolders.map(folder => ({ +export const getBookmarkFolderEntries = state => state.bookmarkFolders.allFolders.map(folder => ({ name: 'bookmark-folder-' + folder.id, routeObject: { name: 'bookmark-folder', params: { id: folder.id } }, labelRaw: folder.name, diff --git a/src/components/navigation/navigation_pins.js b/src/components/navigation/navigation_pins.js index f9cdef71b..e657e94a0 100644 --- a/src/components/navigation/navigation_pins.js +++ b/src/components/navigation/navigation_pins.js @@ -19,7 +19,6 @@ import { } from '@fortawesome/free-solid-svg-icons' import { useListsStore } from 'src/stores/lists' import { useAnnouncementsStore } from 'src/stores/announcements' -import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders' library.add( faUsers, @@ -53,10 +52,8 @@ const NavPanel = { ...mapPiniaState(useAnnouncementsStore, { supportsAnnouncements: store => store.supportsAnnouncements }), - ...mapPiniaState(useBookmarkFoldersStore, { - bookmarks: getBookmarkFolderEntries - }), ...mapState({ + bookmarks: getBookmarkFolderEntries, currentUser: state => state.users.currentUser, followRequestCount: state => state.api.followRequests.length, privateMode: state => state.instance.private, diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index 87c88d8f7..f9eb1bdcd 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -376,14 +376,6 @@ const PostStatusForm = { this.newStatus.hasPoll ) && this.saveable }, - hasEmptyDraft () { - return this.newStatus.id && !( - this.newStatus.status || - this.newStatus.spoilerText || - this.newStatus.files?.length || - this.newStatus.hasPoll - ) - }, ...mapGetters(['mergedConfig']), ...mapState(useInterfaceStore, { mobileLayout: store => store.mobileLayout @@ -792,7 +784,7 @@ const PostStatusForm = { this.$emit('draft-done') } }) - } else if (this.hasEmptyDraft) { + } else if (this.newStatus.id) { // There is a draft, but there is nothing in it, clear it return this.abandonDraft() .then(() => { diff --git a/src/components/post_status_form/post_status_form.vue b/src/components/post_status_form/post_status_form.vue index 303413b55..260a89cb7 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -322,7 +322,6 @@ trigger="click" placement="bottom" :offset="{ y: 5 }" - :trigger-attrs="{ 'aria-label': $t('post_status.more_post_actions') }" > -