mirror of
https://github.com/mastodon/mastodon.git
synced 2025-05-07 12:16:14 +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:
|
||||
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
|
||||
|
|
4
.github/workflows/test-ruby.yml
vendored
4
.github/workflows/test-ruby.yml
vendored
|
@ -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'
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
3
Gemfile
3
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'
|
||||
|
|
18
Gemfile.lock
18
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)
|
||||
|
||||
|
|
|
@ -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, **)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
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 { 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';
|
||||
|
|
|
@ -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<ModalRef, Props & Partial<RestoreProps>>(
|
|||
|
||||
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<ModalRef, Props & Partial<RestoreProps>>(
|
|||
);
|
||||
},
|
||||
);
|
||||
|
||||
AltTextModal.displayName = 'AltTextModal';
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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<void>}
|
||||
*/
|
||||
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<string, unknown>;
|
||||
|
||||
const root = createRoot(mountNode);
|
||||
root.render(<Mastodon {...props} />);
|
||||
|
@ -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;
|
|
@ -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: [
|
|
@ -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 { 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';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
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 */
|
||||
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<SVGSVGElement> {
|
||||
title?: string;
|
||||
}
|
||||
|
||||
const ReactComponent: React.FC<SVGPropsWithTitle>;
|
||||
|
||||
export default ReactComponent;
|
||||
}
|
||||
|
||||
declare module '*.webp' {
|
||||
const path: string;
|
||||
export default path;
|
||||
|
|
|
@ -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' }
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
||||
|
|
|
@ -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 <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?
|
||||
%meta{ name: 'style-nonce', content: request.content_security_policy_nonce }
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
- content_for :header_tags do
|
||||
= javascript_pack_tag 'public', crossorigin: 'anonymous'
|
||||
= vite_typescript_tag 'public.tsx', crossorigin: 'anonymous'
|
||||
|
||||
- content_for :content do
|
||||
.container-alt
|
||||
|
|
|
@ -11,11 +11,12 @@
|
|||
- if storage_host?
|
||||
%link{ rel: 'dns-prefetch', href: storage_host }/
|
||||
|
||||
= vite_client_tag
|
||||
= vite_react_refresh_tag
|
||||
= 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
|
||||
= javascript_pack_tag 'embed', integrity: true, crossorigin: 'anonymous'
|
||||
= vite_typescript_tag 'embed.tsx', integrity: true, crossorigin: 'anonymous'
|
||||
%body.embed
|
||||
= yield
|
||||
|
||||
|
|
|
@ -5,9 +5,10 @@
|
|||
%meta{ charset: 'utf-8' }/
|
||||
%title= safe_join([yield(:page_title), Setting.default_settings['site_title']], ' - ')
|
||||
%meta{ content: 'width=device-width,initial-scale=1', name: 'viewport' }/
|
||||
= vite_client_tag
|
||||
= vite_react_refresh_tag
|
||||
= theme_style_tags Setting.default_settings['theme']
|
||||
= javascript_pack_tag 'common', crossorigin: 'anonymous'
|
||||
= javascript_pack_tag 'error', crossorigin: 'anonymous'
|
||||
= vite_typescript_tag 'error.ts', crossorigin: 'anonymous'
|
||||
%body.error
|
||||
.dialog
|
||||
.dialog__illustration
|
||||
|
|
|
@ -3,6 +3,4 @@
|
|||
%head
|
||||
%meta{ charset: 'utf-8' }/
|
||||
|
||||
= javascript_pack_tag 'common', crossorigin: 'anonymous'
|
||||
|
||||
= yield :header_tags
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
= stylesheet_pack_tag 'mailer'
|
||||
= vite_stylesheet_tag 'styles/mailer.scss'
|
||||
%body
|
||||
.email{ dir: locale_direction }
|
||||
%table.email-w-full{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' }
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
- 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'
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
- content_for :header_tags do
|
||||
= render_initial_state
|
||||
= javascript_pack_tag 'public', crossorigin: 'anonymous'
|
||||
= vite_typescript_tag 'public.tsx', crossorigin: 'anonymous'
|
||||
|
||||
- content_for :body_classes, 'player'
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
= t('settings.relationships')
|
||||
|
||||
- content_for :header_tags do
|
||||
= javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
|
||||
= vite_typescript_tag 'admin.tsx', crossorigin: 'anonymous'
|
||||
|
||||
.filters
|
||||
.filter-subset
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
- content_for :header_tags do
|
||||
%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
|
||||
= 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 :header_tags do
|
||||
- if user_signed_in?
|
||||
= preload_pack_asset 'features/compose.js'
|
||||
= preload_pack_asset 'features/home_timeline.js'
|
||||
= preload_pack_asset 'features/notifications.js'
|
||||
-#
|
||||
= preload_pack_asset 'features/compose.js'
|
||||
= preload_pack_asset 'features/home_timeline.js'
|
||||
= preload_pack_asset 'features/notifications.js'
|
||||
%meta{ name: 'initialPath', content: request.path }
|
||||
|
||||
%meta{ name: 'applicationServerKey', content: Rails.configuration.x.vapid_public_key }
|
||||
|
||||
= 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) } }
|
||||
%noscript
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
- content_for :header_tags do
|
||||
= render_initial_state
|
||||
= javascript_pack_tag 'share', crossorigin: 'anonymous'
|
||||
= vite_typescript_tag 'share.tsx', crossorigin: 'anonymous'
|
||||
|
||||
#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/strategy/mastodon'
|
||||
require_relative '../lib/chewy/strategy/bypass_with_warning'
|
||||
require_relative '../lib/webpacker/manifest_extensions'
|
||||
require_relative '../lib/webpacker/helper_extensions'
|
||||
require_relative '../lib/rails/engine_extensions'
|
||||
require_relative '../lib/action_dispatch/remote_ip_extensions'
|
||||
require_relative '../lib/stoplight/redis_data_store_extensions'
|
||||
require_relative '../lib/active_record/database_tasks_extensions'
|
||||
require_relative '../lib/active_record/batches'
|
||||
require_relative '../lib/simple_navigation/item_extensions'
|
||||
require_relative '../lib/vite_ruby/sri_extensions'
|
||||
|
||||
Bundler.require(:pam_authentication) if ENV['PAM_ENABLED'] == 'true'
|
||||
|
||||
|
|
|
@ -18,7 +18,6 @@ Rails.application.config.content_security_policy do |p|
|
|||
p.frame_ancestors :none
|
||||
p.font_src :self, assets_host
|
||||
p.img_src :self, :data, :blob, *media_hosts
|
||||
p.style_src :self, assets_host
|
||||
p.media_src :self, :data, *media_hosts
|
||||
p.manifest_src :self, assets_host
|
||||
|
||||
|
@ -32,16 +31,18 @@ Rails.application.config.content_security_policy do |p|
|
|||
p.worker_src :self, :blob, assets_host
|
||||
|
||||
if Rails.env.development?
|
||||
webpacker_public_host = ENV.fetch('WEBPACKER_DEV_SERVER_PUBLIC', Webpacker.config.dev_server[:public])
|
||||
front_end_build_urls = %w(ws http).map { |protocol| "#{protocol}#{Webpacker.dev_server.https? ? 's' : ''}://#{webpacker_public_host}" }
|
||||
# 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}#{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.script_src :self, :unsafe_inline, :unsafe_eval, assets_host
|
||||
p.frame_src :self, :https, :http
|
||||
p.style_src :self, assets_host, :unsafe_inline
|
||||
else
|
||||
p.connect_src :self, :data, :blob, *media_hosts, Rails.configuration.x.streaming_api_base_url
|
||||
p.script_src :self, assets_host, "'wasm-unsafe-eval'"
|
||||
p.frame_src :self, :https
|
||||
p.style_src :self, assets_host
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,17 +1,20 @@
|
|||
{
|
||||
"all": {
|
||||
"sourceCodeDir": "app/javascript",
|
||||
"additionalEntrypoints": ["~/{icons,images}/**/*", "~/styles/*.scss"],
|
||||
"additionalEntrypoints": ["~/{fonts,icons,images}/**/*", "~/styles/*.scss"],
|
||||
"watchAdditionalPaths": []
|
||||
},
|
||||
"production": {
|
||||
"publicOutputDir": "packs"
|
||||
},
|
||||
"development": {
|
||||
"autoBuild": true,
|
||||
"publicOutputDir": "vite-dev",
|
||||
"publicOutputDir": "packs-dev",
|
||||
"port": 3036
|
||||
},
|
||||
"test": {
|
||||
"autoBuild": true,
|
||||
"publicOutputDir": "vite-test",
|
||||
"publicOutputDir": "packs-test",
|
||||
"port": 3037
|
||||
}
|
||||
}
|
||||
|
|
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
|
||||
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?
|
||||
asset_host = "#{Webpacker.dev_server.protocol}://#{Webpacker.dev_server.host_with_port}"
|
||||
url = File.join(asset_host, url)
|
||||
headers = {}
|
||||
# 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)
|
||||
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
|
||||
|
||||
css = if url.start_with?('http')
|
||||
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}/")
|
||||
rescue ViteRuby::MissingEntrypointError
|
||||
# If the path is not in the manifest, ignore it
|
||||
end
|
||||
|
||||
module_function :load
|
||||
|
|
|
@ -14,7 +14,9 @@ end
|
|||
|
||||
if Rake::Task.task_defined?('assets:precompile')
|
||||
Rake::Task['assets:precompile'].enhance do
|
||||
Webpacker.manifest.refresh
|
||||
Rake::Task['assets:generate_static_pages'].invoke
|
||||
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": {
|
||||
"dev": "vite dev",
|
||||
"build:development": "cross-env RAILS_ENV=development NODE_ENV=development ./bin/vite build",
|
||||
"build:production": "cross-env RAILS_ENV=production NODE_ENV=production ./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 vite build",
|
||||
"fix:js": "eslint . --cache --fix",
|
||||
"fix:css": "stylelint --fix \"**/*.{css,scss}\"",
|
||||
"fix": "yarn fix:js && yarn fix:css",
|
||||
|
@ -42,13 +42,18 @@
|
|||
"@formatjs/intl-pluralrules": "^5.4.4",
|
||||
"@gamestdio/websocket": "^0.3.2",
|
||||
"@github/webauthn-json": "^2.1.1",
|
||||
"@optimize-lodash/rollup-plugin": "^5.0.2",
|
||||
"@rails/ujs": "7.1.501",
|
||||
"@react-spring/web": "^9.7.5",
|
||||
"@reduxjs/toolkit": "^2.0.1",
|
||||
"@use-gesture/react": "^10.3.1",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"arrow-key-navigation": "^1.2.0",
|
||||
"async-mutex": "^0.5.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",
|
||||
"classnames": "^2.3.2",
|
||||
"color-blend": "^4.0.0",
|
||||
|
@ -101,10 +106,13 @@
|
|||
"tiny-queue": "^0.2.1",
|
||||
"twitter-text": "3.1.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",
|
||||
"workbox-expiration": "^7.0.0",
|
||||
"workbox-precaching": "^7.0.0",
|
||||
"workbox-routing": "^7.0.0",
|
||||
"workbox-strategies": "^7.0.0",
|
||||
"workbox-window": "^7.0.0"
|
||||
|
@ -113,7 +121,6 @@
|
|||
"@eslint/js": "^9.23.0",
|
||||
"@formatjs/cli": "^6.1.1",
|
||||
"@testing-library/dom": "^10.2.0",
|
||||
"@testing-library/jest-dom": "^6.0.0",
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@types/emoji-mart": "3.0.14",
|
||||
"@types/escape-html": "^1.0.2",
|
||||
|
@ -138,7 +145,6 @@
|
|||
"@types/react-toggle": "^4.0.3",
|
||||
"@types/redux-immutable": "^4.0.3",
|
||||
"@types/requestidlecallback": "^0.3.5",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"eslint": "^9.23.0",
|
||||
"eslint-import-resolver-typescript": "^4.2.5",
|
||||
"eslint-plugin-formatjs": "^5.3.1",
|
||||
|
@ -158,9 +164,7 @@
|
|||
"stylelint-config-standard-scss": "^14.0.0",
|
||||
"typescript": "~5.7.3",
|
||||
"typescript-eslint": "^8.29.1",
|
||||
"vite-plugin-rails": "^0.5.0",
|
||||
"vite-plugin-svgr": "^4.2.0",
|
||||
"vitest": "^3.1.1"
|
||||
"vitest": "^3.1.2"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "^18.2.7",
|
||||
|
|
|
@ -17,7 +17,7 @@ RSpec.describe ThemeHelper do
|
|||
)
|
||||
expect(html_links.last.attributes.symbolize_keys)
|
||||
.to include(
|
||||
href: have_attributes(value: match(/default/)),
|
||||
href: have_attributes(value: match(/application/)),
|
||||
media: have_attributes(value: '(prefers-color-scheme: dark)')
|
||||
)
|
||||
end
|
||||
|
|
|
@ -1,22 +1,23 @@
|
|||
/// <reference types="vitest" />
|
||||
|
||||
import path from 'node:path';
|
||||
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 { PluginOption } from 'vite';
|
||||
import svgr from 'vite-plugin-svgr';
|
||||
import {
|
||||
defineConfig,
|
||||
configDefaults,
|
||||
UserConfigFnPromise,
|
||||
} from 'vitest/config';
|
||||
import { analyzer } from 'vite-bundle-analyzer';
|
||||
import RailsPlugin from 'vite-plugin-rails';
|
||||
import { VitePWA } from 'vite-plugin-pwa';
|
||||
|
||||
import { defineConfig, UserConfigFnPromise, UserConfig } from 'vite';
|
||||
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 entrypoints: Record<string, string> = entrypointFiles.reduce(
|
||||
(acc, file) => {
|
||||
|
@ -27,7 +28,7 @@ const config: UserConfigFnPromise = async () => {
|
|||
{} as Record<string, string>,
|
||||
);
|
||||
return {
|
||||
root: path.resolve(__dirname, 'app/javascript'),
|
||||
root: jsRoot,
|
||||
css: {
|
||||
postcss: {
|
||||
plugins: [
|
||||
|
@ -41,15 +42,22 @@ const config: UserConfigFnPromise = async () => {
|
|||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
mastodon: path.resolve(__dirname, 'app/javascript/mastodon'),
|
||||
'@': path.resolve(__dirname, 'app/javascript'),
|
||||
mastodon: path.resolve(jsRoot, 'mastodon'),
|
||||
'@': 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: {
|
||||
commonjsOptions: { transformMixedEsModules: true },
|
||||
outDir: path.resolve(__dirname, '.dist'),
|
||||
emptyOutDir: true,
|
||||
chunkSizeWarningLimit: 1 * 1024 * 1024, // 1MB
|
||||
manifest: 'manifest.json',
|
||||
sourcemap: true,
|
||||
rollupOptions: {
|
||||
input: entrypoints,
|
||||
output: {
|
||||
|
@ -90,25 +98,46 @@ const config: UserConfigFnPromise = async () => {
|
|||
},
|
||||
},
|
||||
},
|
||||
plugins: [react(), svgr(), manifestSRI()],
|
||||
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,
|
||||
},
|
||||
};
|
||||
plugins: [
|
||||
RailsPlugin({
|
||||
compress: mode !== 'production' && command === 'build',
|
||||
}),
|
||||
react({
|
||||
babel: {
|
||||
plugins: ['formatjs', 'transform-react-remove-prop-types'],
|
||||
},
|
||||
}),
|
||||
MastodonServiceWorkerLocales(),
|
||||
VitePWA({
|
||||
srcDir: 'mastodon/service_worker',
|
||||
// We need to use injectManifest because we use our own service worker
|
||||
strategies: 'injectManifest',
|
||||
manifest: false,
|
||||
injectRegister: false,
|
||||
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);
|
||||
|
|
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