diff --git a/.gitattributes b/.gitattributes index c5b9ea10e..1bea4dc8f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1 @@ -/build/webpack.prod.conf.js export-subst +/build/commit_hash.js export-subst diff --git a/.gitignore b/.gitignore index 0d5befd28..01ffda9a8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,6 @@ test/e2e/reports selenium-debug.log .idea/ config/local.json -static/emoji.json +src/assets/emoji.json logs/ +__screenshots__/ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index de3beccca..99c85dd36 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -50,10 +50,15 @@ 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 unit + - 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 build: stage: build diff --git a/build/build.js b/build/build.js deleted file mode 100644 index e84122888..000000000 --- a/build/build.js +++ /dev/null @@ -1,43 +0,0 @@ -// 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 new file mode 100644 index 000000000..c104af5d9 --- /dev/null +++ b/build/commit_hash.js @@ -0,0 +1,18 @@ +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 new file mode 100644 index 000000000..a783fe7ff --- /dev/null +++ b/build/copy_plugin.js @@ -0,0 +1,40 @@ +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 deleted file mode 100644 index 18aa1e219..000000000 --- a/build/dev-client.js +++ /dev/null @@ -1,9 +0,0 @@ -/* 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 deleted file mode 100644 index 8e3b3a12c..000000000 --- a/build/dev-server.js +++ /dev/null @@ -1,82 +0,0 @@ -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 new file mode 100644 index 000000000..c078e8563 --- /dev/null +++ b/build/service_worker_messages.js @@ -0,0 +1,30 @@ +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 new file mode 100644 index 000000000..90ab856ad --- /dev/null +++ b/build/sw_plugin.js @@ -0,0 +1,163 @@ +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 9f4b4e67a..5d578ba61 100644 --- a/build/update-emoji.js +++ b/build/update-emoji.js @@ -1,27 +1,23 @@ -module.exports = { - updateEmoji () { - const emojis = require('@kazvmoe-infra/unicode-emoji-json/data-by-group') - const fs = require('fs') +import emojis from '@kazvmoe-infra/unicode-emoji-json/data-by-group.json' with { type: 'json' } +import fs from '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('static/emoji.json', JSON.stringify(res)) - console.info('Done.') - } -} +console.info('Updating emojis...') +fs.writeFileSync('src/assets/emoji.json', JSON.stringify(res)) +console.info('Done.') diff --git a/build/utils.js b/build/utils.js deleted file mode 100644 index 02fa2f724..000000000 --- a/build/utils.js +++ /dev/null @@ -1,55 +0,0 @@ -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 deleted file mode 100644 index 72b4ac0a6..000000000 --- a/build/webpack.base.conf.js +++ /dev/null @@ -1,130 +0,0 @@ -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 deleted file mode 100644 index 2369e0e87..000000000 --- a/build/webpack.dev.conf.js +++ /dev/null @@ -1,38 +0,0 @@ -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 deleted file mode 100644 index 4282e4bd0..000000000 --- a/build/webpack.prod.conf.js +++ /dev/null @@ -1,105 +0,0 @@ -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 new file mode 100644 index 000000000..8007d2332 --- /dev/null +++ b/changelog.d/boot-improvements.change @@ -0,0 +1 @@ +Speed up initial boot. diff --git a/changelog.d/cover-image-path.change b/changelog.d/cover-image-path.change new file mode 100644 index 000000000..2c9c4d1f3 --- /dev/null +++ b/changelog.d/cover-image-path.change @@ -0,0 +1 @@ +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 new file mode 100644 index 000000000..7e3097e04 --- /dev/null +++ b/changelog.d/draft-save.fix @@ -0,0 +1 @@ +Fix draft saving when auto-save is off diff --git a/static/.gitkeep b/changelog.d/migrate-bookmark-folders-store-pinia.skip similarity index 100% rename from static/.gitkeep rename to changelog.d/migrate-bookmark-folders-store-pinia.skip diff --git a/changelog.d/no-non-esm-script.remove b/changelog.d/no-non-esm-script.remove new file mode 100644 index 000000000..db63f90e2 --- /dev/null +++ b/changelog.d/no-non-esm-script.remove @@ -0,0 +1 @@ +BREAKING: drop support for browsers that do not support ` diff --git a/package.json b/package.json index 87f7317fb..3e07e0a85 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,11 @@ "author": "Pleroma contributors ", "private": false, "scripts": { - "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", + "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", "e2e": "node test/e2e/runner.js", "test": "yarn run unit && yarn run e2e", "stylelint": "yarn exec stylelint '**/*.scss' '**/*.vue'", @@ -27,6 +28,7 @@ "@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", @@ -56,23 +58,22 @@ "@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", @@ -81,38 +82,23 @@ "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", - "serviceworker-webpack5-plugin": "2.0.0", + "serve-static": "1.16.2", "shelljs": "0.8.5", "sinon": "15.2.0", "sinon-chai": "3.7.0", @@ -122,14 +108,12 @@ "stylelint-config-recommended-vue": "^1.4.0", "stylelint-config-standard": "29.0.0", "stylelint-rscss": "0.4.0", - "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" + "vite": "^6.1.0", + "vite-plugin-eslint2": "^5.0.3", + "vite-plugin-stylelint": "^6.0.0", + "vitest": "^3.0.7" }, + "type": "module", "engines": { "node": ">= 16.0.0" }, diff --git a/postcss.config.js b/postcss.config.js index 88752c6cb..95ebbf2a6 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,5 +1,7 @@ -module.exports = { +import autoprefixer from 'autoprefixer' + +export default { plugins: [ - require('autoprefixer') + autoprefixer ] } diff --git a/static/.gitignore b/public/static/.gitignore similarity index 100% rename from static/.gitignore rename to public/static/.gitignore diff --git a/public/static/.gitkeep b/public/static/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/static/aurora_borealis.jpg b/public/static/aurora_borealis.jpg similarity index 100% rename from static/aurora_borealis.jpg rename to public/static/aurora_borealis.jpg diff --git a/static/config.json b/public/static/config.json similarity index 100% rename from static/config.json rename to public/static/config.json diff --git a/static/logo.svg b/public/static/logo.svg similarity index 100% rename from static/logo.svg rename to public/static/logo.svg diff --git a/static/palettes/index.json b/public/static/palettes/index.json similarity index 100% rename from static/palettes/index.json rename to public/static/palettes/index.json diff --git a/static/pleromatan_apology.png b/public/static/pleromatan_apology.png similarity index 100% rename from static/pleromatan_apology.png rename to public/static/pleromatan_apology.png diff --git a/static/pleromatan_apology_fox.png b/public/static/pleromatan_apology_fox.png similarity index 100% rename from static/pleromatan_apology_fox.png rename to public/static/pleromatan_apology_fox.png diff --git a/public/static/pleromatan_apology_fox_small.webp b/public/static/pleromatan_apology_fox_small.webp new file mode 100644 index 000000000..eacbf3cbf Binary files /dev/null and b/public/static/pleromatan_apology_fox_small.webp differ diff --git a/public/static/pleromatan_apology_small.webp b/public/static/pleromatan_apology_small.webp new file mode 100644 index 000000000..e27e06d06 Binary files /dev/null and b/public/static/pleromatan_apology_small.webp differ diff --git a/static/pleromatan_orz.png b/public/static/pleromatan_orz.png similarity index 100% rename from static/pleromatan_orz.png rename to public/static/pleromatan_orz.png diff --git a/static/pleromatan_orz_fox.png b/public/static/pleromatan_orz_fox.png similarity index 100% rename from static/pleromatan_orz_fox.png rename to public/static/pleromatan_orz_fox.png diff --git a/static/styles.json b/public/static/styles.json similarity index 100% rename from static/styles.json rename to public/static/styles.json diff --git a/static/styles/Breezy DX.iss b/public/static/styles/Breezy DX.iss similarity index 100% rename from static/styles/Breezy DX.iss rename to public/static/styles/Breezy DX.iss diff --git a/static/styles/Redmond DX.iss b/public/static/styles/Redmond DX.iss similarity index 100% rename from static/styles/Redmond DX.iss rename to public/static/styles/Redmond DX.iss diff --git a/static/styles/index.json b/public/static/styles/index.json similarity index 100% rename from static/styles/index.json rename to public/static/styles/index.json diff --git a/static/terms-of-service.html b/public/static/terms-of-service.html similarity index 100% rename from static/terms-of-service.html rename to public/static/terms-of-service.html diff --git a/static/themes/breezy-dark.json b/public/static/themes/breezy-dark.json similarity index 100% rename from static/themes/breezy-dark.json rename to public/static/themes/breezy-dark.json diff --git a/static/themes/breezy-light.json b/public/static/themes/breezy-light.json similarity index 100% rename from static/themes/breezy-light.json rename to public/static/themes/breezy-light.json diff --git a/static/themes/mammal.json b/public/static/themes/mammal.json similarity index 100% rename from static/themes/mammal.json rename to public/static/themes/mammal.json diff --git a/static/themes/paper.json b/public/static/themes/paper.json similarity index 100% rename from static/themes/paper.json rename to public/static/themes/paper.json diff --git a/static/themes/pleroma-dark.json b/public/static/themes/pleroma-dark.json similarity index 100% rename from static/themes/pleroma-dark.json rename to public/static/themes/pleroma-dark.json diff --git a/static/themes/pleroma-light.json b/public/static/themes/pleroma-light.json similarity index 100% rename from static/themes/pleroma-light.json rename to public/static/themes/pleroma-light.json diff --git a/static/themes/redmond-xx-se.json b/public/static/themes/redmond-xx-se.json similarity index 100% rename from static/themes/redmond-xx-se.json rename to public/static/themes/redmond-xx-se.json diff --git a/static/themes/redmond-xx.json b/public/static/themes/redmond-xx.json similarity index 100% rename from static/themes/redmond-xx.json rename to public/static/themes/redmond-xx.json diff --git a/static/themes/redmond-xxi.json b/public/static/themes/redmond-xxi.json similarity index 100% rename from static/themes/redmond-xxi.json rename to public/static/themes/redmond-xxi.json diff --git a/src/App.scss b/src/App.scss index fa02ad929..34cb590a2 100644 --- a/src/App.scss +++ b/src/App.scss @@ -931,7 +931,7 @@ option { #splash { pointer-events: none; - transition: opacity 2s; + transition: opacity 0.5s; opacity: 1; &.hidden { @@ -960,7 +960,7 @@ option { &.dead { animation-name: dead; - animation-duration: 2s; + animation-duration: 0.5s; 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 deleted file mode 120000 index a7f6191f6..000000000 --- a/src/assets/pleromatan_apology.png +++ /dev/null @@ -1 +0,0 @@ -../../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 deleted file mode 120000 index b3db4af3f..000000000 --- a/src/assets/pleromatan_apology_fox.png +++ /dev/null @@ -1 +0,0 @@ -../../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 ada812739..8e27d7457 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -230,12 +230,16 @@ const getStickers = async ({ store }) => { const getAppSecret = async ({ store }) => { const { state, commit } = store const { oauth, instance } = state - 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())) - }) + 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())) + }) + } } const resolveStaffAccounts = ({ store, accounts }) => { @@ -377,10 +381,9 @@ 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 4ed440eb7..bb96585bf 100644 --- a/src/components/bookmark_folder_edit/bookmark_folder_edit.js +++ b/src/components/bookmark_folder_edit/bookmark_folder_edit.js @@ -1,5 +1,7 @@ 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 () { @@ -52,18 +54,18 @@ const BookmarkFolderEdit = { this.emojiPickerExpanded = false }, updateFolder () { - this.$store.dispatch('setBookmarkFolder', { folderId: this.id, name: this.nameDraft, emoji: this.emojiDraft }) + useBookmarkFoldersStore().updateBookmarkFolder({ folderId: this.id, name: this.nameDraft, emoji: this.emojiDraft }) .then(() => { this.$router.push({ name: 'bookmark-folders' }) }) }, createFolder () { - this.$store.dispatch('createBookmarkFolder', { name: this.nameDraft, emoji: this.emojiDraft }) + useBookmarkFoldersStore().createBookmarkFolder({ name: this.nameDraft, emoji: this.emojiDraft }) .then(() => { this.$router.push({ name: 'bookmark-folders' }) }) .catch((e) => { - this.$store.useInterfaceStore().pushGlobalNotice({ + useInterfaceStore().pushGlobalNotice({ messageKey: 'bookmark_folders.error', messageArgs: [e.message], level: 'error' @@ -71,7 +73,7 @@ const BookmarkFolderEdit = { }) }, deleteFolder () { - this.$store.dispatch('deleteBookmarkFolder', { folderId: this.id }) + useBookmarkFoldersStore().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 9f1f1fed0..096f3769d 100644 --- a/src/components/bookmark_folders/bookmark_folders.js +++ b/src/components/bookmark_folders/bookmark_folders.js @@ -1,4 +1,5 @@ import BookmarkFolderCard from '../bookmark_folder_card/bookmark_folder_card.vue' +import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders' const BookmarkFolders = { data () { @@ -11,7 +12,7 @@ const BookmarkFolders = { }, computed: { bookmarkFolders () { - return this.$store.state.bookmarkFolders.allFolders + return useBookmarkFoldersStore().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 43db7df32..dc46b91b3 100644 --- a/src/components/bookmark_folders_menu/bookmark_folders_menu_content.js +++ b/src/components/bookmark_folders_menu/bookmark_folders_menu_content.js @@ -1,6 +1,7 @@ -import { mapState } from 'vuex' +import { mapState } from 'pinia' 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: [ @@ -10,7 +11,7 @@ export const BookmarkFoldersMenuContent = { NavigationEntry }, computed: { - ...mapState({ + ...mapState(useBookmarkFoldersStore, { folders: getBookmarkFolderEntries }) } diff --git a/src/components/navigation/filter.js b/src/components/navigation/filter.js index 0974bc1e3..12ab9266e 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 = state => state.bookmarkFolders.allFolders.map(folder => ({ +export const getBookmarkFolderEntries = store => store.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 e657e94a0..f9cdef71b 100644 --- a/src/components/navigation/navigation_pins.js +++ b/src/components/navigation/navigation_pins.js @@ -19,6 +19,7 @@ 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, @@ -52,8 +53,10 @@ 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 f9eb1bdcd..87c88d8f7 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -376,6 +376,14 @@ 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 @@ -784,7 +792,7 @@ const PostStatusForm = { this.$emit('draft-done') } }) - } else if (this.newStatus.id) { + } else if (this.hasEmptyDraft) { // 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 260a89cb7..303413b55 100644 --- a/src/components/post_status_form/post_status_form.vue +++ b/src/components/post_status_form/post_status_form.vue @@ -322,6 +322,7 @@ trigger="click" placement="bottom" :offset="{ y: 5 }" + :trigger-attrs="{ 'aria-label': $t('post_status.more_post_actions') }" > - +