Convert from Webpack to Vite (#34450)

Co-authored-by: Renaud Chaput <renchap@gmail.com>
This commit is contained in:
Echo 2025-05-16 15:26:12 +02:00 committed by GitHub
parent a5a2c6dc7e
commit c4f47adb49
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
100 changed files with 2031 additions and 7424 deletions

View file

@ -46,14 +46,13 @@ require_relative '../lib/chewy/settings_extensions'
require_relative '../lib/chewy/index_extensions'
require_relative '../lib/chewy/strategy/mastodon'
require_relative '../lib/chewy/strategy/bypass_with_warning'
require_relative '../lib/webpacker/manifest_extensions'
require_relative '../lib/webpacker/helper_extensions'
require_relative '../lib/rails/engine_extensions'
require_relative '../lib/action_dispatch/remote_ip_extensions'
require_relative '../lib/stoplight/redis_data_store_extensions'
require_relative '../lib/active_record/database_tasks_extensions'
require_relative '../lib/active_record/batches'
require_relative '../lib/simple_navigation/item_extensions'
require_relative '../lib/vite_ruby/sri_extensions'
Bundler.require(:pam_authentication) if ENV['PAM_ENABLED'] == 'true'

View file

@ -18,7 +18,6 @@ Rails.application.config.content_security_policy do |p|
p.frame_ancestors :none
p.font_src :self, assets_host
p.img_src :self, :data, :blob, *media_hosts
p.style_src :self, assets_host
p.media_src :self, :data, *media_hosts
p.manifest_src :self, assets_host
@ -32,16 +31,18 @@ Rails.application.config.content_security_policy do |p|
p.worker_src :self, :blob, assets_host
if Rails.env.development?
webpacker_public_host = ENV.fetch('WEBPACKER_DEV_SERVER_PUBLIC', Webpacker.config.dev_server[:public])
front_end_build_urls = %w(ws http).map { |protocol| "#{protocol}#{Webpacker.dev_server.https? ? 's' : ''}://#{webpacker_public_host}" }
vite_public_host = ENV.fetch('VITE_DEV_SERVER_PUBLIC', "localhost:#{ViteRuby.config.port}")
front_end_build_urls = %w(ws http).map { |protocol| "#{protocol}#{ViteRuby.config.https ? 's' : ''}://#{vite_public_host}" }
p.connect_src :self, :data, :blob, *media_hosts, Rails.configuration.x.streaming_api_base_url, *front_end_build_urls
p.script_src :self, :unsafe_inline, :unsafe_eval, assets_host
p.frame_src :self, :https, :http
p.style_src :self, assets_host, :unsafe_inline
else
p.connect_src :self, :data, :blob, *media_hosts, Rails.configuration.x.streaming_api_base_url
p.script_src :self, assets_host, "'wasm-unsafe-eval'"
p.frame_src :self, :https
p.style_src :self, assets_host
end
end

View file

@ -1,17 +1,23 @@
{
"all": {
"sourceCodeDir": "app/javascript",
"additionalEntrypoints": ["~/{icons,images}/**/*", "~/styles/*.scss"],
"additionalEntrypoints": [
"~/{fonts,icons,images}/**/*",
"~/styles/entrypoints/*.scss"
],
"watchAdditionalPaths": []
},
"production": {
"publicOutputDir": "packs"
},
"development": {
"autoBuild": true,
"publicOutputDir": "vite-dev",
"publicOutputDir": "packs-dev",
"port": 3036
},
"test": {
"autoBuild": true,
"publicOutputDir": "vite-test",
"publicOutputDir": "packs-test",
"port": 3037
}
}

View file

@ -0,0 +1,27 @@
import type { Plugin } from 'vite';
export function MastodonEmojiCompressed(): Plugin {
const virtualModuleId = 'virtual:mastodon-emoji-compressed';
const resolvedVirtualModuleId = '\0' + virtualModuleId;
return {
name: 'mastodon-emoji-compressed',
resolveId(id) {
if (id === virtualModuleId) {
return resolvedVirtualModuleId;
}
return undefined;
},
async load(id) {
if (id === resolvedVirtualModuleId) {
const { default: emojiCompressed } = await import(
'../../app/javascript/mastodon/features/emoji/emoji_compressed.mjs'
);
return `export default ${JSON.stringify(emojiCompressed)};`;
}
return undefined;
},
};
}

View file

@ -0,0 +1,102 @@
/* This plugin provides the `virtual:mastodon-sw-locales` import
which exports translations for every locales, but only with the
keys defined below.
This is used by the notifications code in the service-worker, to
provide localised texts without having to load all the translations
*/
import fs from 'node:fs';
import path from 'node:path';
import { defineMessages } from 'react-intl';
import type { Plugin, ResolvedConfig } from 'vite';
const translations = defineMessages({
mentioned_you: {
id: 'notification.mentioned_you',
defaultMessage: '{name} mentioned you',
},
});
const CUSTOM_TRANSLATIONS = {
'notification.mention': translations.mentioned_you.id,
};
const KEEP_KEYS = [
'notification.favourite',
'notification.follow',
'notification.follow_request',
'notification.mention',
'notification.reblog',
'notification.poll',
'notification.status',
'notification.update',
'notification.admin.sign_up',
'status.show_more',
'status.reblog',
'status.favourite',
'notifications.group',
];
export function MastodonServiceWorkerLocales(): Plugin {
const virtualModuleId = 'virtual:mastodon-sw-locales';
const resolvedVirtualModuleId = '\0' + virtualModuleId;
let config: ResolvedConfig;
return {
name: 'mastodon-sw-locales',
configResolved(resolvedConfig) {
config = resolvedConfig;
},
resolveId(id) {
if (id === virtualModuleId) {
return resolvedVirtualModuleId;
}
return undefined;
},
load(id) {
if (id === resolvedVirtualModuleId) {
const filteredLocales: Record<string, Record<string, string>> = {};
const localesPath = path.resolve(config.root, 'mastodon/locales');
const filenames = fs.readdirSync(localesPath);
filenames
.filter((filename) => /[a-zA-Z-]+\.json$/.exec(filename))
.forEach((filename) => {
const content = fs.readFileSync(
path.resolve(localesPath, filename),
'utf-8',
);
const full = JSON.parse(content) as Record<string, string>;
const locale = filename.split('.')[0];
if (!locale)
throw new Error('Could not parse locale from filename');
const filteredLocale: Record<string, string> = {};
Object.entries(full).forEach(([key, value]) => {
if (KEEP_KEYS.includes(key)) {
filteredLocale[key] = value;
}
});
Object.entries(CUSTOM_TRANSLATIONS).forEach(([key, value]) => {
const translation = full[value];
if (translation) filteredLocale[key] = translation;
});
filteredLocales[locale] = filteredLocale;
});
return `const locales = ${JSON.stringify(filteredLocales)}; \n export default locales;`;
}
return undefined;
},
};
}

View file

@ -1,28 +0,0 @@
// Common configuration for webpacker loaded from config/webpacker.yml
const { readFileSync } = require('fs');
const { resolve } = require('path');
const { env } = require('process');
const { load } = require('js-yaml');
const configPath = resolve('config', 'webpacker.yml');
const settings = load(readFileSync(configPath), 'utf8')[env.RAILS_ENV || env.NODE_ENV];
const themePath = resolve('config', 'themes.yml');
const themes = load(readFileSync(themePath), 'utf8');
const output = {
path: resolve('public', settings.public_output_path),
publicPath: `/${settings.public_output_path}/`,
};
module.exports = {
settings,
themes,
env: {
NODE_ENV: env.NODE_ENV,
PUBLIC_OUTPUT_PATH: settings.public_output_path,
},
output,
};

View file

@ -1,62 +0,0 @@
// Note: You must restart bin/webpack-dev-server for changes to take effect
const { merge } = require('webpack-merge');
const { settings, output } = require('./configuration');
const sharedConfig = require('./shared');
const watchOptions = {};
if (process.env.VAGRANT) {
// If we are in Vagrant, we can't rely on inotify to update us with changed
// files, so we must poll instead. Here, we poll every second to see if
// anything has changed.
watchOptions.poll = 1000;
}
module.exports = merge(sharedConfig, {
mode: 'development',
cache: true,
devtool: 'cheap-module-eval-source-map',
stats: {
errorDetails: true,
},
output: {
pathinfo: true,
},
devServer: {
clientLogLevel: 'none',
compress: settings.dev_server.compress,
quiet: settings.dev_server.quiet,
disableHostCheck: settings.dev_server.disable_host_check,
host: settings.dev_server.host,
port: settings.dev_server.port,
https: settings.dev_server.https,
hot: settings.dev_server.hmr,
contentBase: output.path,
inline: settings.dev_server.inline,
useLocalIp: settings.dev_server.use_local_ip,
public: settings.dev_server.public,
publicPath: output.publicPath,
historyApiFallback: {
disableDotRule: true,
},
headers: settings.dev_server.headers,
overlay: settings.dev_server.overlay,
stats: {
entrypoints: false,
errorDetails: false,
modules: false,
moduleTrace: false,
},
watchOptions: Object.assign(
{},
settings.dev_server.watch_options,
watchOptions,
),
writeToDisk: filePath => /ocr/.test(filePath),
},
});

View file

@ -1,74 +0,0 @@
// Note: You must restart bin/webpack-dev-server for changes to take effect
const { createHash } = require('crypto');
const { readFileSync } = require('fs');
const { resolve } = require('path');
const CompressionPlugin = require('compression-webpack-plugin');
const TerserPlugin = require('terser-webpack-plugin');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
const { merge } = require('webpack-merge');
const { InjectManifest } = require('workbox-webpack-plugin');
const sharedConfig = require('./shared');
const root = resolve(__dirname, '..', '..');
module.exports = merge(sharedConfig, {
mode: 'production',
devtool: 'source-map',
stats: 'normal',
bail: true,
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
cache: true,
parallel: true,
sourceMap: true,
}),
],
},
plugins: [
new CompressionPlugin({
filename: '[path][base].gz[query]',
cache: true,
test: /\.(js|css|html|json|ico|svg|eot|otf|ttf|map)$/,
}),
new CompressionPlugin({
filename: '[path][base].br[query]',
algorithm: 'brotliCompress',
cache: true,
test: /\.(js|css|html|json|ico|svg|eot|otf|ttf|map)$/,
}),
new BundleAnalyzerPlugin({ // generates report.html
analyzerMode: 'static',
openAnalyzer: false,
logLevel: 'silent', // do not bother Webpacker, who runs with --json and parses stdout
}),
new InjectManifest({
additionalManifestEntries: ['1f602.svg', 'sheet_15_1.png'].map((filename) => {
const path = resolve(root, 'public', 'emoji', filename);
const body = readFileSync(path);
const md5 = createHash('md5');
md5.update(body);
return {
revision: md5.digest('hex'),
url: `/emoji/${filename}`,
};
}),
exclude: [
/(?:base|extra)_polyfills-.*\.js$/,
/locale_.*\.js$/,
/mailer-.*\.(?:css|js)$/,
],
include: [/\.js$/, /\.css$/],
maximumFileSizeToCacheInBytes: 2 * 1_024 * 1_024, // 2 MiB
swDest: resolve(root, 'public', 'packs', 'sw.js'),
swSrc: resolve(root, 'app', 'javascript', 'mastodon', 'service_worker', 'entry.js'),
}),
],
});

View file

@ -1,28 +0,0 @@
const { join, resolve } = require('path');
const { env, settings } = require('../configuration');
// Those modules contain modern ES code that need to be transpiled for Webpack to process it
const nodeModulesToProcess = [
'@reduxjs', 'fuzzysort', 'toygrad', '@react-spring'
];
module.exports = {
test: /\.(js|jsx|mjs|ts|tsx)$/,
include: [
settings.source_path,
...settings.resolved_paths,
...nodeModulesToProcess.map(p => resolve(`node_modules/${p}`)),
].map(p => resolve(p)),
exclude: new RegExp('node_modules\\/(?!(' + nodeModulesToProcess.join('|')+')\\/).*'),
use: [
{
loader: 'babel-loader',
options: {
cacheDirectory: join(settings.cache_path, 'babel-loader'),
cacheCompression: env.NODE_ENV === 'production',
compact: env.NODE_ENV === 'production',
},
},
],
};

View file

@ -1,28 +0,0 @@
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
test: /\.s?css$/i,
use: [
MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
sourceMap: true,
importLoaders: 2,
},
},
{
loader: 'postcss-loader',
options: {
sourceMap: true,
},
},
{
loader: 'sass-loader',
options: {
implementation: require('sass'),
sourceMap: true,
},
},
],
};

View file

@ -1,22 +0,0 @@
const { join } = require('path');
const { settings } = require('../configuration');
module.exports = {
test: new RegExp(`(${settings.static_assets_extensions.join('|')})$`, 'i'),
exclude: [/material-icons/, /svg-icons/],
use: [
{
loader: 'file-loader',
options: {
name(file) {
if (file.includes(settings.source_path)) {
return 'media/[path][name]-[hash].[ext]';
}
return 'media/[folder]/[name]-[hash:8].[ext]';
},
context: join(settings.source_path),
},
},
],
};

View file

@ -1,16 +0,0 @@
const babel = require('./babel');
const css = require('./css');
const file = require('./file');
const materialIcons = require('./material_icons');
const tesseract = require('./tesseract');
// Webpack loaders are processed in reverse order
// https://webpack.js.org/concepts/loaders/#loader-features
// Lastly, process static files using file loader
module.exports = {
materialIcons,
file,
tesseract,
css,
babel,
};

View file

@ -1,8 +0,0 @@
if (process.env.NODE_ENV === 'production') {
module.exports = {};
} else {
module.exports = {
test: /\.js$/,
loader: 'mark-loader',
};
}

View file

@ -1,14 +0,0 @@
module.exports = {
test: /\.svg$/,
include: [/material-icons/, /svg-icons/],
issuer: /\.[jt]sx?$/,
use: [
{
loader: '@svgr/webpack',
options: {
svgo: false,
titleProp: true,
},
},
],
};

View file

@ -1,13 +0,0 @@
module.exports = {
test: [
/tesseract\.js\/dist\/worker\.min\.js$/,
/tesseract\.js\/dist\/worker\.min\.js\.map$/,
/tesseract\.js-core\/tesseract-core\.wasm\.js$/,
],
use: {
loader: 'file-loader',
options: {
name: 'ocr/[name]-[hash].[ext]',
},
},
};

View file

@ -1,113 +0,0 @@
// Note: You must restart bin/webpack-dev-server for changes to take effect
const { basename, dirname, join, relative, resolve } = require('path');
const CircularDependencyPlugin = require('circular-dependency-plugin');
const { sync } = require('glob');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const extname = require('path-complete-extname');
const webpack = require('webpack');
const AssetsManifestPlugin = require('webpack-assets-manifest');
const { env, settings, themes, output } = require('./configuration');
const rules = require('./rules');
const extensionGlob = `**/*{${settings.extensions.join(',')}}*`;
const entryPath = join(settings.source_path, settings.source_entry_path);
const packPaths = sync(join(entryPath, extensionGlob));
module.exports = {
entry: Object.assign(
packPaths.reduce((map, entry) => {
const localMap = map;
const namespace = relative(join(entryPath), dirname(entry));
localMap[join(namespace, basename(entry, extname(entry)))] = resolve(entry);
return localMap;
}, {}),
Object.keys(themes).reduce((themePaths, name) => {
themePaths[name] = resolve(join(settings.source_path, themes[name]));
return themePaths;
}, {}),
),
output: {
filename: 'js/[name]-[chunkhash].js',
chunkFilename: 'js/[name]-[chunkhash].chunk.js',
hotUpdateChunkFilename: 'js/[id]-[hash].hot-update.js',
hashFunction: 'sha256',
crossOriginLoading: 'anonymous',
path: output.path,
publicPath: output.publicPath,
},
optimization: {
runtimeChunk: {
name: 'common',
},
splitChunks: {
cacheGroups: {
default: false,
vendors: false,
common: {
name: 'common',
chunks: 'all',
minChunks: 2,
minSize: 0,
test: /^(?!.*[\\/]node_modules[\\/]react-intl[\\/]).+$/,
},
},
},
occurrenceOrder: true,
},
module: {
rules: Object.keys(rules).map(key => rules[key]),
strictExportPresence: true,
},
plugins: [
new webpack.EnvironmentPlugin(JSON.parse(JSON.stringify(env))),
new webpack.NormalModuleReplacementPlugin(
/^history\//, (resource) => {
// temporary fix for https://github.com/ReactTraining/react-router/issues/5576
// to reduce bundle size
resource.request = resource.request.replace(/^history/, 'history/es');
},
),
new MiniCssExtractPlugin({
filename: 'css/[name]-[contenthash:8].css',
chunkFilename: 'css/[name]-[contenthash:8].chunk.css',
}),
new AssetsManifestPlugin({
integrity: true,
integrityHashes: ['sha256'],
entrypoints: true,
writeToDisk: true,
publicPath: true,
}),
new CircularDependencyPlugin({
failOnError: true,
})
],
resolve: {
extensions: settings.extensions,
modules: [
resolve(settings.source_path),
'node_modules',
],
alias: {
"@": resolve(settings.source_path),
}
},
resolveLoader: {
modules: ['node_modules'],
},
node: {
// Called by http-link-header in an API we never use, increases
// bundle size unnecessarily
Buffer: false,
},
};

View file

@ -1,94 +0,0 @@
# Note: You must restart bin/webpack-dev-server for changes to take effect
default: &default
source_path: app/javascript
source_entry_path: entrypoints
public_root_path: public
public_output_path: packs
cache_path: tmp/cache/webpacker
check_yarn_integrity: false
webpack_compile_output: false
# Additional paths webpack should lookup modules
# ['app/assets', 'engine/foo/app/assets']
resolved_paths: []
# Cache manifest.json for performance
cache_manifest: true
# Extract and emit a css file
extract_css: true
static_assets_extensions:
- .jpg
- .jpeg
- .png
- .tiff
- .ico
- .svg
- .eot
- .otf
- .ttf
- .woff
- .woff2
extensions:
- .mjs
- .js
- .jsx
- .ts
- .tsx
- .sass
- .scss
- .css
- .module.sass
- .module.scss
- .module.css
- .png
- .svg
- .gif
- .jpeg
- .jpg
development:
<<: *default
compile: true
# Reload manifest in development environment so we pick up changes
cache_manifest: false
# Reference: https://webpack.js.org/configuration/dev-server/
dev_server:
https: false
host: 0.0.0.0
port: 3035
public: localhost:3035
hmr: false
# Inline should be set to true if using HMR
inline: true
overlay: true
compress: true
disable_host_check: true
use_local_ip: false
quiet: false
headers:
'Access-Control-Allow-Origin': '*'
watch_options:
ignored: '**/node_modules/**'
test:
<<: *default
# CI precompiles packs prior to running the tests.
# Also avoids race conditions in parallel_tests.
compile: false
# Compile test packs to a separate directory
public_output_path: packs-test
production:
<<: *default
# Production depends on precompilation of packs prior to booting for performance.
compile: false