mirror of
https://github.com/mastodon/mastodon.git
synced 2025-05-11 12:11:12 +00:00
Webpack to Vite: Utilize Ruby Vite (#34469)
Co-authored-by: Renaud Chaput <renchap@gmail.com>
This commit is contained in:
parent
b68f93fc47
commit
f2b85d4696
|
@ -9,6 +9,7 @@ services:
|
||||||
environment:
|
environment:
|
||||||
RAILS_ENV: development
|
RAILS_ENV: development
|
||||||
NODE_ENV: development
|
NODE_ENV: development
|
||||||
|
VITE_RUBY_HOST: 0.0.0.0
|
||||||
BIND: 0.0.0.0
|
BIND: 0.0.0.0
|
||||||
BOOTSNAP_CACHE_DIR: /tmp
|
BOOTSNAP_CACHE_DIR: /tmp
|
||||||
REDIS_HOST: redis
|
REDIS_HOST: redis
|
||||||
|
@ -27,6 +28,7 @@ services:
|
||||||
ports:
|
ports:
|
||||||
- '3000:3000'
|
- '3000:3000'
|
||||||
- '3035:3035'
|
- '3035:3035'
|
||||||
|
- '3036:3036'
|
||||||
- '4000:4000'
|
- '4000:4000'
|
||||||
networks:
|
networks:
|
||||||
- external_network
|
- external_network
|
||||||
|
|
4
.github/workflows/test-ruby.yml
vendored
4
.github/workflows/test-ruby.yml
vendored
|
@ -49,7 +49,7 @@ jobs:
|
||||||
public/assets
|
public/assets
|
||||||
public/packs
|
public/packs
|
||||||
public/packs-test
|
public/packs-test
|
||||||
tmp/cache/webpacker
|
tmp/cache/vite
|
||||||
key: ${{ matrix.mode }}-assets-${{ github.head_ref || github.ref_name }}-${{ github.sha }}
|
key: ${{ matrix.mode }}-assets-${{ github.head_ref || github.ref_name }}-${{ github.sha }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ matrix.mode }}-assets-${{ github.head_ref || github.ref_name }}-${{ github.sha }}
|
${{ matrix.mode }}-assets-${{ github.head_ref || github.ref_name }}-${{ github.sha }}
|
||||||
|
@ -63,7 +63,7 @@ jobs:
|
||||||
|
|
||||||
- name: Archive asset artifacts
|
- name: Archive asset artifacts
|
||||||
run: |
|
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
|
- uses: actions/upload-artifact@v4
|
||||||
if: matrix.mode == 'test'
|
if: matrix.mode == 'test'
|
||||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -21,6 +21,7 @@
|
||||||
/public/system
|
/public/system
|
||||||
/public/assets
|
/public/assets
|
||||||
/public/packs
|
/public/packs
|
||||||
|
/public/packs-dev
|
||||||
/public/packs-test
|
/public/packs-test
|
||||||
.env
|
.env
|
||||||
.env.production
|
.env.production
|
||||||
|
@ -74,5 +75,3 @@ docker-compose.override.yml
|
||||||
|
|
||||||
# Ignore local-only rspec configuration
|
# Ignore local-only rspec configuration
|
||||||
.rspec-local
|
.rspec-local
|
||||||
|
|
||||||
/.dist
|
|
||||||
|
|
|
@ -18,10 +18,6 @@
|
||||||
!/log/.keep
|
!/log/.keep
|
||||||
/tmp
|
/tmp
|
||||||
/coverage
|
/coverage
|
||||||
/public/system
|
|
||||||
/public/assets
|
|
||||||
/public/packs
|
|
||||||
/public/packs-test
|
|
||||||
.env
|
.env
|
||||||
.env.production
|
.env.production
|
||||||
.env.development
|
.env.development
|
||||||
|
@ -60,6 +56,7 @@ docker-compose.override.yml
|
||||||
/public/packs
|
/public/packs
|
||||||
/public/packs-test
|
/public/packs-test
|
||||||
/public/system
|
/public/system
|
||||||
|
/public/vite*
|
||||||
|
|
||||||
# Ignore emoji map file
|
# Ignore emoji map file
|
||||||
/app/javascript/mastodon/features/emoji/emoji_map.json
|
/app/javascript/mastodon/features/emoji/emoji_map.json
|
||||||
|
|
|
@ -307,6 +307,7 @@ RUN \
|
||||||
ldconfig; \
|
ldconfig; \
|
||||||
# Use Ruby on Rails to create Mastodon assets
|
# Use Ruby on Rails to create Mastodon assets
|
||||||
SECRET_KEY_BASE_DUMMY=1 \
|
SECRET_KEY_BASE_DUMMY=1 \
|
||||||
|
# Do not run `yarn` when precompiling assets, we already ran it before
|
||||||
bundle exec rails assets:precompile; \
|
bundle exec rails assets:precompile; \
|
||||||
# Cleanup temporary files
|
# Cleanup temporary files
|
||||||
rm -fr /opt/mastodon/tmp;
|
rm -fr /opt/mastodon/tmp;
|
||||||
|
|
3
Gemfile
3
Gemfile
|
@ -95,7 +95,6 @@ gem 'tty-prompt', '~> 0.23', require: false
|
||||||
gem 'twitter-text', '~> 3.1.0'
|
gem 'twitter-text', '~> 3.1.0'
|
||||||
gem 'tzinfo-data', '~> 1.2023'
|
gem 'tzinfo-data', '~> 1.2023'
|
||||||
gem 'webauthn', '~> 3.0'
|
gem 'webauthn', '~> 3.0'
|
||||||
gem 'webpacker', '~> 5.4'
|
|
||||||
gem 'webpush', github: 'mastodon/webpush', ref: '9631ac63045cfabddacc69fc06e919b4c13eb913'
|
gem 'webpush', github: 'mastodon/webpush', ref: '9631ac63045cfabddacc69fc06e919b4c13eb913'
|
||||||
|
|
||||||
gem 'json-ld'
|
gem 'json-ld'
|
||||||
|
@ -230,3 +229,5 @@ gem 'rubyzip', '~> 2.3'
|
||||||
gem 'hcaptcha', '~> 7.1'
|
gem 'hcaptcha', '~> 7.1'
|
||||||
|
|
||||||
gem 'mail', '~> 2.8'
|
gem 'mail', '~> 2.8'
|
||||||
|
|
||||||
|
gem 'vite_rails', '~> 3.0.19'
|
||||||
|
|
18
Gemfile.lock
18
Gemfile.lock
|
@ -203,6 +203,7 @@ GEM
|
||||||
railties (>= 5)
|
railties (>= 5)
|
||||||
dotenv (3.1.8)
|
dotenv (3.1.8)
|
||||||
drb (2.2.1)
|
drb (2.2.1)
|
||||||
|
dry-cli (1.2.0)
|
||||||
elasticsearch (7.17.11)
|
elasticsearch (7.17.11)
|
||||||
elasticsearch-api (= 7.17.11)
|
elasticsearch-api (= 7.17.11)
|
||||||
elasticsearch-transport (= 7.17.11)
|
elasticsearch-transport (= 7.17.11)
|
||||||
|
@ -806,7 +807,6 @@ GEM
|
||||||
rexml (~> 3.2, >= 3.2.5)
|
rexml (~> 3.2, >= 3.2.5)
|
||||||
rubyzip (>= 1.2.2, < 3.0)
|
rubyzip (>= 1.2.2, < 3.0)
|
||||||
websocket (~> 1.0)
|
websocket (~> 1.0)
|
||||||
semantic_range (3.1.0)
|
|
||||||
shoulda-matchers (6.5.0)
|
shoulda-matchers (6.5.0)
|
||||||
activesupport (>= 5.2.0)
|
activesupport (>= 5.2.0)
|
||||||
sidekiq (6.5.12)
|
sidekiq (6.5.12)
|
||||||
|
@ -892,6 +892,15 @@ GEM
|
||||||
validate_url (1.0.15)
|
validate_url (1.0.15)
|
||||||
activemodel (>= 3.0.0)
|
activemodel (>= 3.0.0)
|
||||||
public_suffix
|
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)
|
warden (1.2.9)
|
||||||
rack (>= 2.0.9)
|
rack (>= 2.0.9)
|
||||||
webauthn (3.4.0)
|
webauthn (3.4.0)
|
||||||
|
@ -910,11 +919,6 @@ GEM
|
||||||
addressable (>= 2.8.0)
|
addressable (>= 2.8.0)
|
||||||
crack (>= 0.3.2)
|
crack (>= 0.3.2)
|
||||||
hashdiff (>= 0.4.0, < 2.0.0)
|
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)
|
webrick (1.9.1)
|
||||||
websocket (1.2.11)
|
websocket (1.2.11)
|
||||||
websocket-driver (0.7.7)
|
websocket-driver (0.7.7)
|
||||||
|
@ -1078,9 +1082,9 @@ DEPENDENCIES
|
||||||
tty-prompt (~> 0.23)
|
tty-prompt (~> 0.23)
|
||||||
twitter-text (~> 3.1.0)
|
twitter-text (~> 3.1.0)
|
||||||
tzinfo-data (~> 1.2023)
|
tzinfo-data (~> 1.2023)
|
||||||
|
vite_rails (~> 3.0.19)
|
||||||
webauthn (~> 3.0)
|
webauthn (~> 3.0)
|
||||||
webmock (~> 3.18)
|
webmock (~> 3.18)
|
||||||
webpacker (~> 5.4)
|
|
||||||
webpush!
|
webpush!
|
||||||
xorcist (~> 1.1)
|
xorcist (~> 1.1)
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ module RoutingHelper
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
include ActionView::Helpers::AssetTagHelper
|
include ActionView::Helpers::AssetTagHelper
|
||||||
include Webpacker::Helper
|
include ViteRails::TagHelpers
|
||||||
|
|
||||||
included do
|
included do
|
||||||
include Rails.application.routes.url_helpers
|
include Rails.application.routes.url_helpers
|
||||||
|
@ -25,7 +25,7 @@ module RoutingHelper
|
||||||
end
|
end
|
||||||
|
|
||||||
def frontend_asset_path(source, **)
|
def frontend_asset_path(source, **)
|
||||||
asset_pack_path("media/#{source}", **)
|
vite_asset_path(source, **)
|
||||||
end
|
end
|
||||||
|
|
||||||
def frontend_asset_url(source, **)
|
def frontend_asset_url(source, **)
|
||||||
|
|
|
@ -4,11 +4,14 @@ module ThemeHelper
|
||||||
def theme_style_tags(theme)
|
def theme_style_tags(theme)
|
||||||
if theme == 'system'
|
if theme == 'system'
|
||||||
''.html_safe.tap do |tags|
|
''.html_safe.tap do |tags|
|
||||||
tags << stylesheet_pack_tag('mastodon-light', media: 'not all and (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 << stylesheet_pack_tag('default', media: '(prefers-color-scheme: dark)', crossorigin: 'anonymous')
|
tags << vite_stylesheet_tag('styles/application.scss', media: '(prefers-color-scheme: dark)', crossorigin: 'anonymous')
|
||||||
end
|
end
|
||||||
|
# TODO: Determine why default doesn't map correctly.
|
||||||
|
elsif theme == 'default'
|
||||||
|
vite_stylesheet_tag 'styles/application.scss', media: 'all', crossorigin: 'anonymous'
|
||||||
else
|
else
|
||||||
stylesheet_pack_tag theme, media: 'all', crossorigin: 'anonymous'
|
vite_stylesheet_tag "styles/#{theme}.scss", media: 'all', crossorigin: 'anonymous'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -272,7 +272,7 @@ async function mountReactComponent(element: Element) {
|
||||||
);
|
);
|
||||||
|
|
||||||
const { default: Component } = (await import(
|
const { default: Component } = (await import(
|
||||||
`@/mastodon/components/admin/${componentName}`
|
`@/mastodon/components/admin/${componentName}.jsx`
|
||||||
)) as { default: React.ComponentType };
|
)) as { default: React.ComponentType };
|
||||||
|
|
||||||
const root = createRoot(element);
|
const root = createRoot(element);
|
||||||
|
|
8
app/javascript/entrypoints/index.html
Normal file
8
app/javascript/entrypoints/index.html
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<script type="module" src="./application.ts"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="mastodon"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -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';
|
|
|
@ -1 +0,0 @@
|
||||||
import '../styles/mailer.scss';
|
|
|
@ -9,7 +9,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
|
||||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||||
import { connect } from 'react-redux';
|
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 BookmarkBorderIcon from '@/material-icons/400-24px/bookmark.svg?react';
|
||||||
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
|
||||||
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
|
import RepeatIcon from '@/material-icons/400-24px/repeat.svg?react';
|
||||||
|
|
|
@ -15,10 +15,6 @@ import type { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
||||||
import { useSpring, animated } from '@react-spring/web';
|
import { useSpring, animated } from '@react-spring/web';
|
||||||
import Textarea from 'react-textarea-autosize';
|
import Textarea from 'react-textarea-autosize';
|
||||||
import { length } from 'stringz';
|
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 { showAlertForError } from 'mastodon/actions/alerts';
|
||||||
import { uploadThumbnail } from 'mastodon/actions/compose';
|
import { uploadThumbnail } from 'mastodon/actions/compose';
|
||||||
|
@ -350,9 +346,15 @@ export const AltTextModal = forwardRef<ModalRef, Props & Partial<RestoreProps>>(
|
||||||
|
|
||||||
fetchTesseract()
|
fetchTesseract()
|
||||||
.then(async ({ createWorker }) => {
|
.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, {
|
const worker = await createWorker('eng', 1, {
|
||||||
workerPath: tesseractWorkerPath as string,
|
workerPath: tesseractWorkerPath.default,
|
||||||
corePath: tesseractCorePath as string,
|
corePath: tesseractCorePath.default,
|
||||||
langPath: `${assetHost}/ocr/lang-data`,
|
langPath: `${assetHost}/ocr/lang-data`,
|
||||||
cacheMethod: 'write',
|
cacheMethod: 'write',
|
||||||
});
|
});
|
||||||
|
@ -501,5 +503,4 @@ export const AltTextModal = forwardRef<ModalRef, Props & Partial<RestoreProps>>(
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
AltTextModal.displayName = 'AltTextModal';
|
AltTextModal.displayName = 'AltTextModal';
|
||||||
|
|
|
@ -22,9 +22,9 @@ export async function loadLocale() {
|
||||||
if (isLocaleLoaded()) return;
|
if (isLocaleLoaded()) return;
|
||||||
|
|
||||||
// If there is no locale file, then fallback to english
|
// 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[`./${locale}.json`]
|
||||||
: localeFiles[`./en.json`];
|
: localeFiles['./en.json'];
|
||||||
|
|
||||||
if (!localeFile) throw new Error('Could not load the locale JSON file');
|
if (!localeFile) throw new Error('Could not load the locale JSON file');
|
||||||
|
|
||||||
|
|
|
@ -7,17 +7,19 @@ import * as perf from 'mastodon/performance';
|
||||||
import ready from 'mastodon/ready';
|
import ready from 'mastodon/ready';
|
||||||
import { store } from 'mastodon/store';
|
import { store } from 'mastodon/store';
|
||||||
|
|
||||||
import { isProduction } from './utils/environment';
|
import { isProduction, isDevelopment } from './utils/environment';
|
||||||
|
|
||||||
/**
|
|
||||||
* @returns {Promise<void>}
|
|
||||||
*/
|
|
||||||
function main() {
|
function main() {
|
||||||
perf.start('main()');
|
perf.start('main()');
|
||||||
|
|
||||||
return ready(async () => {
|
return ready(async () => {
|
||||||
const mountNode = document.getElementById('mastodon');
|
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<string, unknown>;
|
||||||
|
|
||||||
const root = createRoot(mountNode);
|
const root = createRoot(mountNode);
|
||||||
root.render(<Mastodon {...props} />);
|
root.render(<Mastodon {...props} />);
|
||||||
|
@ -25,8 +27,10 @@ function main() {
|
||||||
|
|
||||||
if (isProduction() && me && 'serviceWorker' in navigator) {
|
if (isProduction() && me && 'serviceWorker' in navigator) {
|
||||||
const { Workbox } = await import('workbox-window');
|
const { Workbox } = await import('workbox-window');
|
||||||
const wb = new Workbox('/sw.js');
|
const wb = new Workbox(
|
||||||
/** @type {ServiceWorkerRegistration} */
|
isDevelopment() ? '/packs-dev/dev-sw.js?dev-sw' : '/sw.js',
|
||||||
|
{ type: 'module', scope: '/' },
|
||||||
|
);
|
||||||
let registration;
|
let registration;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -35,8 +39,14 @@ function main() {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (registration && 'Notification' in window && Notification.permission === 'granted') {
|
if (
|
||||||
const registerPushNotifications = await import('mastodon/actions/push_notifications');
|
registration &&
|
||||||
|
'Notification' in window &&
|
||||||
|
Notification.permission === 'granted'
|
||||||
|
) {
|
||||||
|
const registerPushNotifications = await import(
|
||||||
|
'mastodon/actions/push_notifications'
|
||||||
|
);
|
||||||
|
|
||||||
store.dispatch(registerPushNotifications.register());
|
store.dispatch(registerPushNotifications.register());
|
||||||
}
|
}
|
||||||
|
@ -46,4 +56,5 @@ function main() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/no-default-export
|
||||||
export default main;
|
export default main;
|
|
@ -1,5 +1,5 @@
|
||||||
import { ExpirationPlugin } from 'workbox-expiration';
|
import { ExpirationPlugin } from 'workbox-expiration';
|
||||||
import { precacheAndRoute } from 'workbox-precaching';
|
// import { precacheAndRoute } from 'workbox-precaching';
|
||||||
import { registerRoute } from 'workbox-routing';
|
import { registerRoute } from 'workbox-routing';
|
||||||
import { CacheFirst } from 'workbox-strategies';
|
import { CacheFirst } from 'workbox-strategies';
|
||||||
|
|
||||||
|
@ -15,10 +15,10 @@ function fetchRoot() {
|
||||||
return fetch('/', { credentials: 'include', redirect: 'manual' });
|
return fetch('/', { credentials: 'include', redirect: 'manual' });
|
||||||
}
|
}
|
||||||
|
|
||||||
precacheAndRoute(self.__WB_MANIFEST);
|
// precacheAndRoute(self.__WB_MANIFEST);
|
||||||
|
|
||||||
registerRoute(
|
registerRoute(
|
||||||
/locale_.*\.js$/,
|
/intl\/.*\.js$/,
|
||||||
new CacheFirst({
|
new CacheFirst({
|
||||||
cacheName: `${CACHE_NAME_PREFIX}locales`,
|
cacheName: `${CACHE_NAME_PREFIX}locales`,
|
||||||
plugins: [
|
plugins: [
|
|
@ -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));
|
|
|
@ -1,8 +1,10 @@
|
||||||
import { IntlMessageFormat } from 'intl-messageformat';
|
import { IntlMessageFormat } from 'intl-messageformat';
|
||||||
|
|
||||||
import { unescape } from 'lodash';
|
import { unescape } from 'lodash';
|
||||||
|
// see config/vite/plugins/sw-locales
|
||||||
import locales from './web_push_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 MAX_NOTIFICATIONS = 5;
|
||||||
const GROUP_TAG = 'tag';
|
const GROUP_TAG = 'tag';
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
export function isDevelopment() {
|
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() {
|
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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -393,7 +393,7 @@ code {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
border-radius: var(--avatar-border-radius);
|
border-radius: var(--avatar-border-radius);
|
||||||
background: url('images/void.png');
|
background: url('@/images/void.png');
|
||||||
|
|
||||||
&[src$='missing.png'] {
|
&[src$='missing.png'] {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
|
|
19
app/javascript/types/image.d.ts
vendored
19
app/javascript/types/image.d.ts
vendored
|
@ -1,3 +1,5 @@
|
||||||
|
/// <reference types="vite-plugin-svgr/client" />
|
||||||
|
|
||||||
/* eslint-disable import/no-default-export */
|
/* eslint-disable import/no-default-export */
|
||||||
declare module '*.avif' {
|
declare module '*.avif' {
|
||||||
const path: string;
|
const path: string;
|
||||||
|
@ -19,23 +21,6 @@ declare module '*.png' {
|
||||||
export default path;
|
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<SVGSVGElement> {
|
|
||||||
title?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ReactComponent: React.FC<SVGPropsWithTitle>;
|
|
||||||
|
|
||||||
export default ReactComponent;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module '*.webp' {
|
declare module '*.webp' {
|
||||||
const path: string;
|
const path: string;
|
||||||
export default path;
|
export default path;
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
- content_for :page_title do
|
- content_for :page_title do
|
||||||
= t('auth.login')
|
= t('auth.login')
|
||||||
|
|
||||||
= javascript_pack_tag 'two_factor_authentication', crossorigin: 'anonymous'
|
= vite_typescript_tag 'two_factor_authentication.ts', crossorigin: 'anonymous'
|
||||||
|
|
||||||
- if webauthn_enabled?
|
- if webauthn_enabled?
|
||||||
= render partial: 'auth/sessions/two_factor/webauthn_form', locals: { hidden: @scheme_type != 'webauthn' }
|
= render partial: 'auth/sessions/two_factor/webauthn_form', locals: { hidden: @scheme_type != 'webauthn' }
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
- content_for :page_title do
|
- content_for :page_title do
|
||||||
= t('auth.setup.title')
|
= 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|
|
= simple_form_for(@user, url: auth_setup_path) do |f|
|
||||||
= render 'auth/shared/progress', stage: 'confirm'
|
= render 'auth/shared/progress', stage: 'confirm'
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
- content_for :header_tags do
|
- content_for :header_tags do
|
||||||
= render_initial_state
|
= render_initial_state
|
||||||
= javascript_pack_tag 'public', crossorigin: 'anonymous'
|
= vite_typescript_tag 'public.tsx', crossorigin: 'anonymous'
|
||||||
= javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
|
= vite_typescript_tag 'admin.tsx', crossorigin: 'anonymous'
|
||||||
|
|
||||||
- content_for :body_classes, 'admin'
|
- content_for :body_classes, 'admin'
|
||||||
|
|
||||||
|
|
|
@ -26,11 +26,12 @@
|
||||||
%title= html_title
|
%title= html_title
|
||||||
|
|
||||||
= theme_style_tags current_theme
|
= 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 <style> tag, with this `id`
|
-# Needed for the wicg-inert polyfill. It needs to be on it's own <style> tag, with this `id`
|
||||||
= stylesheet_pack_tag 'inert', media: 'all', crossorigin: 'anonymous', id: 'inert-style'
|
= vite_stylesheet_tag 'styles/inert.scss', media: 'all', id: 'inert-style'
|
||||||
|
|
||||||
= javascript_pack_tag 'common', crossorigin: 'anonymous'
|
-# = preload_pack_asset "locale/#{I18n.locale}-json.js"
|
||||||
= preload_pack_asset "locale/#{I18n.locale}-json.js"
|
|
||||||
= csrf_meta_tags unless skip_csrf_meta_tags?
|
= csrf_meta_tags unless skip_csrf_meta_tags?
|
||||||
%meta{ name: 'style-nonce', content: request.content_security_policy_nonce }
|
%meta{ name: 'style-nonce', content: request.content_security_policy_nonce }
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
- content_for :header_tags do
|
- content_for :header_tags do
|
||||||
= javascript_pack_tag 'public', crossorigin: 'anonymous'
|
= vite_typescript_tag 'public.tsx', crossorigin: 'anonymous'
|
||||||
|
|
||||||
- content_for :content do
|
- content_for :content do
|
||||||
.container-alt
|
.container-alt
|
||||||
|
|
|
@ -11,11 +11,12 @@
|
||||||
- if storage_host?
|
- if storage_host?
|
||||||
%link{ rel: 'dns-prefetch', href: storage_host }/
|
%link{ rel: 'dns-prefetch', href: storage_host }/
|
||||||
|
|
||||||
|
= vite_client_tag
|
||||||
|
= vite_react_refresh_tag
|
||||||
= theme_style_tags 'mastodon-light'
|
= theme_style_tags 'mastodon-light'
|
||||||
= javascript_pack_tag 'common', integrity: true, crossorigin: 'anonymous'
|
-# = preload_pack_asset "locale/#{I18n.locale}-json.js"
|
||||||
= preload_pack_asset "locale/#{I18n.locale}-json.js"
|
|
||||||
= render_initial_state
|
= render_initial_state
|
||||||
= javascript_pack_tag 'embed', integrity: true, crossorigin: 'anonymous'
|
= vite_typescript_tag 'embed.tsx', integrity: true, crossorigin: 'anonymous'
|
||||||
%body.embed
|
%body.embed
|
||||||
= yield
|
= yield
|
||||||
|
|
||||||
|
|
|
@ -5,9 +5,10 @@
|
||||||
%meta{ charset: 'utf-8' }/
|
%meta{ charset: 'utf-8' }/
|
||||||
%title= safe_join([yield(:page_title), Setting.default_settings['site_title']], ' - ')
|
%title= safe_join([yield(:page_title), Setting.default_settings['site_title']], ' - ')
|
||||||
%meta{ content: 'width=device-width,initial-scale=1', name: 'viewport' }/
|
%meta{ content: 'width=device-width,initial-scale=1', name: 'viewport' }/
|
||||||
|
= vite_client_tag
|
||||||
|
= vite_react_refresh_tag
|
||||||
= theme_style_tags Setting.default_settings['theme']
|
= theme_style_tags Setting.default_settings['theme']
|
||||||
= javascript_pack_tag 'common', crossorigin: 'anonymous'
|
= vite_typescript_tag 'error.ts', crossorigin: 'anonymous'
|
||||||
= javascript_pack_tag 'error', crossorigin: 'anonymous'
|
|
||||||
%body.error
|
%body.error
|
||||||
.dialog
|
.dialog
|
||||||
.dialog__illustration
|
.dialog__illustration
|
||||||
|
|
|
@ -3,6 +3,4 @@
|
||||||
%head
|
%head
|
||||||
%meta{ charset: 'utf-8' }/
|
%meta{ charset: 'utf-8' }/
|
||||||
|
|
||||||
= javascript_pack_tag 'common', crossorigin: 'anonymous'
|
|
||||||
|
|
||||||
= yield :header_tags
|
= yield :header_tags
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||||
</o:OfficeDocumentSettings>
|
</o:OfficeDocumentSettings>
|
||||||
</xml>
|
</xml>
|
||||||
= stylesheet_pack_tag 'mailer'
|
= vite_stylesheet_tag 'styles/mailer.scss'
|
||||||
%body
|
%body
|
||||||
.email{ dir: locale_direction }
|
.email{ dir: locale_direction }
|
||||||
%table.email-w-full{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' }
|
%table.email-w-full{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' }
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
- content_for :header_tags do
|
- content_for :header_tags do
|
||||||
= javascript_pack_tag 'public', crossorigin: 'anonymous'
|
= vite_typescript_tag 'public.tsx', crossorigin: 'anonymous'
|
||||||
|
|
||||||
- content_for :body_classes, 'modal-layout compose-standalone'
|
- content_for :body_classes, 'modal-layout compose-standalone'
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
- content_for :header_tags do
|
- content_for :header_tags do
|
||||||
= render_initial_state
|
= render_initial_state
|
||||||
= javascript_pack_tag 'public', crossorigin: 'anonymous'
|
= vite_typescript_tag 'public.tsx', crossorigin: 'anonymous'
|
||||||
|
|
||||||
- content_for :body_classes, 'player'
|
- content_for :body_classes, 'player'
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
= t('settings.relationships')
|
= t('settings.relationships')
|
||||||
|
|
||||||
- content_for :header_tags do
|
- content_for :header_tags do
|
||||||
= javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
|
= vite_typescript_tag 'admin.tsx', crossorigin: 'anonymous'
|
||||||
|
|
||||||
.filters
|
.filters
|
||||||
.filter-subset
|
.filter-subset
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
- content_for :header_tags do
|
- content_for :header_tags do
|
||||||
%meta{ name: 'robots', content: 'noindex' }/
|
%meta{ name: 'robots', content: 'noindex' }/
|
||||||
|
|
||||||
= javascript_pack_tag 'remote_interaction_helper', crossorigin: 'anonymous'
|
= vite_typescript_tag 'remote_interaction_helper.ts', crossorigin: 'anonymous'
|
||||||
|
|
|
@ -13,4 +13,4 @@
|
||||||
.actions
|
.actions
|
||||||
= f.button :button, t('webauthn_credentials.add'), class: 'js-webauthn', type: :submit
|
= f.button :button, t('webauthn_credentials.add'), class: 'js-webauthn', type: :submit
|
||||||
|
|
||||||
= javascript_pack_tag 'two_factor_authentication', crossorigin: 'anonymous'
|
= vite_typescript_tag 'two_factor_authentication.ts', crossorigin: 'anonymous'
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
- content_for :body_classes, 'app-body'
|
- content_for :body_classes, 'app-body'
|
||||||
- content_for :header_tags do
|
- content_for :header_tags do
|
||||||
- if user_signed_in?
|
- if user_signed_in?
|
||||||
= preload_pack_asset 'features/compose.js'
|
-#
|
||||||
= preload_pack_asset 'features/home_timeline.js'
|
= preload_pack_asset 'features/compose.js'
|
||||||
= preload_pack_asset 'features/notifications.js'
|
= preload_pack_asset 'features/home_timeline.js'
|
||||||
|
= preload_pack_asset 'features/notifications.js'
|
||||||
%meta{ name: 'initialPath', content: request.path }
|
%meta{ name: 'initialPath', content: request.path }
|
||||||
|
|
||||||
%meta{ name: 'applicationServerKey', content: Rails.configuration.x.vapid_public_key }
|
%meta{ name: 'applicationServerKey', content: Rails.configuration.x.vapid_public_key }
|
||||||
|
|
||||||
= render_initial_state
|
= render_initial_state
|
||||||
= javascript_pack_tag 'application', crossorigin: 'anonymous'
|
= vite_typescript_tag 'application.ts', crossorigin: 'anonymous'
|
||||||
|
|
||||||
.notranslate.app-holder#mastodon{ data: { props: Oj.dump(default_props) } }
|
.notranslate.app-holder#mastodon{ data: { props: Oj.dump(default_props) } }
|
||||||
%noscript
|
%noscript
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
- content_for :header_tags do
|
- content_for :header_tags do
|
||||||
= render_initial_state
|
= render_initial_state
|
||||||
= javascript_pack_tag 'share', crossorigin: 'anonymous'
|
= vite_typescript_tag 'share.tsx', crossorigin: 'anonymous'
|
||||||
|
|
||||||
#mastodon-compose{ data: { props: Oj.dump(default_props) } }
|
#mastodon-compose{ data: { props: Oj.dump(default_props) } }
|
||||||
|
|
27
bin/vite
Executable file
27
bin/vite
Executable file
|
@ -0,0 +1,27 @@
|
||||||
|
#!/usr/bin/env ruby
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
#
|
||||||
|
# This file was generated by Bundler.
|
||||||
|
#
|
||||||
|
# The application 'vite' is installed as part of a gem, and
|
||||||
|
# this file is here to facilitate running it.
|
||||||
|
#
|
||||||
|
|
||||||
|
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
|
||||||
|
|
||||||
|
bundle_binstub = File.expand_path("bundle", __dir__)
|
||||||
|
|
||||||
|
if File.file?(bundle_binstub)
|
||||||
|
if File.read(bundle_binstub, 300).include?("This file was generated by Bundler")
|
||||||
|
load(bundle_binstub)
|
||||||
|
else
|
||||||
|
abort("Your `bin/bundle` was not generated by Bundler, so this binstub cannot run.
|
||||||
|
Replace `bin/bundle` by running `bundle binstubs bundler --force`, then run this command again.")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
require "rubygems"
|
||||||
|
require "bundler/setup"
|
||||||
|
|
||||||
|
load Gem.bin_path("vite_ruby", "vite")
|
|
@ -46,14 +46,13 @@ require_relative '../lib/chewy/settings_extensions'
|
||||||
require_relative '../lib/chewy/index_extensions'
|
require_relative '../lib/chewy/index_extensions'
|
||||||
require_relative '../lib/chewy/strategy/mastodon'
|
require_relative '../lib/chewy/strategy/mastodon'
|
||||||
require_relative '../lib/chewy/strategy/bypass_with_warning'
|
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/rails/engine_extensions'
|
||||||
require_relative '../lib/action_dispatch/remote_ip_extensions'
|
require_relative '../lib/action_dispatch/remote_ip_extensions'
|
||||||
require_relative '../lib/stoplight/redis_data_store_extensions'
|
require_relative '../lib/stoplight/redis_data_store_extensions'
|
||||||
require_relative '../lib/active_record/database_tasks_extensions'
|
require_relative '../lib/active_record/database_tasks_extensions'
|
||||||
require_relative '../lib/active_record/batches'
|
require_relative '../lib/active_record/batches'
|
||||||
require_relative '../lib/simple_navigation/item_extensions'
|
require_relative '../lib/simple_navigation/item_extensions'
|
||||||
|
require_relative '../lib/vite_ruby/sri_extensions'
|
||||||
|
|
||||||
Bundler.require(:pam_authentication) if ENV['PAM_ENABLED'] == 'true'
|
Bundler.require(:pam_authentication) if ENV['PAM_ENABLED'] == 'true'
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,6 @@ Rails.application.config.content_security_policy do |p|
|
||||||
p.frame_ancestors :none
|
p.frame_ancestors :none
|
||||||
p.font_src :self, assets_host
|
p.font_src :self, assets_host
|
||||||
p.img_src :self, :data, :blob, *media_hosts
|
p.img_src :self, :data, :blob, *media_hosts
|
||||||
p.style_src :self, assets_host
|
|
||||||
p.media_src :self, :data, *media_hosts
|
p.media_src :self, :data, *media_hosts
|
||||||
p.manifest_src :self, assets_host
|
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
|
p.worker_src :self, :blob, assets_host
|
||||||
|
|
||||||
if Rails.env.development?
|
if Rails.env.development?
|
||||||
webpacker_public_host = ENV.fetch('WEBPACKER_DEV_SERVER_PUBLIC', Webpacker.config.dev_server[:public])
|
# Hacky solution to force CSP to correctly allow localhost, even if ViteRuby is bound to 0.0.0.0.
|
||||||
front_end_build_urls = %w(ws http).map { |protocol| "#{protocol}#{Webpacker.dev_server.https? ? 's' : ''}://#{webpacker_public_host}" }
|
front_end_build_urls = %w(ws http).map { |protocol| "#{protocol}#{ViteRuby.config.https ? 's' : ''}://localhost:#{ViteRuby.config.port}" }
|
||||||
|
|
||||||
p.connect_src :self, :data, :blob, *media_hosts, Rails.configuration.x.streaming_api_base_url, *front_end_build_urls
|
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.script_src :self, :unsafe_inline, :unsafe_eval, assets_host
|
||||||
p.frame_src :self, :https, :http
|
p.frame_src :self, :https, :http
|
||||||
|
p.style_src :self, assets_host, :unsafe_inline
|
||||||
else
|
else
|
||||||
p.connect_src :self, :data, :blob, *media_hosts, Rails.configuration.x.streaming_api_base_url
|
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.script_src :self, assets_host, "'wasm-unsafe-eval'"
|
||||||
p.frame_src :self, :https
|
p.frame_src :self, :https
|
||||||
|
p.style_src :self, assets_host
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,20 @@
|
||||||
{
|
{
|
||||||
"all": {
|
"all": {
|
||||||
"sourceCodeDir": "app/javascript",
|
"sourceCodeDir": "app/javascript",
|
||||||
"additionalEntrypoints": ["~/{icons,images}/**/*", "~/styles/*.scss"],
|
"additionalEntrypoints": ["~/{fonts,icons,images}/**/*", "~/styles/*.scss"],
|
||||||
"watchAdditionalPaths": []
|
"watchAdditionalPaths": []
|
||||||
},
|
},
|
||||||
|
"production": {
|
||||||
|
"publicOutputDir": "packs"
|
||||||
|
},
|
||||||
"development": {
|
"development": {
|
||||||
"autoBuild": true,
|
"autoBuild": true,
|
||||||
"publicOutputDir": "vite-dev",
|
"publicOutputDir": "packs-dev",
|
||||||
"port": 3036
|
"port": 3036
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
"autoBuild": true,
|
"autoBuild": true,
|
||||||
"publicOutputDir": "vite-test",
|
"publicOutputDir": "packs-test",
|
||||||
"port": 3037
|
"port": 3037
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
82
config/vite/plugin-sw-locales.ts
Normal file
82
config/vite/plugin-sw-locales.ts
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
/* 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 type { Plugin, ResolvedConfig } from 'vite';
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
|
||||||
|
filteredLocales[locale] = filteredLocale;
|
||||||
|
});
|
||||||
|
|
||||||
|
return `const locales = ${JSON.stringify(filteredLocales)}; \n export default locales;`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -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
|
|
|
@ -2,21 +2,26 @@
|
||||||
|
|
||||||
module PremailerBundledAssetStrategy
|
module PremailerBundledAssetStrategy
|
||||||
def load(url)
|
def load(url)
|
||||||
asset_host = ENV['CDN_HOST'] || ENV['WEB_DOMAIN'] || ENV.fetch('LOCAL_DOMAIN', nil)
|
if ViteRuby.instance.dev_server_running?
|
||||||
|
# Request from the dev server
|
||||||
|
return unless url.start_with?("/#{ViteRuby.config.public_output_dir}/")
|
||||||
|
|
||||||
if Webpacker.dev_server.running?
|
headers = {}
|
||||||
asset_host = "#{Webpacker.dev_server.protocol}://#{Webpacker.dev_server.host_with_port}"
|
# Vite dev server wants this header for CSS files, otherwise it will respond with a JS file that inserts the CSS (to support hot reloading)
|
||||||
url = File.join(asset_host, url)
|
headers['Accept'] = 'text/css' if url.end_with?('.scss', '.css')
|
||||||
|
|
||||||
|
Net::HTTP.get(
|
||||||
|
URI("#{ViteRuby.config.origin}#{url}"),
|
||||||
|
headers
|
||||||
|
).presence
|
||||||
|
else
|
||||||
|
path = Rails.public_path.join(url.delete_prefix('/'))
|
||||||
|
return unless path.exist?
|
||||||
|
|
||||||
|
path.read
|
||||||
end
|
end
|
||||||
|
rescue ViteRuby::MissingEntrypointError
|
||||||
css = if url.start_with?('http')
|
# If the path is not in the manifest, ignore it
|
||||||
HTTP.get(url).to_s
|
|
||||||
else
|
|
||||||
url = url[1..] if url.start_with?('/')
|
|
||||||
Rails.public_path.join(url).read
|
|
||||||
end
|
|
||||||
|
|
||||||
css.gsub(%r{url\(/}, "url(#{asset_host}/")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
module_function :load
|
module_function :load
|
||||||
|
|
|
@ -14,7 +14,9 @@ end
|
||||||
|
|
||||||
if Rake::Task.task_defined?('assets:precompile')
|
if Rake::Task.task_defined?('assets:precompile')
|
||||||
Rake::Task['assets:precompile'].enhance do
|
Rake::Task['assets:precompile'].enhance do
|
||||||
Webpacker.manifest.refresh
|
|
||||||
Rake::Task['assets:generate_static_pages'].invoke
|
Rake::Task['assets:generate_static_pages'].invoke
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# We don't want vite_ruby to run yarn, we do that in a separate step
|
||||||
|
Rake::Task['vite:install_dependencies'].clear
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
# Disable this task as we use pnpm
|
|
||||||
|
|
||||||
require 'semantic_range'
|
|
||||||
|
|
||||||
Rake::Task['webpacker:check_yarn'].clear
|
|
||||||
|
|
||||||
namespace :webpacker do
|
|
||||||
desc 'Verifies if Yarn is installed'
|
|
||||||
task check_yarn: :environment do
|
|
||||||
begin
|
|
||||||
yarn_version = `yarn --version`.strip
|
|
||||||
raise Errno::ENOENT if yarn_version.blank?
|
|
||||||
|
|
||||||
yarn_range = '>=4 <5'
|
|
||||||
is_valid = begin
|
|
||||||
SemanticRange.satisfies?(yarn_version, yarn_range)
|
|
||||||
rescue
|
|
||||||
false
|
|
||||||
end
|
|
||||||
|
|
||||||
unless is_valid
|
|
||||||
warn "Mastodon and Webpacker requires Yarn \"#{yarn_range}\" and you are using #{yarn_version}"
|
|
||||||
warn 'Exiting!'
|
|
||||||
exit!
|
|
||||||
end
|
|
||||||
rescue Errno::ENOENT
|
|
||||||
warn 'Yarn not installed. Please see the Mastodon documentation to install the correct version.'
|
|
||||||
warn 'Exiting!'
|
|
||||||
exit!
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
100
lib/vite_ruby/sri_extensions.rb
Normal file
100
lib/vite_ruby/sri_extensions.rb
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ViteRuby::ManifestIntegrityExtension
|
||||||
|
def path_and_integrity_for(name, **)
|
||||||
|
entry = lookup!(name, **)
|
||||||
|
|
||||||
|
{ path: entry.fetch('file'), integrity: entry.fetch('integrity', nil) }
|
||||||
|
end
|
||||||
|
|
||||||
|
# Find a manifest entry by the *final* file name
|
||||||
|
def integrity_hash_for_file(file_name)
|
||||||
|
@integrity_cache ||= {}
|
||||||
|
@integrity_cache[file_name] ||= begin
|
||||||
|
entry = manifest.find { |_key, entry| entry['file'] == file_name }
|
||||||
|
|
||||||
|
entry[1].fetch('integrity', nil) if entry
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def resolve_entries_with_integrity(*names, **options)
|
||||||
|
entries = names.map { |name| lookup!(name, **options) }
|
||||||
|
script_paths = entries.map do |entry|
|
||||||
|
{
|
||||||
|
file: entry.fetch('file'),
|
||||||
|
# TODO: Secure this so we require the integrity hash outside of dev
|
||||||
|
integrity: entry['integrity'],
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
imports = dev_server_running? ? [] : entries.flat_map { |entry| entry['imports'] }.compact
|
||||||
|
|
||||||
|
{
|
||||||
|
scripts: script_paths,
|
||||||
|
imports: imports.filter_map { |entry| { file: entry.fetch('file'), integrity: entry.fetch('integrity') } }.uniq,
|
||||||
|
stylesheets: dev_server_running? ? [] : (entries + imports).flat_map { |entry| entry['css'] }.compact.uniq,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
ViteRuby::Manifest.prepend ViteRuby::ManifestIntegrityExtension
|
||||||
|
|
||||||
|
module ViteRails::TagHelpers::IntegrityExtension
|
||||||
|
def vite_javascript_tag(*names,
|
||||||
|
type: 'module',
|
||||||
|
asset_type: :javascript,
|
||||||
|
skip_preload_tags: false,
|
||||||
|
skip_style_tags: false,
|
||||||
|
crossorigin: 'anonymous',
|
||||||
|
media: 'screen',
|
||||||
|
**options)
|
||||||
|
entries = vite_manifest.resolve_entries_with_integrity(*names, type: asset_type)
|
||||||
|
|
||||||
|
''.html_safe.tap do |tags|
|
||||||
|
entries.fetch(:scripts).each do |script|
|
||||||
|
tags << javascript_include_tag(
|
||||||
|
script[:file],
|
||||||
|
integrity: script[:integrity],
|
||||||
|
crossorigin: crossorigin,
|
||||||
|
type: type,
|
||||||
|
extname: false,
|
||||||
|
**options
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
unless skip_preload_tags
|
||||||
|
entries.fetch(:imports).each do |import|
|
||||||
|
tags << vite_preload_tag(import[:file], integrity: import[:integrity], crossorigin: crossorigin, **options)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
options[:extname] = false if Rails::VERSION::MAJOR >= 7
|
||||||
|
|
||||||
|
unless skip_style_tags
|
||||||
|
entries.fetch(:stylesheets).each do |stylesheet|
|
||||||
|
# This is for stylesheets imported from Javascript. The entry for the JS entrypoint only contains the final CSS file name, so we need to look it up in the manifest
|
||||||
|
tags << stylesheet_link_tag(
|
||||||
|
stylesheet,
|
||||||
|
integrity: vite_manifest.integrity_hash_for_file(stylesheet),
|
||||||
|
media: media,
|
||||||
|
**options
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def vite_stylesheet_tag(*names, **options)
|
||||||
|
''.html_safe.tap do |tags|
|
||||||
|
names.each do |name|
|
||||||
|
entry = vite_manifest.path_and_integrity_for(name, type: :stylesheet)
|
||||||
|
|
||||||
|
options[:extname] = false if Rails::VERSION::MAJOR >= 7
|
||||||
|
|
||||||
|
tags << stylesheet_link_tag(entry[:path], integrity: entry[:integrity], **options)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
ViteRails::TagHelpers.prepend ViteRails::TagHelpers::IntegrityExtension
|
|
@ -1,27 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Webpacker::HelperExtensions
|
|
||||||
def javascript_pack_tag(name, **options)
|
|
||||||
src, integrity = current_webpacker_instance.manifest.lookup!(name, type: :javascript, with_integrity: true)
|
|
||||||
javascript_include_tag(src, options.merge(integrity: integrity))
|
|
||||||
end
|
|
||||||
|
|
||||||
def stylesheet_pack_tag(name, **options)
|
|
||||||
src, integrity = current_webpacker_instance.manifest.lookup!(name, type: :stylesheet, with_integrity: true)
|
|
||||||
stylesheet_link_tag(src, options.merge(integrity: integrity))
|
|
||||||
end
|
|
||||||
|
|
||||||
def preload_pack_asset(name, **options)
|
|
||||||
src, integrity = current_webpacker_instance.manifest.lookup!(name, with_integrity: true)
|
|
||||||
|
|
||||||
# This attribute will only work if the assets are on a different domain.
|
|
||||||
# And Webpack will (correctly) only add it in this case, so we need to conditionally set it here
|
|
||||||
# otherwise the preloaded request and the real request will have different crossorigin values
|
|
||||||
# and the preloaded file wont be loaded
|
|
||||||
crossorigin = 'anonymous' if Rails.configuration.action_controller.asset_host.present?
|
|
||||||
|
|
||||||
preload_link_tag(src, options.merge(integrity: integrity, crossorigin: crossorigin))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
Webpacker::Helper.prepend(Webpacker::HelperExtensions)
|
|
|
@ -1,17 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
module Webpacker::ManifestExtensions
|
|
||||||
def lookup(name, pack_type = {})
|
|
||||||
asset = super
|
|
||||||
|
|
||||||
if pack_type[:with_integrity] && asset.respond_to?(:dig)
|
|
||||||
[asset['src'], asset['integrity']]
|
|
||||||
elsif asset.respond_to?(:dig)
|
|
||||||
asset['src']
|
|
||||||
else
|
|
||||||
asset
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
Webpacker::Manifest.prepend(Webpacker::ManifestExtensions)
|
|
22
package.json
22
package.json
|
@ -11,8 +11,8 @@
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
"build:development": "cross-env RAILS_ENV=development NODE_ENV=development ./bin/vite build",
|
"build:development": "cross-env RAILS_ENV=development NODE_ENV=development vite build",
|
||||||
"build:production": "cross-env RAILS_ENV=production NODE_ENV=production ./bin/vite build",
|
"build:production": "cross-env RAILS_ENV=production NODE_ENV=production vite build",
|
||||||
"fix:js": "eslint . --cache --fix",
|
"fix:js": "eslint . --cache --fix",
|
||||||
"fix:css": "stylelint --fix \"**/*.{css,scss}\"",
|
"fix:css": "stylelint --fix \"**/*.{css,scss}\"",
|
||||||
"fix": "yarn fix:js && yarn fix:css",
|
"fix": "yarn fix:js && yarn fix:css",
|
||||||
|
@ -42,13 +42,18 @@
|
||||||
"@formatjs/intl-pluralrules": "^5.4.4",
|
"@formatjs/intl-pluralrules": "^5.4.4",
|
||||||
"@gamestdio/websocket": "^0.3.2",
|
"@gamestdio/websocket": "^0.3.2",
|
||||||
"@github/webauthn-json": "^2.1.1",
|
"@github/webauthn-json": "^2.1.1",
|
||||||
|
"@optimize-lodash/rollup-plugin": "^5.0.2",
|
||||||
"@rails/ujs": "7.1.501",
|
"@rails/ujs": "7.1.501",
|
||||||
"@react-spring/web": "^9.7.5",
|
"@react-spring/web": "^9.7.5",
|
||||||
"@reduxjs/toolkit": "^2.0.1",
|
"@reduxjs/toolkit": "^2.0.1",
|
||||||
"@use-gesture/react": "^10.3.1",
|
"@use-gesture/react": "^10.3.1",
|
||||||
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"arrow-key-navigation": "^1.2.0",
|
"arrow-key-navigation": "^1.2.0",
|
||||||
"async-mutex": "^0.5.0",
|
"async-mutex": "^0.5.0",
|
||||||
"axios": "^1.4.0",
|
"axios": "^1.4.0",
|
||||||
|
"babel-plugin-formatjs": "^10.5.37",
|
||||||
|
"babel-plugin-preval": "^5.1.0",
|
||||||
|
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
|
||||||
"blurhash": "^2.0.5",
|
"blurhash": "^2.0.5",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.3.2",
|
||||||
"color-blend": "^4.0.0",
|
"color-blend": "^4.0.0",
|
||||||
|
@ -101,10 +106,13 @@
|
||||||
"tiny-queue": "^0.2.1",
|
"tiny-queue": "^0.2.1",
|
||||||
"twitter-text": "3.1.0",
|
"twitter-text": "3.1.0",
|
||||||
"use-debounce": "^10.0.0",
|
"use-debounce": "^10.0.0",
|
||||||
"vite": "^6.2.6",
|
"vite": "^6.3.0",
|
||||||
|
"vite-bundle-analyzer": "^0.18.1",
|
||||||
|
"vite-plugin-pwa": "^1.0.0",
|
||||||
|
"vite-plugin-rails": "^0.5.0",
|
||||||
|
"vite-plugin-svgr": "^4.3.0",
|
||||||
"wicg-inert": "^3.1.2",
|
"wicg-inert": "^3.1.2",
|
||||||
"workbox-expiration": "^7.0.0",
|
"workbox-expiration": "^7.0.0",
|
||||||
"workbox-precaching": "^7.0.0",
|
|
||||||
"workbox-routing": "^7.0.0",
|
"workbox-routing": "^7.0.0",
|
||||||
"workbox-strategies": "^7.0.0",
|
"workbox-strategies": "^7.0.0",
|
||||||
"workbox-window": "^7.0.0"
|
"workbox-window": "^7.0.0"
|
||||||
|
@ -113,7 +121,6 @@
|
||||||
"@eslint/js": "^9.23.0",
|
"@eslint/js": "^9.23.0",
|
||||||
"@formatjs/cli": "^6.1.1",
|
"@formatjs/cli": "^6.1.1",
|
||||||
"@testing-library/dom": "^10.2.0",
|
"@testing-library/dom": "^10.2.0",
|
||||||
"@testing-library/jest-dom": "^6.0.0",
|
|
||||||
"@testing-library/react": "^16.0.0",
|
"@testing-library/react": "^16.0.0",
|
||||||
"@types/emoji-mart": "3.0.14",
|
"@types/emoji-mart": "3.0.14",
|
||||||
"@types/escape-html": "^1.0.2",
|
"@types/escape-html": "^1.0.2",
|
||||||
|
@ -138,7 +145,6 @@
|
||||||
"@types/react-toggle": "^4.0.3",
|
"@types/react-toggle": "^4.0.3",
|
||||||
"@types/redux-immutable": "^4.0.3",
|
"@types/redux-immutable": "^4.0.3",
|
||||||
"@types/requestidlecallback": "^0.3.5",
|
"@types/requestidlecallback": "^0.3.5",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
|
||||||
"eslint": "^9.23.0",
|
"eslint": "^9.23.0",
|
||||||
"eslint-import-resolver-typescript": "^4.2.5",
|
"eslint-import-resolver-typescript": "^4.2.5",
|
||||||
"eslint-plugin-formatjs": "^5.3.1",
|
"eslint-plugin-formatjs": "^5.3.1",
|
||||||
|
@ -158,9 +164,7 @@
|
||||||
"stylelint-config-standard-scss": "^14.0.0",
|
"stylelint-config-standard-scss": "^14.0.0",
|
||||||
"typescript": "~5.7.3",
|
"typescript": "~5.7.3",
|
||||||
"typescript-eslint": "^8.29.1",
|
"typescript-eslint": "^8.29.1",
|
||||||
"vite-plugin-rails": "^0.5.0",
|
"vitest": "^3.1.2"
|
||||||
"vite-plugin-svgr": "^4.2.0",
|
|
||||||
"vitest": "^3.1.1"
|
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@types/react": "^18.2.7",
|
"@types/react": "^18.2.7",
|
||||||
|
|
|
@ -17,7 +17,7 @@ RSpec.describe ThemeHelper do
|
||||||
)
|
)
|
||||||
expect(html_links.last.attributes.symbolize_keys)
|
expect(html_links.last.attributes.symbolize_keys)
|
||||||
.to include(
|
.to include(
|
||||||
href: have_attributes(value: match(/default/)),
|
href: have_attributes(value: match(/application/)),
|
||||||
media: have_attributes(value: '(prefers-color-scheme: dark)')
|
media: have_attributes(value: '(prefers-color-scheme: dark)')
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,22 +1,23 @@
|
||||||
/// <reference types="vitest" />
|
|
||||||
|
|
||||||
import path from 'node:path';
|
|
||||||
import fs from 'node:fs/promises';
|
import fs from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
import { optimizeLodashImports } from '@optimize-lodash/rollup-plugin';
|
||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
|
import { PluginOption } from 'vite';
|
||||||
import svgr from 'vite-plugin-svgr';
|
import svgr from 'vite-plugin-svgr';
|
||||||
import {
|
import { analyzer } from 'vite-bundle-analyzer';
|
||||||
defineConfig,
|
import RailsPlugin from 'vite-plugin-rails';
|
||||||
configDefaults,
|
import { VitePWA } from 'vite-plugin-pwa';
|
||||||
UserConfigFnPromise,
|
|
||||||
} from 'vitest/config';
|
import { defineConfig, UserConfigFnPromise, UserConfig } from 'vite';
|
||||||
import postcssPresetEnv from 'postcss-preset-env';
|
import postcssPresetEnv from 'postcss-preset-env';
|
||||||
|
|
||||||
import { manifestSRI } from './config/vite/plugin-manifest-sri';
|
import { MastodonServiceWorkerLocales } from './config/vite/plugin-sw-locales';
|
||||||
|
|
||||||
const entrypointRoot = path.resolve(__dirname, 'app/javascript/entrypoints');
|
const jsRoot = path.resolve(__dirname, 'app/javascript');
|
||||||
|
const entrypointRoot = path.resolve(jsRoot, 'entrypoints');
|
||||||
|
|
||||||
const config: UserConfigFnPromise = async () => {
|
export const config: UserConfigFnPromise = async ({ mode, command }) => {
|
||||||
const entrypointFiles = await fs.readdir(entrypointRoot);
|
const entrypointFiles = await fs.readdir(entrypointRoot);
|
||||||
const entrypoints: Record<string, string> = entrypointFiles.reduce(
|
const entrypoints: Record<string, string> = entrypointFiles.reduce(
|
||||||
(acc, file) => {
|
(acc, file) => {
|
||||||
|
@ -27,7 +28,7 @@ const config: UserConfigFnPromise = async () => {
|
||||||
{} as Record<string, string>,
|
{} as Record<string, string>,
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
root: path.resolve(__dirname, 'app/javascript'),
|
root: jsRoot,
|
||||||
css: {
|
css: {
|
||||||
postcss: {
|
postcss: {
|
||||||
plugins: [
|
plugins: [
|
||||||
|
@ -41,15 +42,22 @@ const config: UserConfigFnPromise = async () => {
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
mastodon: path.resolve(__dirname, 'app/javascript/mastodon'),
|
mastodon: path.resolve(jsRoot, 'mastodon'),
|
||||||
'@': path.resolve(__dirname, 'app/javascript'),
|
'@': jsRoot,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
headers: {
|
||||||
|
// This is needed in dev environment because we load the worker from `/dev-sw/dev-sw.js`,
|
||||||
|
// but it needs to be scoped to the whole domain
|
||||||
|
'Service-Worker-Allowed': '/',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
commonjsOptions: { transformMixedEsModules: true },
|
commonjsOptions: { transformMixedEsModules: true },
|
||||||
outDir: path.resolve(__dirname, '.dist'),
|
chunkSizeWarningLimit: 1 * 1024 * 1024, // 1MB
|
||||||
emptyOutDir: true,
|
|
||||||
manifest: 'manifest.json',
|
manifest: 'manifest.json',
|
||||||
|
sourcemap: true,
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: entrypoints,
|
input: entrypoints,
|
||||||
output: {
|
output: {
|
||||||
|
@ -90,25 +98,46 @@ const config: UserConfigFnPromise = async () => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [react(), svgr(), manifestSRI()],
|
plugins: [
|
||||||
test: {
|
RailsPlugin({
|
||||||
environment: 'jsdom',
|
compress: mode !== 'production' && command === 'build',
|
||||||
include: [
|
}),
|
||||||
...configDefaults.include,
|
react({
|
||||||
'**/__tests__/**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
|
babel: {
|
||||||
],
|
plugins: ['formatjs', 'transform-react-remove-prop-types'],
|
||||||
exclude: [
|
},
|
||||||
...configDefaults.exclude,
|
}),
|
||||||
'**/node_modules/**',
|
MastodonServiceWorkerLocales(),
|
||||||
'vendor/**',
|
VitePWA({
|
||||||
'config/**',
|
srcDir: 'mastodon/service_worker',
|
||||||
'log/**',
|
// We need to use injectManifest because we use our own service worker
|
||||||
'public/**',
|
strategies: 'injectManifest',
|
||||||
'tmp/**',
|
manifest: false,
|
||||||
],
|
injectRegister: false,
|
||||||
globals: true,
|
injectManifest: {
|
||||||
},
|
// Do not inject a manifest, we dont use precache
|
||||||
};
|
injectionPoint: undefined,
|
||||||
|
buildPlugins: {
|
||||||
|
vite: [
|
||||||
|
// Provide a virtual import with only the locales used in the ServiceWorker
|
||||||
|
MastodonServiceWorkerLocales(),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
// Force the output location, because we have a symlink in `public/sw.js`
|
||||||
|
},
|
||||||
|
outDir: path.resolve(__dirname, 'public/packs'),
|
||||||
|
devOptions: {
|
||||||
|
enabled: true,
|
||||||
|
type: 'module',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
svgr(),
|
||||||
|
// manifestSRI(),
|
||||||
|
// Old library types need to be converted
|
||||||
|
optimizeLodashImports() as PluginOption,
|
||||||
|
!!process.env.ANALYZE_BUNDLE_SIZE && analyzer({ analyzerMode: 'static' }),
|
||||||
|
],
|
||||||
|
} satisfies UserConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default defineConfig(config);
|
export default defineConfig(config);
|
||||||
|
|
26
vitest.config.mts
Normal file
26
vitest.config.mts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { configDefaults, defineConfig } from 'vitest/config';
|
||||||
|
|
||||||
|
import { config as viteConfig } from './vite.config.mjs';
|
||||||
|
|
||||||
|
export default defineConfig(async (context) => {
|
||||||
|
return {
|
||||||
|
...(await viteConfig(context)),
|
||||||
|
test: {
|
||||||
|
environment: 'jsdom',
|
||||||
|
include: [
|
||||||
|
...configDefaults.include,
|
||||||
|
'**/__tests__/**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
|
||||||
|
],
|
||||||
|
exclude: [
|
||||||
|
...configDefaults.exclude,
|
||||||
|
'**/node_modules/**',
|
||||||
|
'vendor/**',
|
||||||
|
'config/**',
|
||||||
|
'log/**',
|
||||||
|
'public/**',
|
||||||
|
'tmp/**',
|
||||||
|
],
|
||||||
|
globals: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user