Merge branch 'tusooa/vite' into 'develop'

Use vite to replace webpack

Closes #1205

See merge request pleroma/pleroma-fe!2052
This commit is contained in:
HJ 2025-03-04 17:13:36 +00:00
commit 157e227f6f
83 changed files with 1762 additions and 2908 deletions

2
.gitattributes vendored
View file

@ -1 +1 @@
/build/webpack.prod.conf.js export-subst
/build/commit_hash.js export-subst

3
.gitignore vendored
View file

@ -7,5 +7,6 @@ test/e2e/reports
selenium-debug.log
.idea/
config/local.json
static/emoji.json
src/assets/emoji.json
logs/
__screenshots__/

View file

@ -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

View file

@ -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)
}
})

18
build/commit_hash.js Normal file
View file

@ -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'
}
}
})

40
build/copy_plugin.js Normal file
View file

@ -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

View file

@ -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()
}
})

View file

@ -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)
})

View file

@ -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
}, {})
}

163
build/sw_plugin.js Normal file
View file

@ -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
}
}
}

View file

@ -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.')

View file

@ -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)
}

View file

@ -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,
},
})
]
}

View file

@ -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
})
]
})

View file

@ -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

View file

@ -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

View file

@ -0,0 +1 @@
BREAKING: drop support for browsers that do not support `<script type="module">`

View file

@ -0,0 +1 @@
BREAKING: css source map does not work in production (see https://github.com/vitejs/vite/issues/2830 )

View file

@ -0,0 +1 @@
BREAKING: static/emoji.json is replaced with a properly hashed path under static/js in the production build, meaning server admins cannot provide their own set of unicode emojis by overriding this file (custom (image-based) emojis not affected)

View file

@ -1,6 +0,0 @@
var merge = require('webpack-merge')
var prodEnv = require('./prod.env')
module.exports = merge(prodEnv, {
NODE_ENV: '"development"'
})

View file

@ -1,73 +0,0 @@
// see http://vuejs-templates.github.io/webpack for documentation.
const path = require('path')
let settings = {}
try {
settings = require('./local.json')
if (settings.target && settings.target.endsWith('/')) {
// replacing trailing slash since it can conflict with some apis
// and that's how actual BE reports its url
settings.target = settings.target.replace(/\/$/, '')
}
console.info('Using local dev server settings (/config/local.json):')
console.info(JSON.stringify(settings, null, 2))
} catch (e) {
console.info('Local dev server settings not found (/config/local.json)')
}
const target = settings.target || 'http://localhost:4000/'
module.exports = {
build: {
env: require('./prod.env'),
index: path.resolve(__dirname, '../dist/index.html'),
assetsRoot: path.resolve(__dirname, '../dist'),
assetsSubDirectory: 'static',
assetsPublicPath: '/',
productionSourceMap: true,
// Gzip off by default as many popular static hosts such as
// Surge or Netlify already gzip all static assets for you.
// Before setting to `true`, make sure to:
// npm install --save-dev compression-webpack-plugin
productionGzip: false,
productionGzipExtensions: ['js', 'css']
},
dev: {
env: require('./dev.env'),
port: 8080,
settings,
assetsSubDirectory: 'static',
assetsPublicPath: '/',
proxyTable: {
'/api': {
target,
changeOrigin: true,
cookieDomainRewrite: 'localhost'
},
'/nodeinfo': {
target,
changeOrigin: true,
cookieDomainRewrite: 'localhost'
},
'/socket': {
target,
changeOrigin: true,
cookieDomainRewrite: 'localhost',
ws: true,
headers: {
'Origin': target
}
},
'/oauth/revoke': {
target,
changeOrigin: true,
cookieDomainRewrite: 'localhost'
}
},
// CSS Sourcemaps off by default because relative paths are "buggy"
// with this option, according to the CSS-Loader README
// (https://github.com/webpack/css-loader#sourcemaps)
// In our experience, they generally work as expected,
// just be aware of this issue when enabling this option.
cssSourceMap: false
}
}

View file

@ -1,3 +0,0 @@
module.exports = {
NODE_ENV: '"production"'
}

View file

@ -1,6 +0,0 @@
var merge = require('webpack-merge')
var devEnv = require('./dev.env')
module.exports = merge(devEnv, {
NODE_ENV: '"testing"'
})

View file

@ -19,7 +19,7 @@ export default [
},
globals: {
...globals.browser,
...globals.mocha,
...globals.vitest,
...globals.chai,
...globals.commonjs,
...globals.serviceworker

View file

@ -167,6 +167,7 @@
<div id="app" class="hidden"></div>
<div id="modal"></div>
<!-- built files will be auto injected -->
<div id="popovers" />
<div id="popovers"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View file

@ -5,10 +5,11 @@
"author": "Pleroma contributors <https://git.pleroma.social/pleroma/pleroma-fe/-/blob/develop/CONTRIBUTORS.md>",
"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"
},

View file

@ -1,5 +1,7 @@
module.exports = {
import autoprefixer from 'autoprefixer'
export default {
plugins: [
require('autoprefixer')
autoprefixer
]
}

0
public/static/.gitkeep Normal file
View file

View file

Before

Width:  |  Height:  |  Size: 628 KiB

After

Width:  |  Height:  |  Size: 628 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 396 KiB

After

Width:  |  Height:  |  Size: 396 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 521 KiB

After

Width:  |  Height:  |  Size: 521 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Before After
Before After

View file

@ -1 +0,0 @@
../../static/pleromatan_apology.png

View file

@ -1 +0,0 @@
../../static/pleromatan_apology_fox.png

View file

@ -3,7 +3,8 @@ import { getTagName, processTextForEmoji, getAttrs } from 'src/services/html_con
import { convertHtmlToTree } from 'src/services/html_converter/html_tree_converter.service.js'
import { convertHtmlToLines } from 'src/services/html_converter/html_line_converter.service.js'
import StillImage from 'src/components/still-image/still-image.vue'
import MentionsLine, { MENTIONS_LIMIT } from 'src/components/mentions_line/mentions_line.vue'
import MentionsLine from 'src/components/mentions_line/mentions_line.vue'
import { MENTIONS_LIMIT } from 'src/components/mentions_line/mentions_line.js'
import HashtagLink from 'src/components/hashtag_link/hashtag_link.vue'
import './rich_content.scss'

View file

@ -2,7 +2,8 @@ import BooleanSetting from '../helpers/boolean_setting.vue'
import ChoiceSetting from '../helpers/choice_setting.vue'
import IntegerSetting from '../helpers/integer_setting.vue'
import FloatSetting from '../helpers/float_setting.vue'
import UnitSetting, { defaultHorizontalUnits } from '../helpers/unit_setting.vue'
import UnitSetting from '../helpers/unit_setting.vue'
import { defaultHorizontalUnits } from '../helpers/unit_setting.js'
import PaletteEditor from 'src/components/palette_editor/palette_editor.vue'
import Preview from './theme_tab/theme_preview.vue'
import FontControl from 'src/components/font_control/font_control.vue'

View file

@ -221,12 +221,15 @@ export default {
// ## Components stuff
// Getting existing components
const componentsContext = require.context('src', true, /\.style.js(on)?$/)
const componentKeysAll = componentsContext.keys()
const componentsContext = import.meta.glob(
['/src/**/*.style.js', '/src/**/*.style.json'],
{ eager: true }
)
const componentKeysAll = Object.keys(componentsContext)
const componentsMap = new Map(
componentKeysAll
.map(
key => [key, componentsContext(key).default]
key => [key, componentsContext[key].default]
).filter(([, component]) => !component.virtual && !component.notEditable)
)
exports.componentsMap = componentsMap

View file

@ -22,4 +22,4 @@
</template>
<script src="./status_bookmark_folder_menu.js"></script>
<stlye src="./status_bookmark_folder_menu.scss" />
<style src="./status_bookmark_folder_menu.scss" />

View file

@ -1,8 +1,8 @@
import Popover from '../popover/popover.vue'
import NavigationEntry from 'src/components/navigation/navigation_entry.vue'
import { mapState } from 'vuex'
import { ListsMenuContent } from '../lists_menu/lists_menu_content.vue'
import { BookmarkFoldersMenuContent } from '../bookmark_folders_menu/bookmark_folders_menu_content.vue'
import ListsMenuContent from '../lists_menu/lists_menu_content.vue'
import BookmarkFoldersMenuContent from '../bookmark_folders_menu/bookmark_folders_menu_content.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import { TIMELINES } from 'src/components/navigation/navigation.js'
import { filterNavigation } from 'src/components/navigation/filter.js'

View file

@ -1,7 +1,5 @@
import Modal from 'src/components/modal/modal.vue'
import { library } from '@fortawesome/fontawesome-svg-core'
import pleromaTan from 'src/assets/pleromatan_apology.png'
import pleromaTanFox from 'src/assets/pleromatan_apology_fox.png'
import pleromaTanMask from 'src/assets/pleromatan_apology_mask.png'
import pleromaTanFoxMask from 'src/assets/pleromatan_apology_fox_mask.png'
@ -14,6 +12,9 @@ library.add(
export const CURRENT_UPDATE_COUNTER = 1
const pleromaTan = '/static/pleromatan_apology.png'
const pleromaTanFox = '/static/pleromatan_apology_fox.png'
const UpdateNotification = {
data () {
return {

View file

@ -46,7 +46,7 @@ const ensureFinalFallback = codes => {
return codeList.includes('en') ? codeList : codeList.concat(['en'])
}
module.exports = {
export {
languages,
langCodeToJsonName,
langCodeToCldrName,

View file

@ -9,23 +9,23 @@
import { isEqual } from 'lodash'
import { languages, langCodeToJsonName } from './languages.js'
import enMessages from './en.json'
const ULTIMATE_FALLBACK_LOCALE = 'en'
const hasLanguageFile = (code) => languages.includes(code)
const languageFileMap = import.meta.glob('./*.json')
const loadLanguageFile = (code) => {
return import(
/* webpackInclude: /\.json$/ */
/* webpackChunkName: "i18n/[request]" */
`./${langCodeToJsonName(code)}.json`
)
const jsonName = langCodeToJsonName(code)
return languageFileMap[`./${jsonName}.json`]()
}
const messages = {
languages,
default: {
en: require('./en.json').default
en: enMessages
},
setLanguage: async (i18n, language) => {
const languages = (Array.isArray(language) ? language : [language]).filter(k => k)

View file

@ -179,9 +179,9 @@ const defaultState = {
}
const loadAnnotations = (lang) => {
const code = langCodeToCldrName(lang)
return import(
/* webpackChunkName: "emoji-annotations/[request]" */
`@kazvmoe-infra/unicode-emoji-json/annotations/${langCodeToCldrName(lang)}.json`
`../../node_modules/@kazvmoe-infra/unicode-emoji-json/annotations/${code}.json`
)
.then(k => k.default)
}
@ -304,7 +304,7 @@ const instance = {
},
async getStaticEmoji ({ commit }) {
try {
const values = (await import(/* webpackChunkName: 'emoji' */ '../../static/emoji.json')).default
const values = (await import('/src/assets/emoji.json')).default
const emoji = Object.keys(values).reduce((res, groupId) => {
res[groupId] = values[groupId].map(e => ({

View file

@ -119,6 +119,8 @@ const getLatestScrobble = (state, user) => {
state.scrobblesNextFetch[user.id] = Date.now() + 60 * 1000
}
}).catch(e => {
console.warn('cannot fetch scrobbles', e)
})
}

View file

@ -1,5 +1,5 @@
import escape from 'escape-html'
import parseLinkHeader from 'parse-link-header'
import { parseLinkHeader } from '@web3-storage/parse-link-header'
import { isStatusNotification } from '../notification_utils/notification_utils.js'
import punycode from 'punycode.js'
@ -484,8 +484,8 @@ export const parseLinkHeaderPagination = (linkHeader, opts = {}) => {
const flakeId = opts.flakeId
const parsedLinkHeader = parseLinkHeader(linkHeader)
if (!parsedLinkHeader) return
const maxId = parsedLinkHeader.next.max_id
const minId = parsedLinkHeader.prev.min_id
const maxId = parsedLinkHeader.next?.max_id
const minId = parsedLinkHeader.prev?.min_id
return {
maxId: flakeId ? maxId : parseInt(maxId, 10),

View file

@ -1,11 +1,12 @@
const createRuffleService = () => {
let ruffleInstance = null
const getRuffle = () => new Promise((resolve, reject) => {
const getRuffle = async () => new Promise((resolve, reject) => {
if (ruffleInstance) {
resolve(ruffleInstance)
return
}
// Ruffle needs these to be set before it's loaded
// https://github.com/ruffle-rs/ruffle/issues/3952
window.RufflePlayer = {}

View file

@ -1,5 +1,4 @@
import runtime from 'serviceworker-webpack5-plugin/lib/runtime'
/* global process */
function urlBase64ToUint8Array (base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4)
const base64 = (base64String + padding)
@ -19,7 +18,8 @@ function isPushSupported () {
}
function getOrCreateServiceWorker () {
return runtime.register()
const swType = process.env.HAS_MODULE_SERVICE_WORKER ? 'module' : 'classic'
return navigator.serviceWorker.register('/sw-pleroma.js', { type: swType })
.catch((err) => console.error('Unable to get or create a service worker.', err))
}
@ -98,14 +98,14 @@ export async function initServiceWorker (store) {
export async function showDesktopNotification (content) {
if (!isSWSupported) return
const { active: sw } = await window.navigator.serviceWorker.getRegistration()
const { active: sw } = (await window.navigator.serviceWorker.getRegistration()) || {}
if (!sw) return console.error('No serviceworker found!')
sw.postMessage({ type: 'desktopNotification', content })
}
export async function closeDesktopNotification ({ id }) {
if (!isSWSupported) return
const { active: sw } = await window.navigator.serviceWorker.getRegistration()
const { active: sw } = (await window.navigator.serviceWorker.getRegistration()) || {}
if (!sw) return console.error('No serviceworker found!')
if (id >= 0) {
sw.postMessage({ type: 'desktopNotificationClose', content: { id } })
@ -116,7 +116,7 @@ export async function closeDesktopNotification ({ id }) {
export async function updateFocus () {
if (!isSWSupported) return
const { active: sw } = await window.navigator.serviceWorker.getRegistration()
const { active: sw } = (await window.navigator.serviceWorker.getRegistration()) || {}
if (!sw) return console.error('No serviceworker found!')
sw.postMessage({ type: 'updateFocus' })
}

View file

@ -146,9 +146,12 @@ const getTextColorAlpha = (directives, intendedTextColor, dynamicVars, staticVar
}
// Loading all style.js[on] files dynamically
const componentsContext = require.context('src', true, /\.style.js(on)?$/)
componentsContext.keys().forEach(key => {
const component = componentsContext(key).default
const componentsContext = import.meta.glob(
['/src/**/*.style.js', '/src/**/*.style.json'],
{ eager: true }
)
Object.keys(componentsContext).forEach(key => {
const component = componentsContext[key].default
if (components[component.name] != null) {
console.warn(`Component in file ${key} is trying to override existing component ${component.name}! You have collisions/duplicates!`)
}

View file

@ -4,7 +4,10 @@ import { storage } from 'src/lib/storage.js'
import { parseNotification } from './services/entity_normalizer/entity_normalizer.service.js'
import { prepareNotificationObject } from './services/notification_utils/notification_utils.js'
import { createI18n } from 'vue-i18n'
import messages from './i18n/service_worker_messages.js'
// Collects all messages for service workers
// Needed because service workers cannot use dynamic imports
// See /build/sw_plugin.js for more information
import messages from 'virtual:pleroma-fe/service_worker_messages'
const i18n = createI18n({
// By default, use the browser locale, we will update it if neccessary
@ -139,3 +142,5 @@ self.addEventListener('notificationclick', (event) => {
if (clients.openWindow) return clients.openWindow('/')
}))
})
console.log('sw here')

View file

@ -109,23 +109,18 @@ export const waitForEvent = (wrapper, event, {
timesEmitted = 1
} = {}) => {
const tick = 10
const totalTries = timeout / tick
return new Promise((resolve, reject) => {
let currentTries = 0
const wait = () => {
return vi.waitFor(
() => {
const e = wrapper.emitted(event)
if (e && e.length >= timesEmitted) {
resolve()
return
}
if (currentTries >= totalTries) {
reject(new Error('Event did not fire'))
return
}
++currentTries
setTimeout(wait, tick)
throw new Error('event is not emitted')
},
{
timeout,
interval: tick
}
wait()
})
)
}

View file

@ -1,9 +0,0 @@
{
"env": {
"mocha": true
},
"globals": {
"expect": true,
"sinon": true
}
}

View file

@ -1,9 +0,0 @@
// require all test files (files that ends with .spec.js)
const testsContext = require.context('./specs', true, /\.spec$/)
testsContext.keys().forEach(testsContext)
// require all src files except main.js for coverage.
// you can also change this to match only the subset of files that
// you want coverage for.
// const srcContext = require.context('../../src', true, /^\.\/(?!main(\.js)?$)/)
// srcContext.keys().forEach(srcContext)

View file

@ -1,73 +0,0 @@
// This is a karma config file. For more details see
// http://karma-runner.github.io/0.13/config/configuration-file.html
// we are also using it with karma-webpack
// https://github.com/webpack/karma-webpack
// var path = require('path')
const merge = require('webpack-merge')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const baseConfig = require('../../build/webpack.base.conf')
const utils = require('../../build/utils')
const webpack = require('webpack')
// var projectRoot = path.resolve(__dirname, '../../')
const webpackConfig = merge(baseConfig, {
// use inline sourcemap for karma-sourcemap-loader
module: {
rules: utils.styleLoaders()
},
devtool: 'inline-source-map',
plugins: [
new webpack.DefinePlugin({
'process.env': require('../../config/test.env')
}),
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'index.html',
inject: true
})
]
})
// no need for app entry during tests
delete webpackConfig.entry
module.exports = function (config) {
config.set({
// to run in additional browsers:
// 1. install corresponding karma launcher
// http://karma-runner.github.io/0.13/config/browsers.html
// 2. add it to the `browsers` array below.
browsers: ['FirefoxHeadless'],
frameworks: ['mocha', 'sinon-chai'],
reporters: ['mocha'],
customLaunchers: {
FirefoxHeadless: {
base: 'Firefox',
flags: [
'-headless'
]
}
},
files: [
'./index.js'
],
preprocessors: {
'./index.js': ['webpack', 'sourcemap']
},
webpack: webpackConfig,
webpackMiddleware: {
noInfo: true
},
mochaReporter: {
showDiff: true
},
coverageReporter: {
dir: './coverage',
reporters: [
{ type: 'lcov', subdir: '.' },
{ type: 'text-summary' }
]
}
})
}

View file

@ -1,6 +1,5 @@
import { mount, flushPromises } from '@vue/test-utils'
import { nextTick } from 'vue'
import sinon from 'sinon'
import PostStatusForm from 'src/components/post_status_form/post_status_form.vue'
import { mountOpts, waitForEvent, $t } from '../../../fixtures/setup_test'
@ -25,7 +24,7 @@ const saveManually = async (wrapper) => {
const waitSaveTime = 4000
afterEach(() => {
sinon.restore()
vi.useRealTimers()
})
describe('Draft saving', () => {
@ -43,12 +42,10 @@ describe('Draft saving', () => {
await saveManually(wrapper)
expect(wrapper.vm.$store.getters.draftCount).to.equal(1)
expect(wrapper.vm.$store.getters.draftsArray[0].status).to.equal('mew mew')
console.log('done')
})
it('should auto-save if it is enabled', async function () {
this.timeout(5000)
const clock = sinon.useFakeTimers(Date.now())
vi.useFakeTimers()
const wrapper = mount(PostStatusForm, mountOpts())
await wrapper.vm.$store.dispatch('setOption', {
name: 'autoSaveDraft',
@ -59,10 +56,9 @@ describe('Draft saving', () => {
await textarea.setValue('mew mew')
expect(wrapper.vm.$store.getters.draftCount).to.equal(0)
await clock.tickAsync(waitSaveTime)
await vi.advanceTimersByTimeAsync(waitSaveTime)
expect(wrapper.vm.$store.getters.draftCount).to.equal(1)
expect(wrapper.vm.$store.getters.draftsArray[0].status).to.equal('mew mew')
clock.restore()
})
it('should auto-save when close if auto-save is on', async () => {
@ -81,7 +77,6 @@ describe('Draft saving', () => {
wrapper.vm.requestClose()
expect(wrapper.vm.$store.getters.draftCount).to.equal(1)
await waitForEvent(wrapper, 'can-close')
console.log('done')
})
it('should save when close if auto-save is off, and unsavedPostAction is save', async () => {
@ -104,7 +99,6 @@ describe('Draft saving', () => {
wrapper.vm.requestClose()
expect(wrapper.vm.$store.getters.draftCount).to.equal(1)
await waitForEvent(wrapper, 'can-close')
console.log('done')
})
it('should discard when close if auto-save is off, and unsavedPostAction is discard', async () => {
@ -127,40 +121,33 @@ describe('Draft saving', () => {
wrapper.vm.requestClose()
await waitForEvent(wrapper, 'can-close')
expect(wrapper.vm.$store.getters.draftCount).to.equal(0)
console.log('done')
})
it('should confirm when close if auto-save is off, and unsavedPostAction is confirm', async () => {
try {
const wrapper = mount(PostStatusForm, mountOpts({
props: {
closeable: true
}
}))
await wrapper.vm.$store.dispatch('setOption', {
name: 'autoSaveDraft',
value: false
})
await wrapper.vm.$store.dispatch('setOption', {
name: 'unsavedPostAction',
value: 'confirm'
})
expect(wrapper.vm.$store.getters.draftCount).to.equal(0)
const textarea = wrapper.get('textarea')
await textarea.setValue('mew mew')
wrapper.vm.requestClose()
await nextTick()
const saveButton = wrapper.findByText('button', $t('post_status.close_confirm_save_button'))
expect(saveButton).to.be.ok
await saveButton.trigger('click')
console.log('clicked')
expect(wrapper.vm.$store.getters.draftCount).to.equal(1)
await flushPromises()
await waitForEvent(wrapper, 'can-close')
console.log('done')
} catch (e) {
console.log('error:', e)
throw e
}
const wrapper = mount(PostStatusForm, mountOpts({
props: {
closeable: true
}
}))
await wrapper.vm.$store.dispatch('setOption', {
name: 'autoSaveDraft',
value: false
})
await wrapper.vm.$store.dispatch('setOption', {
name: 'unsavedPostAction',
value: 'confirm'
})
expect(wrapper.vm.$store.getters.draftCount).to.equal(0)
const textarea = wrapper.get('textarea')
await textarea.setValue('mew mew')
wrapper.vm.requestClose()
await nextTick()
const saveButton = wrapper.findByText('button', $t('post_status.close_confirm_save_button'))
expect(saveButton).to.be.ok
await saveButton.trigger('click')
console.log('clicked')
expect(wrapper.vm.$store.getters.draftCount).to.equal(1)
await flushPromises()
await waitForEvent(wrapper, 'can-close')
})
})

View file

@ -18,7 +18,13 @@ const generateInput = (value, padEmoji = true) => {
$t: (msg) => msg
},
stubs: {
FAIcon: true
FAIcon: true,
Popover: {
template: `<div><slot trigger /></div>`,
methods: {
updateStyles () {}
}
}
},
directives: {
'click-outside': vClickOutside
@ -104,43 +110,37 @@ describe('EmojiInput', () => {
expect(inputEvents[inputEvents.length - 1][0]).to.eql('Eat some spam!:spam:')
})
it('correctly sets caret after insertion at beginning', (done) => {
it('correctly sets caret after insertion at beginning', async () => {
const initialString = '1234'
const wrapper = generateInput(initialString)
const input = wrapper.find('input')
input.setValue(initialString)
wrapper.setData({ caret: 0 })
wrapper.vm.insert({ insertion: '1234', keepOpen: false })
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.caret).to.eql(5)
done()
})
await wrapper.vm.$nextTick()
expect(wrapper.vm.caret).to.eql(5)
})
it('correctly sets caret after insertion at end', (done) => {
it('correctly sets caret after insertion at end', async () => {
const initialString = '1234'
const wrapper = generateInput(initialString)
const input = wrapper.find('input')
input.setValue(initialString)
wrapper.setData({ caret: initialString.length })
wrapper.vm.insert({ insertion: '1234', keepOpen: false })
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.caret).to.eql(10)
done()
})
await wrapper.vm.$nextTick()
expect(wrapper.vm.caret).to.eql(10)
})
it('correctly sets caret after insertion if padEmoji setting is set to false', (done) => {
it('correctly sets caret after insertion if padEmoji setting is set to false', async () => {
const initialString = '1234'
const wrapper = generateInput(initialString, false)
const input = wrapper.find('input')
input.setValue(initialString)
wrapper.setData({ caret: initialString.length })
wrapper.vm.insert({ insertion: '1234', keepOpen: false })
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.caret).to.eql(8)
done()
})
await wrapper.vm.$nextTick()
expect(wrapper.vm.caret).to.eql(8)
})
})
})

View file

@ -1,5 +1,8 @@
import * as DateUtils from 'src/services/date_utils/date_utils.js'
beforeEach(() => { vi.useFakeTimers() })
afterEach(() => { vi.useRealTimers() })
describe('DateUtils', () => {
describe('relativeTime', () => {
it('returns now with low enough amount of seconds', () => {

View file

@ -1,10 +1,13 @@
import { deserialize } from 'src/services/theme_data/iss_deserializer.js'
import { serialize } from 'src/services/theme_data/iss_serializer.js'
const componentsContext = require.context('src', true, /\.style.js(on)?$/)
const componentsContext = import.meta.glob(
['/src/**/*.style.js', '/src/**/*.style.json'],
{ eager: true }
)
describe('ISS (de)serialization', () => {
componentsContext.keys().forEach(key => {
const component = componentsContext(key).default
Object.keys(componentsContext).forEach(key => {
const component = componentsContext[key].default
it(`(De)serialization of component ${component.name} works`, () => {
const normalized = component.defaultRules.map(x => ({ component: component.name, ...x }))

View file

@ -16,10 +16,10 @@ const checkColors = (output) => {
}
describe('Theme Data utility functions', () => {
const context = require.context('static/themes/', false, /\.json$/)
context.keys().forEach((key) => {
const context = import.meta.glob('/public/static/themes/*.json', { import: 'default', eager: true })
Object.keys(context).forEach((key) => {
it(`Should render all colors for ${key} properly`, () => {
const { theme, source } = context(key)
const { theme, source } = context[key]
const data = source || theme
const colors = getColors(data.colors, data.opacity, 1)
checkColors(colors)

View file

@ -62,8 +62,6 @@ describe('Theme Data 3', () => {
})
describe('init', function () {
this.timeout(5000)
it('Test initialization without anything', () => {
const out = init({ inputRuleset: [], ultimateBackgroundColor: '#DEADAF' })

202
vite.config.js Normal file
View file

@ -0,0 +1,202 @@
import { fileURLToPath } from 'node:url'
import { dirname, resolve } from 'node:path'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import stylelint from 'vite-plugin-stylelint'
import eslint from 'vite-plugin-eslint2'
import { devSwPlugin, buildSwPlugin, swMessagesPlugin } from './build/sw_plugin.js'
import copyPlugin from './build/copy_plugin.js'
import { getCommitHash } from './build/commit_hash.js'
const localConfigPath = '<projectRoot>/config/local.json'
const getLocalDevSettings = async () => {
try {
const settings = (await import('./config/local.json')).default
if (settings.target && settings.target.endsWith('/')) {
// replacing trailing slash since it can conflict with some apis
// and that's how actual BE reports its url
settings.target = settings.target.replace(/\/$/, '')
}
console.info(`Using local dev server settings (${localConfigPath}):`)
console.info(JSON.stringify(settings, null, 2))
return settings
} catch (e) {
console.info(`Local dev server settings not found (${localConfigPath}), using default`, e)
return {}
}
}
const projectRoot = dirname(fileURLToPath(import.meta.url))
const getTransformSWSettings = (settings) => {
if ('transformSW' in settings) {
return settings.transformSW
} else {
console.info(
'`transformSW` is not present in your local settings.\n' +
'This option controls whether the service worker should be bundled and transformed into iife (immediately-invoked function expression) during development.\n' +
'If set to false, the service worker will be served as-is, as an ES Module.\n' +
'Some browsers (e.g. Firefox) does not support ESM service workers.\n' +
'To avoid surprises, it is defaulted to true, but this can be slow.\n' +
'If you are using a browser that supports ESM service workers, you can set this option to false.\n' +
`No matter your choice, you can set the transformSW option in ${localConfigPath} in to disable this message.`
)
return true
}
}
export default defineConfig(async ({ mode, command }) => {
const settings = await getLocalDevSettings()
const target = settings.target || 'http://localhost:4000/'
const transformSW = getTransformSWSettings(settings)
const proxy = {
'/api': {
target,
changeOrigin: true,
cookieDomainRewrite: 'localhost',
ws: true
},
'/nodeinfo': {
target,
changeOrigin: true,
cookieDomainRewrite: 'localhost'
},
'/socket': {
target,
changeOrigin: true,
cookieDomainRewrite: 'localhost',
ws: true,
headers: {
'Origin': target
}
},
'/oauth': {
target,
changeOrigin: true,
cookieDomainRewrite: 'localhost'
}
}
const swSrc = 'src/sw.js'
const swDest = 'sw-pleroma.js'
const alias = {
src: '/src',
components: '/src/components',
...(mode === 'test' ? { vue: 'vue/dist/vue.esm-bundler.js' } : {})
}
return {
plugins: [
vue({
template: {
compilerOptions: {
isCustomElement(tag) {
if (tag === 'pinch-zoom') {
return true
}
return false
}
}
}
}),
vueJsx(),
devSwPlugin({ swSrc, swDest, transformSW, alias }),
buildSwPlugin({ swSrc, swDest }),
swMessagesPlugin(),
copyPlugin({
inUrl: '/static/ruffle',
inFs: resolve(projectRoot, 'node_modules/@ruffle-rs/ruffle')
}),
eslint({
lintInWorker: true,
lintOnStart: true,
cacheLocation: resolve(projectRoot, 'node_modules/.cache/eslintcache')
}),
stylelint({
lintInWorker: true,
lintOnStart: true,
cacheLocation: resolve(projectRoot, 'node_modules/.cache/stylelintcache')
})
],
optimizeDeps: {
// For unknown reasons, during vitest, vite will re-optimize the following
// deps, causing the test to reload, so add them here so that it will not
// reload during tests
include: [
'custom-event-polyfill',
'vue-i18n',
'@ungap/event-target',
'lodash.merge',
'body-scroll-lock',
'@kazvmoe-infra/pinch-zoom-element'
]
},
css: {
devSourcemap: true
},
resolve: {
alias
},
define: {
'process.env': JSON.stringify({
NODE_ENV: mode === 'test' ? 'testing' : command === 'serve' ? 'development' : 'production',
HAS_MODULE_SERVICE_WORKER: command === 'serve' && !transformSW
}),
'COMMIT_HASH': JSON.stringify(command === 'serve' ? 'DEV' : getCommitHash()),
'DEV_OVERRIDES': JSON.stringify(command === 'serve' ? settings : undefined),
'__VUE_OPTIONS_API__': true,
'__VUE_PROD_DEVTOOLS__': false,
'__VUE_PROD_HYDRATION_MISMATCH_DETAILS__': false
},
build: {
sourcemap: true,
rollupOptions: {
input: {
main: 'index.html'
},
output: {
inlineDynamicImports: false,
entryFileNames (chunkInfo) {
const id = chunkInfo.facadeModuleId
if (id.endsWith(swSrc)) {
return swDest
} else {
return 'static/js/[name].[hash].js'
}
},
chunkFileNames () {
return 'static/js/[name].[hash].js'
},
assetFileNames (assetInfo) {
const name = assetInfo.names?.[0] || ''
if (/\.(png|jpe?g|gif|svg)(\?.*)?$/.test(name)) {
return 'static/img/[name].[hash][extname]'
} else if (/\.css$/.test(name)) {
return 'static/css/[name].[hash][extname]'
} else {
return 'static/misc/[name].[hash][extname]'
}
}
}
},
},
server: {
...(mode === 'test' ? {} : { proxy }),
port: Number(process.env.PORT) || 8080
},
preview: {
proxy
},
test: {
globals: true,
browser: {
enabled: true,
provider: 'playwright',
instances: [
{ browser: 'firefox' }
]
}
}
}
})

3224
yarn.lock

File diff suppressed because it is too large Load diff