diff --git a/.devcontainer/compose.yaml b/.devcontainer/compose.yaml
index 5da1ec3a24..433729cbc4 100644
--- a/.devcontainer/compose.yaml
+++ b/.devcontainer/compose.yaml
@@ -9,6 +9,7 @@ services:
environment:
RAILS_ENV: development
NODE_ENV: development
+ VITE_RUBY_HOST: 0.0.0.0
BIND: 0.0.0.0
BOOTSNAP_CACHE_DIR: /tmp
REDIS_HOST: redis
@@ -27,6 +28,7 @@ services:
ports:
- '3000:3000'
- '3035:3035'
+ - '3036:3036'
- '4000:4000'
networks:
- external_network
diff --git a/.github/workflows/test-ruby.yml b/.github/workflows/test-ruby.yml
index 1f7f8f93a8..2fa28a587c 100644
--- a/.github/workflows/test-ruby.yml
+++ b/.github/workflows/test-ruby.yml
@@ -49,7 +49,7 @@ jobs:
public/assets
public/packs
public/packs-test
- tmp/cache/webpacker
+ tmp/cache/vite
key: ${{ matrix.mode }}-assets-${{ github.head_ref || github.ref_name }}-${{ github.sha }}
restore-keys: |
${{ matrix.mode }}-assets-${{ github.head_ref || github.ref_name }}-${{ github.sha }}
@@ -63,7 +63,7 @@ jobs:
- name: Archive asset artifacts
run: |
- tar --exclude={"*.br","*.gz"} -zcf artifacts.tar.gz public/assets public/packs*
+ tar --exclude={"*.br","*.gz"} -zcf artifacts.tar.gz public/assets public/packs* tmp/cache/vite/last-build*.json
- uses: actions/upload-artifact@v4
if: matrix.mode == 'test'
diff --git a/.gitignore b/.gitignore
index 0a2c11c534..b4fb2c946b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,6 +21,7 @@
/public/system
/public/assets
/public/packs
+/public/packs-dev
/public/packs-test
.env
.env.production
@@ -74,5 +75,3 @@ docker-compose.override.yml
# Ignore local-only rspec configuration
.rspec-local
-
-/.dist
diff --git a/.prettierignore b/.prettierignore
index 07f90e4bbd..8f16e731c8 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -18,10 +18,6 @@
!/log/.keep
/tmp
/coverage
-/public/system
-/public/assets
-/public/packs
-/public/packs-test
.env
.env.production
.env.development
@@ -60,6 +56,7 @@ docker-compose.override.yml
/public/packs
/public/packs-test
/public/system
+/public/vite*
# Ignore emoji map file
/app/javascript/mastodon/features/emoji/emoji_map.json
diff --git a/Dockerfile b/Dockerfile
index 7e9393efea..389a744b87 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -307,6 +307,7 @@ RUN \
ldconfig; \
# Use Ruby on Rails to create Mastodon assets
SECRET_KEY_BASE_DUMMY=1 \
+ # Do not run `yarn` when precompiling assets, we already ran it before
bundle exec rails assets:precompile; \
# Cleanup temporary files
rm -fr /opt/mastodon/tmp;
diff --git a/Gemfile b/Gemfile
index 44c4c9a54d..68fa90f73b 100644
--- a/Gemfile
+++ b/Gemfile
@@ -95,7 +95,6 @@ gem 'tty-prompt', '~> 0.23', require: false
gem 'twitter-text', '~> 3.1.0'
gem 'tzinfo-data', '~> 1.2023'
gem 'webauthn', '~> 3.0'
-gem 'webpacker', '~> 5.4'
gem 'webpush', github: 'mastodon/webpush', ref: '9631ac63045cfabddacc69fc06e919b4c13eb913'
gem 'json-ld'
@@ -230,3 +229,5 @@ gem 'rubyzip', '~> 2.3'
gem 'hcaptcha', '~> 7.1'
gem 'mail', '~> 2.8'
+
+gem 'vite_rails', '~> 3.0.19'
diff --git a/Gemfile.lock b/Gemfile.lock
index 1df8c3ed3d..a9c4ebee9e 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -203,6 +203,7 @@ GEM
railties (>= 5)
dotenv (3.1.8)
drb (2.2.1)
+ dry-cli (1.2.0)
elasticsearch (7.17.11)
elasticsearch-api (= 7.17.11)
elasticsearch-transport (= 7.17.11)
@@ -806,7 +807,6 @@ GEM
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
- semantic_range (3.1.0)
shoulda-matchers (6.5.0)
activesupport (>= 5.2.0)
sidekiq (6.5.12)
@@ -892,6 +892,15 @@ GEM
validate_url (1.0.15)
activemodel (>= 3.0.0)
public_suffix
+ vite_rails (3.0.19)
+ railties (>= 5.1, < 9)
+ vite_ruby (~> 3.0, >= 3.2.2)
+ vite_ruby (3.9.2)
+ dry-cli (>= 0.7, < 2)
+ logger (~> 1.6)
+ mutex_m
+ rack-proxy (~> 0.6, >= 0.6.1)
+ zeitwerk (~> 2.2)
warden (1.2.9)
rack (>= 2.0.9)
webauthn (3.4.0)
@@ -910,11 +919,6 @@ GEM
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
- webpacker (5.4.4)
- activesupport (>= 5.2)
- rack-proxy (>= 0.6.1)
- railties (>= 5.2)
- semantic_range (>= 2.3.0)
webrick (1.9.1)
websocket (1.2.11)
websocket-driver (0.7.7)
@@ -1078,9 +1082,9 @@ DEPENDENCIES
tty-prompt (~> 0.23)
twitter-text (~> 3.1.0)
tzinfo-data (~> 1.2023)
+ vite_rails (~> 3.0.19)
webauthn (~> 3.0)
webmock (~> 3.18)
- webpacker (~> 5.4)
webpush!
xorcist (~> 1.1)
diff --git a/app/helpers/routing_helper.rb b/app/helpers/routing_helper.rb
index 22efc5f092..1ff8b992c3 100644
--- a/app/helpers/routing_helper.rb
+++ b/app/helpers/routing_helper.rb
@@ -4,7 +4,7 @@ module RoutingHelper
extend ActiveSupport::Concern
include ActionView::Helpers::AssetTagHelper
- include Webpacker::Helper
+ include ViteRails::TagHelpers
included do
include Rails.application.routes.url_helpers
@@ -25,7 +25,7 @@ module RoutingHelper
end
def frontend_asset_path(source, **)
- asset_pack_path("media/#{source}", **)
+ vite_asset_path(source, **)
end
def frontend_asset_url(source, **)
diff --git a/app/helpers/theme_helper.rb b/app/helpers/theme_helper.rb
index cda380b3bc..158ac92d6d 100644
--- a/app/helpers/theme_helper.rb
+++ b/app/helpers/theme_helper.rb
@@ -4,11 +4,14 @@ module ThemeHelper
def theme_style_tags(theme)
if theme == 'system'
''.html_safe.tap do |tags|
- tags << stylesheet_pack_tag('mastodon-light', media: 'not all and (prefers-color-scheme: dark)', crossorigin: 'anonymous')
- tags << stylesheet_pack_tag('default', media: '(prefers-color-scheme: dark)', crossorigin: 'anonymous')
+ tags << vite_stylesheet_tag('styles/mastodon-light.scss', media: 'not all and (prefers-color-scheme: dark)', crossorigin: 'anonymous')
+ tags << vite_stylesheet_tag('styles/application.scss', media: '(prefers-color-scheme: dark)', crossorigin: 'anonymous')
end
+ # TODO: Determine why default doesn't map correctly.
+ elsif theme == 'default'
+ vite_stylesheet_tag 'styles/application.scss', media: 'all', crossorigin: 'anonymous'
else
- stylesheet_pack_tag theme, media: 'all', crossorigin: 'anonymous'
+ vite_stylesheet_tag "styles/#{theme}.scss", media: 'all', crossorigin: 'anonymous'
end
end
diff --git a/app/javascript/entrypoints/admin.tsx b/app/javascript/entrypoints/admin.tsx
index 8f5c2d9f5e..a60778f0c0 100644
--- a/app/javascript/entrypoints/admin.tsx
+++ b/app/javascript/entrypoints/admin.tsx
@@ -272,7 +272,7 @@ async function mountReactComponent(element: Element) {
);
const { default: Component } = (await import(
- `@/mastodon/components/admin/${componentName}`
+ `@/mastodon/components/admin/${componentName}.jsx`
)) as { default: React.ComponentType };
const root = createRoot(element);
diff --git a/app/javascript/entrypoints/index.html b/app/javascript/entrypoints/index.html
new file mode 100644
index 0000000000..025030ba46
--- /dev/null
+++ b/app/javascript/entrypoints/index.html
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/app/javascript/entrypoints/inert.ts b/app/javascript/entrypoints/inert.ts
deleted file mode 100644
index 3d32325505..0000000000
--- a/app/javascript/entrypoints/inert.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-/* Placeholder file to have `inert.scss` compiled by Vite
- This is used by the `wicg-inert` polyfill */
-
-import '../styles/inert.scss';
diff --git a/app/javascript/entrypoints/mailer.ts b/app/javascript/entrypoints/mailer.ts
deleted file mode 100644
index 22b3ef6ecd..0000000000
--- a/app/javascript/entrypoints/mailer.ts
+++ /dev/null
@@ -1 +0,0 @@
-import '../styles/mailer.scss';
diff --git a/app/javascript/mastodon/components/status_action_bar.jsx b/app/javascript/mastodon/components/status_action_bar.jsx
index 25116d19b0..ec5a9780cb 100644
--- a/app/javascript/mastodon/components/status_action_bar.jsx
+++ b/app/javascript/mastodon/components/status_action_bar.jsx
@@ -9,7 +9,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
-import BookmarkIcon from '@/material-icons/400-24px/bookmark-fill.svg';
+import BookmarkIcon from '@/material-icons/400-24px/bookmark-fill.svg?react';
import BookmarkBorderIcon from '@/material-icons/400-24px/bookmark.svg?react';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
diff --git a/app/javascript/mastodon/features/alt_text_modal/index.tsx b/app/javascript/mastodon/features/alt_text_modal/index.tsx
index 08e4a8917c..f24a3b6f70 100644
--- a/app/javascript/mastodon/features/alt_text_modal/index.tsx
+++ b/app/javascript/mastodon/features/alt_text_modal/index.tsx
@@ -15,10 +15,6 @@ import type { List as ImmutableList, Map as ImmutableMap } from 'immutable';
import { useSpring, animated } from '@react-spring/web';
import Textarea from 'react-textarea-autosize';
import { length } from 'stringz';
-// eslint-disable-next-line import/extensions
-import tesseractWorkerPath from 'tesseract.js/dist/worker.min.js';
-// eslint-disable-next-line import/no-extraneous-dependencies
-import tesseractCorePath from 'tesseract.js-core/tesseract-core.wasm.js';
import { showAlertForError } from 'mastodon/actions/alerts';
import { uploadThumbnail } from 'mastodon/actions/compose';
@@ -350,9 +346,15 @@ export const AltTextModal = forwardRef>(
fetchTesseract()
.then(async ({ createWorker }) => {
+ const [tesseractWorkerPath, tesseractCorePath] = await Promise.all([
+ // eslint-disable-next-line import/extensions
+ import('tesseract.js/dist/worker.min.js?url'),
+ // eslint-disable-next-line import/no-extraneous-dependencies
+ import('tesseract.js-core/tesseract-core.wasm.js?url'),
+ ]);
const worker = await createWorker('eng', 1, {
- workerPath: tesseractWorkerPath as string,
- corePath: tesseractCorePath as string,
+ workerPath: tesseractWorkerPath.default,
+ corePath: tesseractCorePath.default,
langPath: `${assetHost}/ocr/lang-data`,
cacheMethod: 'write',
});
@@ -501,5 +503,4 @@ export const AltTextModal = forwardRef>(
);
},
);
-
AltTextModal.displayName = 'AltTextModal';
diff --git a/app/javascript/mastodon/locales/load_locale.ts b/app/javascript/mastodon/locales/load_locale.ts
index 8a6116f324..94c7db1141 100644
--- a/app/javascript/mastodon/locales/load_locale.ts
+++ b/app/javascript/mastodon/locales/load_locale.ts
@@ -22,9 +22,9 @@ export async function loadLocale() {
if (isLocaleLoaded()) return;
// If there is no locale file, then fallback to english
- const localeFile = Object.hasOwn(localeFiles, '`./${locale}.json`')
+ const localeFile = Object.hasOwn(localeFiles, `./${locale}.json`)
? localeFiles[`./${locale}.json`]
- : localeFiles[`./en.json`];
+ : localeFiles['./en.json'];
if (!localeFile) throw new Error('Could not load the locale JSON file');
diff --git a/app/javascript/mastodon/main.jsx b/app/javascript/mastodon/main.tsx
similarity index 58%
rename from app/javascript/mastodon/main.jsx
rename to app/javascript/mastodon/main.tsx
index e7979d56a1..a9696ac50e 100644
--- a/app/javascript/mastodon/main.jsx
+++ b/app/javascript/mastodon/main.tsx
@@ -7,17 +7,19 @@ import * as perf from 'mastodon/performance';
import ready from 'mastodon/ready';
import { store } from 'mastodon/store';
-import { isProduction } from './utils/environment';
+import { isProduction, isDevelopment } from './utils/environment';
-/**
- * @returns {Promise}
- */
function main() {
perf.start('main()');
return ready(async () => {
const mountNode = document.getElementById('mastodon');
- const props = JSON.parse(mountNode.getAttribute('data-props'));
+ if (!mountNode) {
+ throw new Error('Mount node not found');
+ }
+ const props = JSON.parse(
+ mountNode.getAttribute('data-props') ?? '{}',
+ ) as Record;
const root = createRoot(mountNode);
root.render();
@@ -25,8 +27,10 @@ function main() {
if (isProduction() && me && 'serviceWorker' in navigator) {
const { Workbox } = await import('workbox-window');
- const wb = new Workbox('/sw.js');
- /** @type {ServiceWorkerRegistration} */
+ const wb = new Workbox(
+ isDevelopment() ? '/packs-dev/dev-sw.js?dev-sw' : '/sw.js',
+ { type: 'module', scope: '/' },
+ );
let registration;
try {
@@ -35,8 +39,14 @@ function main() {
console.error(err);
}
- if (registration && 'Notification' in window && Notification.permission === 'granted') {
- const registerPushNotifications = await import('mastodon/actions/push_notifications');
+ if (
+ registration &&
+ 'Notification' in window &&
+ Notification.permission === 'granted'
+ ) {
+ const registerPushNotifications = await import(
+ 'mastodon/actions/push_notifications'
+ );
store.dispatch(registerPushNotifications.register());
}
@@ -46,4 +56,5 @@ function main() {
});
}
+// eslint-disable-next-line import/no-default-export
export default main;
diff --git a/app/javascript/mastodon/service_worker/entry.js b/app/javascript/mastodon/service_worker/sw.js
similarity index 95%
rename from app/javascript/mastodon/service_worker/entry.js
rename to app/javascript/mastodon/service_worker/sw.js
index a4aebcd3a7..4609a8fc97 100644
--- a/app/javascript/mastodon/service_worker/entry.js
+++ b/app/javascript/mastodon/service_worker/sw.js
@@ -1,5 +1,5 @@
import { ExpirationPlugin } from 'workbox-expiration';
-import { precacheAndRoute } from 'workbox-precaching';
+// import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { CacheFirst } from 'workbox-strategies';
@@ -15,10 +15,10 @@ function fetchRoot() {
return fetch('/', { credentials: 'include', redirect: 'manual' });
}
-precacheAndRoute(self.__WB_MANIFEST);
+// precacheAndRoute(self.__WB_MANIFEST);
registerRoute(
- /locale_.*\.js$/,
+ /intl\/.*\.js$/,
new CacheFirst({
cacheName: `${CACHE_NAME_PREFIX}locales`,
plugins: [
diff --git a/app/javascript/mastodon/service_worker/web_push_locales.js b/app/javascript/mastodon/service_worker/web_push_locales.js
deleted file mode 100644
index f3d61e0195..0000000000
--- a/app/javascript/mastodon/service_worker/web_push_locales.js
+++ /dev/null
@@ -1,41 +0,0 @@
-/* @preval */
-
-const fs = require('fs');
-const path = require('path');
-
-const { defineMessages } = require('react-intl');
-
-const messages = defineMessages({
- mentioned_you: { id: 'notification.mentioned_you', defaultMessage: '{name} mentioned you' },
-});
-
-const filtered = {};
-const filenames = fs.readdirSync(path.resolve(__dirname, '../locales'));
-
-filenames.forEach(filename => {
- if (!filename.match(/\.json$/)) return;
-
- const content = fs.readFileSync(path.resolve(__dirname, `../locales/${filename}`), 'utf-8');
- const full = JSON.parse(content);
- const locale = filename.split('.')[0];
-
- filtered[locale] = {
- 'notification.favourite': full['notification.favourite'] || '',
- 'notification.follow': full['notification.follow'] || '',
- 'notification.follow_request': full['notification.follow_request'] || '',
- 'notification.mention': full[messages.mentioned_you.id] || '',
- 'notification.reblog': full['notification.reblog'] || '',
- 'notification.poll': full['notification.poll'] || '',
- 'notification.status': full['notification.status'] || '',
- 'notification.update': full['notification.update'] || '',
- 'notification.admin.sign_up': full['notification.admin.sign_up'] || '',
-
- 'status.show_more': full['status.show_more'] || '',
- 'status.reblog': full['status.reblog'] || '',
- 'status.favourite': full['status.favourite'] || '',
-
- 'notifications.group': full['notifications.group'] || '',
- };
-});
-
-module.exports = JSON.parse(JSON.stringify(filtered));
diff --git a/app/javascript/mastodon/service_worker/web_push_notifications.js b/app/javascript/mastodon/service_worker/web_push_notifications.js
index 77187a59ed..5a43449b0d 100644
--- a/app/javascript/mastodon/service_worker/web_push_notifications.js
+++ b/app/javascript/mastodon/service_worker/web_push_notifications.js
@@ -1,8 +1,10 @@
import { IntlMessageFormat } from 'intl-messageformat';
import { unescape } from 'lodash';
-
-import locales from './web_push_locales';
+// see config/vite/plugins/sw-locales
+// it needs to be updated when new locale keys are used in this file
+// eslint-disable-next-line import/no-unresolved
+import locales from "virtual:mastodon-sw-locales";
const MAX_NOTIFICATIONS = 5;
const GROUP_TAG = 'tag';
diff --git a/app/javascript/mastodon/utils/environment.ts b/app/javascript/mastodon/utils/environment.ts
index b6371499f6..5ccd4d27e3 100644
--- a/app/javascript/mastodon/utils/environment.ts
+++ b/app/javascript/mastodon/utils/environment.ts
@@ -1,7 +1,11 @@
export function isDevelopment() {
- return process.env.NODE_ENV === 'development';
+ if (typeof process !== 'undefined')
+ return process.env.NODE_ENV === 'development';
+ else return import.meta.env.DEV;
}
export function isProduction() {
- return process.env.NODE_ENV === 'production';
+ if (typeof process !== 'undefined')
+ return process.env.NODE_ENV === 'production';
+ else return import.meta.env.PROD;
}
diff --git a/app/javascript/styles/mastodon/forms.scss b/app/javascript/styles/mastodon/forms.scss
index 6ec6a4199f..6a3afeb736 100644
--- a/app/javascript/styles/mastodon/forms.scss
+++ b/app/javascript/styles/mastodon/forms.scss
@@ -393,7 +393,7 @@ code {
max-width: 100%;
height: auto;
border-radius: var(--avatar-border-radius);
- background: url('images/void.png');
+ background: url('@/images/void.png');
&[src$='missing.png'] {
visibility: hidden;
diff --git a/app/javascript/types/image.d.ts b/app/javascript/types/image.d.ts
index 8a08eca9f6..aa4b6ded71 100644
--- a/app/javascript/types/image.d.ts
+++ b/app/javascript/types/image.d.ts
@@ -1,3 +1,5 @@
+///
+
/* eslint-disable import/no-default-export */
declare module '*.avif' {
const path: string;
@@ -19,23 +21,6 @@ declare module '*.png' {
export default path;
}
-declare module '*.svg' {
- const path: string;
- export default path;
-}
-
-declare module '*.svg?react' {
- import type React from 'react';
-
- interface SVGPropsWithTitle extends React.SVGProps {
- title?: string;
- }
-
- const ReactComponent: React.FC;
-
- export default ReactComponent;
-}
-
declare module '*.webp' {
const path: string;
export default path;
diff --git a/app/views/auth/sessions/two_factor.html.haml b/app/views/auth/sessions/two_factor.html.haml
index 653f155801..38def20fc5 100644
--- a/app/views/auth/sessions/two_factor.html.haml
+++ b/app/views/auth/sessions/two_factor.html.haml
@@ -1,7 +1,7 @@
- content_for :page_title do
= t('auth.login')
-= javascript_pack_tag 'two_factor_authentication', crossorigin: 'anonymous'
+= vite_typescript_tag 'two_factor_authentication.ts', crossorigin: 'anonymous'
- if webauthn_enabled?
= render partial: 'auth/sessions/two_factor/webauthn_form', locals: { hidden: @scheme_type != 'webauthn' }
diff --git a/app/views/auth/setup/show.html.haml b/app/views/auth/setup/show.html.haml
index 91654ca214..83e0bfd25f 100644
--- a/app/views/auth/setup/show.html.haml
+++ b/app/views/auth/setup/show.html.haml
@@ -1,7 +1,7 @@
- content_for :page_title do
= t('auth.setup.title')
-= javascript_pack_tag 'sign_up', crossorigin: 'anonymous'
+= vite_typescript_tag 'sign_up.ts', crossorigin: 'anonymous'
= simple_form_for(@user, url: auth_setup_path) do |f|
= render 'auth/shared/progress', stage: 'confirm'
diff --git a/app/views/layouts/admin.html.haml b/app/views/layouts/admin.html.haml
index 3f7727cdfb..08432a177c 100644
--- a/app/views/layouts/admin.html.haml
+++ b/app/views/layouts/admin.html.haml
@@ -1,7 +1,7 @@
- content_for :header_tags do
= render_initial_state
- = javascript_pack_tag 'public', crossorigin: 'anonymous'
- = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
+ = vite_typescript_tag 'public.tsx', crossorigin: 'anonymous'
+ = vite_typescript_tag 'admin.tsx', crossorigin: 'anonymous'
- content_for :body_classes, 'admin'
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 6f016c6cf5..690a610bf3 100755
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -26,11 +26,12 @@
%title= html_title
= theme_style_tags current_theme
+ = vite_client_tag
+ = vite_react_refresh_tag
-# Needed for the wicg-inert polyfill. It needs to be on it's own