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