Compare commits

...

15 Commits

Author SHA1 Message Date
ChaosExAnima
866aeeeb4e
add preval again and fix import issues with build 2025-05-06 22:02:25 +02:00
Echo
f2b85d4696
Webpack to Vite: Utilize Ruby Vite (#34469)
Co-authored-by: Renaud Chaput <renchap@gmail.com>
2025-05-06 22:02:25 +02:00
ChaosExAnima
b68f93fc47
add all entrypoints and inserts module polyfill, plus loads Vite in dev mode 2025-05-06 22:02:25 +02:00
ChaosExAnima
653d8965c6
remove references to webpack 2025-05-06 22:02:25 +02:00
ChaosExAnima
ccc22cce76
move postcss to vite config, and get rid of more WP dependencies 2025-05-06 22:02:24 +02:00
ChaosExAnima
3475cfec36
add asset integrity key to file 2025-05-06 22:02:24 +02:00
ChaosExAnima
c07b0db3ab
changes entrypoints and tweaks manifest augmenter 2025-05-06 22:02:24 +02:00
ChaosExAnima
14120381e2
fix typing issues 2025-05-06 22:02:24 +02:00
ChaosExAnima
823cd8fb4d
Migrate from Jest to Vitest 2025-05-06 22:02:24 +02:00
ChaosExAnima
698962c185
Migrate from Jest to Vitest 2025-05-06 22:02:02 +02:00
Claire
fbe9728f36
Bump version to v4.3.8 (#34626)
Some checks are pending
Check i18n / check-i18n (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
CodeQL / Analyze (ruby) (push) Waiting to run
Check formatting / lint (push) Waiting to run
JavaScript Linting / lint (push) Waiting to run
Ruby Linting / lint (push) Waiting to run
JavaScript Testing / test (push) Waiting to run
Historical data migration test / test (14-alpine) (push) Waiting to run
Historical data migration test / test (15-alpine) (push) Waiting to run
Historical data migration test / test (16-alpine) (push) Waiting to run
Historical data migration test / test (17-alpine) (push) Waiting to run
Ruby Testing / build (production) (push) Waiting to run
Ruby Testing / build (test) (push) Waiting to run
Ruby Testing / test (.ruby-version) (push) Blocked by required conditions
Ruby Testing / test (3.2) (push) Blocked by required conditions
Ruby Testing / test (3.3) (push) Blocked by required conditions
Ruby Testing / Libvips tests (.ruby-version) (push) Blocked by required conditions
Ruby Testing / Libvips tests (3.2) (push) Blocked by required conditions
Ruby Testing / Libvips tests (3.3) (push) Blocked by required conditions
Ruby Testing / End to End testing (.ruby-version) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.2) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.3) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:8.10.2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, opensearchproject/opensearch:2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.2, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.3, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
2025-05-06 14:17:07 +00:00
Claire
3bbf3e9709
Fix code style issue (#34624) 2025-05-06 13:35:54 +00:00
Claire
79931bf3ae
Merge commit from fork
* Check scheme in account and post links

* Harden media attachments

* Client-side mitigation

* Client-side mitigation for media attachments
2025-05-06 15:02:13 +02:00
Claire
22e2e7f02b
Fix crash when likes or shares collections are not inlined, for real (#34619)
Some checks are pending
Check i18n / check-i18n (push) Waiting to run
CodeQL / Analyze (javascript) (push) Waiting to run
CodeQL / Analyze (ruby) (push) Waiting to run
Check formatting / lint (push) Waiting to run
JavaScript Linting / lint (push) Waiting to run
Ruby Linting / lint (push) Waiting to run
JavaScript Testing / test (push) Waiting to run
Historical data migration test / test (14-alpine) (push) Waiting to run
Historical data migration test / test (15-alpine) (push) Waiting to run
Historical data migration test / test (16-alpine) (push) Waiting to run
Historical data migration test / test (17-alpine) (push) Waiting to run
Ruby Testing / build (production) (push) Waiting to run
Ruby Testing / build (test) (push) Waiting to run
Ruby Testing / test (.ruby-version) (push) Blocked by required conditions
Ruby Testing / test (3.2) (push) Blocked by required conditions
Ruby Testing / test (3.3) (push) Blocked by required conditions
Ruby Testing / Libvips tests (.ruby-version) (push) Blocked by required conditions
Ruby Testing / Libvips tests (3.2) (push) Blocked by required conditions
Ruby Testing / Libvips tests (3.3) (push) Blocked by required conditions
Ruby Testing / End to End testing (.ruby-version) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.2) (push) Blocked by required conditions
Ruby Testing / End to End testing (3.3) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, docker.elastic.co/elasticsearch/elasticsearch:8.10.2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (.ruby-version, opensearchproject/opensearch:2) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.2, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
Ruby Testing / Elastic Search integration testing (3.3, docker.elastic.co/elasticsearch/elasticsearch:7.17.13) (push) Blocked by required conditions
2025-05-06 09:51:42 +00:00
Claire
41d00bc28b
Fix libvips being unconditionally required by tasks (#34620) 2025-05-06 09:45:32 +00:00
119 changed files with 2945 additions and 9225 deletions

View File

@ -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

View File

@ -25,23 +25,6 @@
'tesseract.js', // Requires code changes 'tesseract.js', // Requires code changes
'react-hotkeys', // Requires code changes 'react-hotkeys', // Requires code changes
// Requires Webpacker upgrade or replacement
'@svgr/webpack',
'@types/webpack',
'babel-loader',
'compression-webpack-plugin',
'css-loader',
'imports-loader',
'mini-css-extract-plugin',
'postcss-loader',
'sass-loader',
'terser-webpack-plugin',
'webpack',
'webpack-assets-manifest',
'webpack-bundle-analyzer',
'webpack-dev-server',
'webpack-cli',
// react-router: Requires manual upgrade // react-router: Requires manual upgrade
'history', 'history',
'react-router-dom', 'react-router-dom',

View File

@ -40,4 +40,4 @@ jobs:
uses: ./.github/actions/setup-javascript uses: ./.github/actions/setup-javascript
- name: JavaScript testing - name: JavaScript testing
run: yarn jest --reporters github-actions summary run: yarn test:js

View File

@ -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
View File

@ -21,10 +21,11 @@
/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
/node_modules/ node_modules/
/build/ /build/
# Ignore Vagrant files # Ignore Vagrant files

View File

@ -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

View File

@ -2,9 +2,34 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [4.3.8] - 2025-05-06
### Security
- Update dependencies
- Check scheme on account, profile, and media URLs ([GHSA-x2rc-v5wx-g3m5](https://github.com/mastodon/mastodon/security/advisories/GHSA-x2rc-v5wx-g3m5))
### Added
- Add warning for REDIS_NAMESPACE deprecation at startup (#34581 by @ClearlyClaire)
- Add built-in context for interaction policies (#34574 by @ClearlyClaire)
### Changed
- Change activity distribution error handling to skip retrying for deleted accounts (#33617 by @ClearlyClaire)
### Removed
- Remove double-query for signed query strings (#34610 by @ClearlyClaire)
### Fixed
- Fix incorrect redirect in response to unauthenticated API requests in limited federation mode (#34549 by @ClearlyClaire)
- Fix sign-up e-mail confirmation page reloading on error or redirect (#34548 by @ClearlyClaire)
## [4.3.7] - 2025-04-02 ## [4.3.7] - 2025-04-02
### Add ### Added
- Add delay to profile updates to debounce them (#34137 by @ClearlyClaire) - Add delay to profile updates to debounce them (#34137 by @ClearlyClaire)
- Add support for paginating partial collections in `SynchronizeFollowersService` (#34272 and #34277 by @ClearlyClaire) - Add support for paginating partial collections in `SynchronizeFollowersService` (#34272 and #34277 by @ClearlyClaire)

View File

@ -66,7 +66,7 @@ Example:
Pull requests that do not pass automated checks on CI may not be reviewed. In Pull requests that do not pass automated checks on CI may not be reviewed. In
particular, please keep in mind: particular, please keep in mind:
- Unit and integration tests (rspec, jest) - Unit and integration tests (rspec, vitest)
- Code style rules (rubocop, eslint) - Code style rules (rubocop, eslint)
- Normalization of locale files (i18n-tasks) - Normalization of locale files (i18n-tasks)
- Relevant accessibility or performance concerns - Relevant accessibility or performance concerns

View File

@ -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;

View File

@ -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'

View File

@ -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)

View File

@ -1,4 +1,4 @@
web: env PORT=3000 RAILS_ENV=development bundle exec puma -C config/puma.rb web: env PORT=3000 RAILS_ENV=development bundle exec puma -C config/puma.rb
sidekiq: env PORT=3000 RAILS_ENV=development bundle exec sidekiq sidekiq: env PORT=3000 RAILS_ENV=development bundle exec sidekiq
stream: env PORT=4000 yarn workspace @mastodon/streaming start stream: env PORT=4000 yarn workspace @mastodon/streaming start
webpack: bin/webpack-dev-server vite: yarn dev

View File

@ -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, **)

View File

@ -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

View File

@ -1,4 +1,3 @@
import './public-path';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import Rails from '@rails/ujs'; import Rails from '@rails/ujs';
@ -273,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);

View File

@ -1,9 +1,7 @@
import './public-path'; import { start } from 'mastodon/common';
import { loadLocale } from 'mastodon/locales';
import main from 'mastodon/main'; import main from 'mastodon/main';
import { loadPolyfills } from 'mastodon/polyfills';
import { start } from '../mastodon/common';
import { loadLocale } from '../mastodon/locales';
import { loadPolyfills } from '../mastodon/polyfills';
start(); start();

View File

@ -1,4 +1,3 @@
import './public-path';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { afterInitialRender } from 'mastodon/hooks/useRenderSignal'; import { afterInitialRender } from 'mastodon/hooks/useRenderSignal';

View File

@ -1,4 +1,3 @@
import './public-path';
import ready from '../mastodon/ready'; import ready from '../mastodon/ready';
ready(() => { ready(() => {

View File

@ -0,0 +1,8 @@
<html>
<head>
<script type="module" src="./application.ts"></script>
</head>
<body>
<div id="mastodon"></div>
</body>
</html>

View File

@ -1,4 +0,0 @@
/* Placeholder file to have `inert.scss` compiled by Webpack
This is used by the `wicg-inert` polyfill */
import '../styles/inert.scss';

View File

@ -1,3 +0,0 @@
import '../styles/mailer.scss';
require.context('../icons');

View File

@ -1,23 +0,0 @@
// Dynamically set webpack's loading path depending on a meta header, in order
// to share the same assets regardless of instance configuration.
// See https://webpack.js.org/guides/public-path/#on-the-fly
function removeOuterSlashes(string: string) {
return string.replace(/^\/*/, '').replace(/\/*$/, '');
}
function formatPublicPath(host = '', path = '') {
let formattedHost = removeOuterSlashes(host);
if (formattedHost && !/^http/i.test(formattedHost)) {
formattedHost = `//${formattedHost}`;
}
const formattedPath = removeOuterSlashes(path);
return `${formattedHost}/${formattedPath}/`;
}
const cdnHost = document.querySelector<HTMLMetaElement>('meta[name=cdn-host]');
__webpack_public_path__ = formatPublicPath(
cdnHost ? cdnHost.content : '',
process.env.PUBLIC_OUTPUT_PATH,
);

View File

@ -1,7 +1,5 @@
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import './public-path';
import { IntlMessageFormat } from 'intl-messageformat'; import { IntlMessageFormat } from 'intl-messageformat';
import type { MessageDescriptor, PrimitiveType } from 'react-intl'; import type { MessageDescriptor, PrimitiveType } from 'react-intl';
import { defineMessages } from 'react-intl'; import { defineMessages } from 'react-intl';
@ -18,8 +16,6 @@ import { loadLocale, getLocale } from '../mastodon/locales';
import { loadPolyfills } from '../mastodon/polyfills'; import { loadPolyfills } from '../mastodon/polyfills';
import ready from '../mastodon/ready'; import ready from '../mastodon/ready';
import 'cocoon-js-vanilla';
start(); start();
const messages = defineMessages({ const messages = defineMessages({
@ -153,9 +149,7 @@ function loaded() {
const reactComponents = document.querySelectorAll('[data-component]'); const reactComponents = document.querySelectorAll('[data-component]');
if (reactComponents.length > 0) { if (reactComponents.length > 0) {
import( import('../mastodon/containers/media_container')
/* webpackChunkName: "containers/media_container" */ '../mastodon/containers/media_container'
)
.then(({ default: MediaContainer }) => { .then(({ default: MediaContainer }) => {
reactComponents.forEach((component) => { reactComponents.forEach((component) => {
Array.from(component.children).forEach((child) => { Array.from(component.children).forEach((child) => {

View File

@ -8,8 +8,6 @@ and performs no other task.
*/ */
import './public-path';
import axios from 'axios'; import axios from 'axios';
interface JRDLink { interface JRDLink {

View File

@ -1,4 +1,3 @@
import './public-path';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { start } from '../mastodon/common'; import { start } from '../mastodon/common';

View File

@ -1,4 +1,3 @@
import './public-path';
import axios from 'axios'; import axios from 'axios';
import ready from '../mastodon/ready'; import ready from '../mastodon/ready';

View File

@ -77,6 +77,17 @@ export function normalizeStatus(status, normalOldStatus) {
normalStatus.contentHtml = emojify(normalStatus.content, emojiMap); normalStatus.contentHtml = emojify(normalStatus.content, emojiMap);
normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap); normalStatus.spoilerHtml = emojify(escapeTextContentForBrowser(spoilerText), emojiMap);
normalStatus.hidden = expandSpoilers ? false : spoilerText.length > 0 || normalStatus.sensitive; normalStatus.hidden = expandSpoilers ? false : spoilerText.length > 0 || normalStatus.sensitive;
if (normalStatus.url && !(normalStatus.url.startsWith('http://') || normalStatus.url.startsWith('https://'))) {
normalStatus.url = null;
}
normalStatus.url ||= normalStatus.uri;
normalStatus.media_attachments.forEach(item => {
if (item.remote_url && !(item.remote_url.startsWith('http://') || item.remote_url.startsWith('https://')))
item.remote_url = null;
});
} }
if (normalOldStatus) { if (normalOldStatus) {

View File

@ -1,7 +1,8 @@
import Rails from '@rails/ujs'; import Rails from '@rails/ujs';
export function start() { export function start() {
require.context('../images/', true, /\.(jpg|png|svg)$/); // TODO: Find alternative to this
// require.context('../images/', true, /\.(jpg|png|svg)$/);
try { try {
Rails.start(); Rails.start();

View File

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<AutosuggestEmoji /> renders emoji with custom url 1`] = ` exports[`<AutosuggestEmoji /> > renders emoji with custom url 1`] = `
<div <div
className="autosuggest-emoji" className="autosuggest-emoji"
> >
@ -17,7 +17,7 @@ exports[`<AutosuggestEmoji /> renders emoji with custom url 1`] = `
</div> </div>
`; `;
exports[`<AutosuggestEmoji /> renders native emoji 1`] = ` exports[`<AutosuggestEmoji /> > renders native emoji 1`] = `
<div <div
className="autosuggest-emoji" className="autosuggest-emoji"
> >

View File

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<Avatar /> Autoplay renders a animated avatar 1`] = ` exports[`<Avatar /> > Autoplay > renders a animated avatar 1`] = `
<div <div
className="account__avatar account__avatar--loading" className="account__avatar account__avatar--loading"
onMouseEnter={[Function]} onMouseEnter={[Function]}
@ -21,7 +21,7 @@ exports[`<Avatar /> Autoplay renders a animated avatar 1`] = `
</div> </div>
`; `;
exports[`<Avatar /> Still renders a still avatar 1`] = ` exports[`<Avatar /> > Still > renders a still avatar 1`] = `
<div <div
className="account__avatar account__avatar--loading" className="account__avatar account__avatar--loading"
onMouseEnter={[Function]} onMouseEnter={[Function]}

View File

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<AvatarOverlay renders a overlay avatar 1`] = ` exports[`<AvatarOverlay > renders a overlay avatar 1`] = `
<div <div
className="account__avatar-overlay" className="account__avatar-overlay"
onMouseEnter={[Function]} onMouseEnter={[Function]}

View File

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<Button /> adds class "button-secondary" if props.secondary given 1`] = ` exports[`<Button /> > adds class "button-secondary" if props.secondary given 1`] = `
<button <button
className="button button-secondary" className="button button-secondary"
onClick={[Function]} onClick={[Function]}
@ -8,7 +8,7 @@ exports[`<Button /> adds class "button-secondary" if props.secondary given 1`] =
/> />
`; `;
exports[`<Button /> renders a button element 1`] = ` exports[`<Button /> > renders a button element 1`] = `
<button <button
className="button" className="button"
onClick={[Function]} onClick={[Function]}
@ -16,7 +16,7 @@ exports[`<Button /> renders a button element 1`] = `
/> />
`; `;
exports[`<Button /> renders a disabled attribute if props.disabled given 1`] = ` exports[`<Button /> > renders a disabled attribute if props.disabled given 1`] = `
<button <button
className="button" className="button"
disabled={true} disabled={true}
@ -25,7 +25,7 @@ exports[`<Button /> renders a disabled attribute if props.disabled given 1`] = `
/> />
`; `;
exports[`<Button /> renders class="button--block" if props.block given 1`] = ` exports[`<Button /> > renders class="button--block" if props.block given 1`] = `
<button <button
className="button button--block" className="button button--block"
onClick={[Function]} onClick={[Function]}
@ -33,7 +33,7 @@ exports[`<Button /> renders class="button--block" if props.block given 1`] = `
/> />
`; `;
exports[`<Button /> renders the children 1`] = ` exports[`<Button /> > renders the children 1`] = `
<button <button
className="button" className="button"
onClick={[Function]} onClick={[Function]}
@ -45,7 +45,7 @@ exports[`<Button /> renders the children 1`] = `
</button> </button>
`; `;
exports[`<Button /> renders the given text 1`] = ` exports[`<Button /> > renders the given text 1`] = `
<button <button
className="button" className="button"
onClick={[Function]} onClick={[Function]}
@ -55,7 +55,7 @@ exports[`<Button /> renders the given text 1`] = `
</button> </button>
`; `;
exports[`<Button /> renders the props.text instead of children 1`] = ` exports[`<Button /> > renders the props.text instead of children 1`] = `
<button <button
className="button" className="button"
onClick={[Function]} onClick={[Function]}

View File

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`<DisplayName /> renders display name + account name 1`] = ` exports[`<DisplayName /> > renders display name + account name 1`] = `
<span <span
className="display-name" className="display-name"
onMouseEnter={[Function]} onMouseEnter={[Function]}

View File

@ -21,7 +21,7 @@ describe('<Button />', () => {
}); });
it('handles click events using the given handler', () => { it('handles click events using the given handler', () => {
const handler = jest.fn(); const handler = vi.fn();
render(<Button onClick={handler}>button</Button>); render(<Button onClick={handler}>button</Button>);
fireEvent.click(screen.getByText('button')); fireEvent.click(screen.getByText('button'));
@ -29,7 +29,7 @@ describe('<Button />', () => {
}); });
it('does not handle click events if props.disabled given', () => { it('does not handle click events if props.disabled given', () => {
const handler = jest.fn(); const handler = vi.fn();
render(<Button onClick={handler} disabled>button</Button>); render(<Button onClick={handler} disabled>button</Button>);
fireEvent.click(screen.getByText('button')); fireEvent.click(screen.getByText('button'));

View File

@ -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';

View File

@ -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';

View File

@ -12,7 +12,7 @@ import Overlay from 'react-overlays/Overlay';
import MoodIcon from '@/material-icons/400-20px/mood.svg?react'; import MoodIcon from '@/material-icons/400-20px/mood.svg?react';
import { IconButton } from 'mastodon/components/icon_button'; import { IconButton } from 'mastodon/components/icon_button';
import emojiCompressed from 'mastodon/features/emoji/emoji_compressed'; import emojiCompressed from '@/mastodon/features/emoji/emoji_compressed.mjs';
import { assetHost } from 'mastodon/utils/config'; import { assetHost } from 'mastodon/utils/config';
import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji'; import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji';

View File

@ -9,28 +9,27 @@
// to ensure that the prevaled file is regenerated by Babel // to ensure that the prevaled file is regenerated by Babel
// version: 4 // version: 4
const { NimbleEmojiIndex } = require('emoji-mart'); import { NimbleEmojiIndex } from 'emoji-mart';
const { uncompress: emojiMartUncompress } = require('emoji-mart/dist/utils/data'); import { uncompress as emojiMartUncompress } from 'emoji-mart/dist/utils/data';
import data from './emoji_data.json';
let data = require('./emoji_data.json'); import emojiMap from './emoji_map.json';
const emojiMap = require('./emoji_map.json'); import { unicodeToFilename } from './unicode_to_filename';
const { unicodeToFilename } = require('./unicode_to_filename'); import { unicodeToUnifiedName } from './unicode_to_unified_name';
const { unicodeToUnifiedName } = require('./unicode_to_unified_name');
emojiMartUncompress(data); emojiMartUncompress(data);
const emojiMartData = data; const emojiMartData = data;
const emojiIndex = new NimbleEmojiIndex(emojiMartData); const emojiIndex = new NimbleEmojiIndex(emojiMartData);
const excluded = ['®', '©', '™']; const excluded = ['®', '©', '™'];
const skinTones = ['🏻', '🏼', '🏽', '🏾', '🏿']; const skinTones = ['🏻', '🏼', '🏽', '🏾', '🏿'];
const shortcodeMap = {}; const shortcodeMap = {};
const shortCodesToEmojiData = {}; const shortCodesToEmojiData = {};
const emojisWithoutShortCodes = []; const emojisWithoutShortCodes = [];
Object.keys(emojiIndex.emojis).forEach(key => { Object.keys(emojiIndex.emojis).forEach((key) => {
let emoji = emojiIndex.emojis[key]; let emoji = emojiIndex.emojis[key];
// Emojis with skin tone modifiers are stored like this // Emojis with skin tone modifiers are stored like this
@ -41,22 +40,22 @@ Object.keys(emojiIndex.emojis).forEach(key => {
shortcodeMap[emoji.native] = emoji.id; shortcodeMap[emoji.native] = emoji.id;
}); });
const stripModifiers = unicode => { const stripModifiers = (unicode) => {
skinTones.forEach(tone => { skinTones.forEach((tone) => {
unicode = unicode.replace(tone, ''); unicode = unicode.replace(tone, '');
}); });
return unicode; return unicode;
}; };
Object.keys(emojiMap).forEach(key => { Object.keys(emojiMap).forEach((key) => {
if (excluded.includes(key)) { if (excluded.includes(key)) {
delete emojiMap[key]; delete emojiMap[key];
return; return;
} }
const normalizedKey = stripModifiers(key); const normalizedKey = stripModifiers(key);
let shortcode = shortcodeMap[normalizedKey]; let shortcode = shortcodeMap[normalizedKey];
if (!shortcode) { if (!shortcode) {
shortcode = shortcodeMap[normalizedKey + '\uFE0F']; shortcode = shortcodeMap[normalizedKey + '\uFE0F'];
@ -82,7 +81,7 @@ Object.keys(emojiMap).forEach(key => {
} }
}); });
Object.keys(emojiIndex.emojis).forEach(key => { Object.keys(emojiIndex.emojis).forEach((key) => {
let emoji = emojiIndex.emojis[key]; let emoji = emojiIndex.emojis[key];
// Emojis with skin tone modifiers are stored like this // Emojis with skin tone modifiers are stored like this
@ -94,9 +93,11 @@ Object.keys(emojiIndex.emojis).forEach(key => {
let { short_names, search, unified } = emojiMartData.emojis[key]; let { short_names, search, unified } = emojiMartData.emojis[key];
if (short_names[0] !== key) { if (short_names[0] !== key) {
throw new Error('The compressor expects the first short_code to be the ' + throw new Error(
'key. It may need to be rewritten if the emoji change such that this ' + 'The compressor expects the first short_code to be the ' +
'is no longer the case.'); 'key. It may need to be rewritten if the emoji change such that this ' +
'is no longer the case.',
);
} }
short_names = short_names.slice(1); // first short name can be inferred from the key short_names = short_names.slice(1); // first short name can be inferred from the key
@ -117,21 +118,23 @@ Object.keys(emojiIndex.emojis).forEach(key => {
// JSON.parse/stringify is to emulate what @preval is doing and avoid any // JSON.parse/stringify is to emulate what @preval is doing and avoid any
// inconsistent behavior in dev mode // inconsistent behavior in dev mode
module.exports = JSON.parse(JSON.stringify([ export default JSON.parse(
shortCodesToEmojiData, JSON.stringify([
/* shortCodesToEmojiData,
* The property `skins` is not found in the current context. /*
* This could potentially lead to issues when interacting with modules or data structures * The property `skins` is not found in the current context.
* that expect the presence of `skins` property. * This could potentially lead to issues when interacting with modules or data structures
* Currently, no definitions or references to `skins` property can be found in: * that expect the presence of `skins` property.
* - {@link node_modules/emoji-mart/dist/utils/data.js} * Currently, no definitions or references to `skins` property can be found in:
* - {@link node_modules/emoji-mart/data/all.json} * - {@link node_modules/emoji-mart/dist/utils/data.js}
* - {@link app/javascript/mastodon/features/emoji/emoji_compressed.d.ts#Skins} * - {@link node_modules/emoji-mart/data/all.json}
* Future refactorings or updates should consider adding definitions or handling for `skins` property. * - {@link app/javascript/mastodon/features/emoji/emoji_compressed.d.ts#Skins}
*/ * Future refactorings or updates should consider adding definitions or handling for `skins` property.
emojiMartData.skins, */
emojiMartData.categories, emojiMartData.skins,
emojiMartData.aliases, emojiMartData.categories,
emojisWithoutShortCodes, emojiMartData.aliases,
emojiMartData emojisWithoutShortCodes,
])); emojiMartData,
]),
);

View File

@ -4,8 +4,8 @@
import type { BaseEmoji } from 'emoji-mart'; import type { BaseEmoji } from 'emoji-mart';
import type { Emoji } from 'emoji-mart/dist-es/utils/data'; import type { Emoji } from 'emoji-mart/dist-es/utils/data';
import type { Search, ShortCodesToEmojiData } from './emoji_compressed'; import type { Search, ShortCodesToEmojiData } from './emoji_compressed.mjs';
import emojiCompressed from './emoji_compressed'; import emojiCompressed from './emoji_compressed.mjs';
import { unicodeToUnifiedName } from './unicode_to_unified_name'; import { unicodeToUnifiedName } from './unicode_to_unified_name';
type Emojis = Record< type Emojis = Record<

View File

@ -5,8 +5,8 @@
import type { import type {
FilenameData, FilenameData,
ShortCodesToEmojiDataKey, ShortCodesToEmojiDataKey,
} from './emoji_compressed'; } from './emoji_compressed.mjs';
import emojiCompressed from './emoji_compressed'; import emojiCompressed from './emoji_compressed.mjs';
import { unicodeToFilename } from './unicode_to_filename'; import { unicodeToFilename } from './unicode_to_filename';
type UnicodeMapping = Record< type UnicodeMapping = Record<

View File

@ -1,6 +1,6 @@
// taken from: // taken from:
// https://github.com/twitter/twemoji/blob/47732c7/twemoji-generator.js#L848-L866 // https://github.com/twitter/twemoji/blob/47732c7/twemoji-generator.js#L848-L866
exports.unicodeToFilename = (str) => { export const unicodeToFilename = (str) => {
let result = ''; let result = '';
let charCode = 0; let charCode = 0;
let p = 0; let p = 0;

View File

@ -6,7 +6,7 @@ function padLeft(str, num) {
return str; return str;
} }
exports.unicodeToUnifiedName = (str) => { export const unicodeToUnifiedName = (str) => {
let output = ''; let output = '';
for (let i = 0; i < str.length; i += 2) { for (let i = 0; i < str.length; i += 2) {

View File

@ -7,7 +7,7 @@ const fakeIcon = () => <span />;
describe('<Column />', () => { describe('<Column />', () => {
describe('<ColumnHeader /> click handler', () => { describe('<ColumnHeader /> click handler', () => {
it('runs the scroll animation if the column contains scrollable content', () => { it('runs the scroll animation if the column contains scrollable content', () => {
const scrollToMock = jest.fn(); const scrollToMock = vi.fn();
const { container } = render( const { container } = render(
<Column heading='notifications' icon='notifications' iconComponent={fakeIcon}> <Column heading='notifications' icon='notifications' iconComponent={fakeIcon}>
<div className='scrollable' /> <div className='scrollable' />

View File

@ -1,235 +1,235 @@
export function EmojiPicker () { export function EmojiPicker () {
return import(/* webpackChunkName: "emoji_picker" */'../../emoji/emoji_picker'); return import('../../emoji/emoji_picker');
} }
export function Compose () { export function Compose () {
return import(/* webpackChunkName: "features/compose" */'../../compose'); return import('../../compose');
} }
export function Notifications () { export function Notifications () {
return import(/* webpackChunkName: "features/notifications" */'../../notifications_v2'); return import('../../notifications_v2');
} }
export function HomeTimeline () { export function HomeTimeline () {
return import(/* webpackChunkName: "features/home_timeline" */'../../home_timeline'); return import('../../home_timeline');
} }
export function PublicTimeline () { export function PublicTimeline () {
return import(/* webpackChunkName: "features/public_timeline" */'../../public_timeline'); return import('../../public_timeline');
} }
export function CommunityTimeline () { export function CommunityTimeline () {
return import(/* webpackChunkName: "features/community_timeline" */'../../community_timeline'); return import('../../community_timeline');
} }
export function Firehose () { export function Firehose () {
return import(/* webpackChunkName: "features/firehose" */'../../firehose'); return import('../../firehose');
} }
export function HashtagTimeline () { export function HashtagTimeline () {
return import(/* webpackChunkName: "features/hashtag_timeline" */'../../hashtag_timeline'); return import('../../hashtag_timeline');
} }
export function DirectTimeline() { export function DirectTimeline() {
return import(/* webpackChunkName: "features/direct_timeline" */'../../direct_timeline'); return import('../../direct_timeline');
} }
export function ListTimeline () { export function ListTimeline () {
return import(/* webpackChunkName: "features/list_timeline" */'../../list_timeline'); return import('../../list_timeline');
} }
export function Lists () { export function Lists () {
return import(/* webpackChunkName: "features/lists" */'../../lists'); return import('../../lists');
} }
export function Status () { export function Status () {
return import(/* webpackChunkName: "features/status" */'../../status'); return import('../../status');
} }
export function GettingStarted () { export function GettingStarted () {
return import(/* webpackChunkName: "features/getting_started" */'../../getting_started'); return import('../../getting_started');
} }
export function KeyboardShortcuts () { export function KeyboardShortcuts () {
return import(/* webpackChunkName: "features/keyboard_shortcuts" */'../../keyboard_shortcuts'); return import('../../keyboard_shortcuts');
} }
export function PinnedStatuses () { export function PinnedStatuses () {
return import(/* webpackChunkName: "features/pinned_statuses" */'../../pinned_statuses'); return import('../../pinned_statuses');
} }
export function AccountTimeline () { export function AccountTimeline () {
return import(/* webpackChunkName: "features/account_timeline" */'../../account_timeline'); return import('../../account_timeline');
} }
export function AccountGallery () { export function AccountGallery () {
return import(/* webpackChunkName: "features/account_gallery" */'../../account_gallery'); return import('../../account_gallery');
} }
export function AccountFeatured() { export function AccountFeatured() {
return import(/* webpackChunkName: "features/account_featured" */'../../account_featured'); return import('../../account_featured');
} }
export function Followers () { export function Followers () {
return import(/* webpackChunkName: "features/followers" */'../../followers'); return import('../../followers');
} }
export function Following () { export function Following () {
return import(/* webpackChunkName: "features/following" */'../../following'); return import('../../following');
} }
export function Reblogs () { export function Reblogs () {
return import(/* webpackChunkName: "features/reblogs" */'../../reblogs'); return import('../../reblogs');
} }
export function Favourites () { export function Favourites () {
return import(/* webpackChunkName: "features/favourites" */'../../favourites'); return import('../../favourites');
} }
export function FollowRequests () { export function FollowRequests () {
return import(/* webpackChunkName: "features/follow_requests" */'../../follow_requests'); return import('../../follow_requests');
} }
export function FavouritedStatuses () { export function FavouritedStatuses () {
return import(/* webpackChunkName: "features/favourited_statuses" */'../../favourited_statuses'); return import('../../favourited_statuses');
} }
export function FollowedTags () { export function FollowedTags () {
return import(/* webpackChunkName: "features/followed_tags" */'../../followed_tags'); return import('../../followed_tags');
} }
export function BookmarkedStatuses () { export function BookmarkedStatuses () {
return import(/* webpackChunkName: "features/bookmarked_statuses" */'../../bookmarked_statuses'); return import('../../bookmarked_statuses');
} }
export function Blocks () { export function Blocks () {
return import(/* webpackChunkName: "features/blocks" */'../../blocks'); return import('../../blocks');
} }
export function DomainBlocks () { export function DomainBlocks () {
return import(/* webpackChunkName: "features/domain_blocks" */'../../domain_blocks'); return import('../../domain_blocks');
} }
export function Mutes () { export function Mutes () {
return import(/* webpackChunkName: "features/mutes" */'../../mutes'); return import('../../mutes');
} }
export function MuteModal () { export function MuteModal () {
return import(/* webpackChunkName: "modals/mute_modal" */'../components/mute_modal'); return import('../components/mute_modal');
} }
export function BlockModal () { export function BlockModal () {
return import(/* webpackChunkName: "modals/block_modal" */'../components/block_modal'); return import('../components/block_modal');
} }
export function DomainBlockModal () { export function DomainBlockModal () {
return import(/* webpackChunkName: "modals/domain_block_modal" */'../components/domain_block_modal'); return import('../components/domain_block_modal');
} }
export function ReportModal () { export function ReportModal () {
return import(/* webpackChunkName: "modals/report_modal" */'../components/report_modal'); return import('../components/report_modal');
} }
export function IgnoreNotificationsModal () { export function IgnoreNotificationsModal () {
return import(/* webpackChunkName: "modals/domain_block_modal" */'../components/ignore_notifications_modal'); return import('../components/ignore_notifications_modal');
} }
export function MediaGallery () { export function MediaGallery () {
return import(/* webpackChunkName: "status/media_gallery" */'../../../components/media_gallery'); return import('../../../components/media_gallery');
} }
export function Video () { export function Video () {
return import(/* webpackChunkName: "features/video" */'../../video'); return import('../../video');
} }
export function EmbedModal () { export function EmbedModal () {
return import(/* webpackChunkName: "modals/embed_modal" */'../components/embed_modal'); return import('../components/embed_modal');
} }
export function ListAdder () { export function ListAdder () {
return import(/*webpackChunkName: "features/list_adder" */'../../list_adder'); return import('../../list_adder');
} }
export function Tesseract () { export function Tesseract () {
return import(/*webpackChunkName: "tesseract" */'tesseract.js'); return import('tesseract.js');
} }
export function Audio () { export function Audio () {
return import(/* webpackChunkName: "features/audio" */'../../audio'); return import('../../audio');
} }
export function Directory () { export function Directory () {
return import(/* webpackChunkName: "features/directory" */'../../directory'); return import('../../directory');
} }
export function OnboardingProfile () { export function OnboardingProfile () {
return import(/* webpackChunkName: "features/onboarding" */'../../onboarding/profile'); return import('../../onboarding/profile');
} }
export function OnboardingFollows () { export function OnboardingFollows () {
return import(/* webpackChunkName: "features/onboarding" */'../../onboarding/follows'); return import('../../onboarding/follows');
} }
export function CompareHistoryModal () { export function CompareHistoryModal () {
return import(/*webpackChunkName: "modals/compare_history_modal" */'../components/compare_history_modal'); return import('../components/compare_history_modal');
} }
export function Explore () { export function Explore () {
return import(/* webpackChunkName: "features/explore" */'../../explore'); return import('../../explore');
} }
export function Search () { export function Search () {
return import(/* webpackChunkName: "features/explore" */'../../search'); return import('../../search');
} }
export function FilterModal () { export function FilterModal () {
return import(/*webpackChunkName: "modals/filter_modal" */'../components/filter_modal'); return import('../components/filter_modal');
} }
export function InteractionModal () { export function InteractionModal () {
return import(/*webpackChunkName: "modals/interaction_modal" */'../../interaction_modal'); return import('../../interaction_modal');
} }
export function SubscribedLanguagesModal () { export function SubscribedLanguagesModal () {
return import(/*webpackChunkName: "modals/subscribed_languages_modal" */'../../subscribed_languages_modal'); return import('../../subscribed_languages_modal');
} }
export function ClosedRegistrationsModal () { export function ClosedRegistrationsModal () {
return import(/*webpackChunkName: "modals/closed_registrations_modal" */'../../closed_registrations_modal'); return import('../../closed_registrations_modal');
} }
export function About () { export function About () {
return import(/*webpackChunkName: "features/about" */'../../about'); return import('../../about');
} }
export function PrivacyPolicy () { export function PrivacyPolicy () {
return import(/*webpackChunkName: "features/privacy_policy" */'../../privacy_policy'); return import('../../privacy_policy');
} }
export function TermsOfService () { export function TermsOfService () {
return import(/*webpackChunkName: "features/terms_of_service" */'../../terms_of_service'); return import('../../terms_of_service');
} }
export function NotificationRequests () { export function NotificationRequests () {
return import(/*webpackChunkName: "features/notifications/requests" */'../../notifications/requests'); return import('../../notifications/requests');
} }
export function NotificationRequest () { export function NotificationRequest () {
return import(/*webpackChunkName: "features/notifications/request" */'../../notifications/request'); return import('../../notifications/request');
} }
export function LinkTimeline () { export function LinkTimeline () {
return import(/*webpackChunkName: "features/link_timeline" */'../../link_timeline'); return import('../../link_timeline');
} }
export function AnnualReportModal () { export function AnnualReportModal () {
return import(/*webpackChunkName: "modals/annual_report_modal" */'../components/annual_report_modal'); return import('../components/annual_report_modal');
} }
export function ListEdit () { export function ListEdit () {
return import(/*webpackChunkName: "features/lists" */'../../lists/new'); return import('../../lists/new');
} }
export function ListMembers () { export function ListMembers () {
return import(/* webpackChunkName: "features/lists" */'../../lists/members'); return import('../../lists/members');
} }

View File

@ -3,7 +3,7 @@
// can at least log in using KaiOS devices). // can at least log in using KaiOS devices).
function importArrowKeyNavigation() { function importArrowKeyNavigation() {
return import(/* webpackChunkName: "arrow-key-navigation" */ 'arrow-key-navigation'); return import('arrow-key-navigation');
} }
export default function loadKeyboardExtensions() { export default function loadKeyboardExtensions() {

View File

@ -5,6 +5,10 @@ import { isLocaleLoaded, setLocale } from './global_locale';
const localeLoadingSemaphore = new Semaphore(1); const localeLoadingSemaphore = new Semaphore(1);
const localeFiles = import.meta.glob<{ default: LocaleData['messages'] }>([
'./*.json',
]);
export async function loadLocale() { export async function loadLocale() {
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- we want to match empty strings // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- we want to match empty strings
const locale = document.querySelector<HTMLElement>('html')?.lang || 'en'; const locale = document.querySelector<HTMLElement>('html')?.lang || 'en';
@ -17,13 +21,14 @@ export async function loadLocale() {
// if the locale is already set, then do nothing // if the locale is already set, then do nothing
if (isLocaleLoaded()) return; if (isLocaleLoaded()) return;
const localeData = (await import( // If there is no locale file, then fallback to english
/* webpackMode: "lazy" */ const localeFile = Object.hasOwn(localeFiles, `./${locale}.json`)
/* webpackChunkName: "locale/[request]" */ ? localeFiles[`./${locale}.json`]
/* webpackInclude: /\.json$/ */ : localeFiles['./en.json'];
/* webpackPreload: true */
`mastodon/locales/${locale}.json` if (!localeFile) throw new Error('Could not load the locale JSON file');
)) as LocaleData['messages'];
const { default: localeData } = await localeFile();
setLocale({ messages: localeData, locale }); setLocale({ messages: localeData, locale });
}); });

View 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;

View File

@ -144,5 +144,10 @@ export function createAccountFromServerJSON(serverJSON: ApiAccountJSON) {
), ),
note_emojified: emojify(accountJSON.note, emojiMap), note_emojified: emojify(accountJSON.note, emojiMap),
note_plain: unescapeHTML(accountJSON.note), note_plain: unescapeHTML(accountJSON.note),
url:
accountJSON.url.startsWith('http://') ||
accountJSON.url.startsWith('https://')
? accountJSON.url
: accountJSON.uri,
}); });
} }

View File

@ -1,7 +1,6 @@
// //
// Tools for performance debugging, only enabled in development mode. // Tools for performance debugging, only enabled in development mode.
// Open up Chrome Dev Tools, then Timeline, then User Timing to see output. // Open up Chrome Dev Tools, then Timeline, then User Timing to see output.
// Also see config/webpack/loaders/mark.js for the webpack loader marks.
import * as marky from 'marky'; import * as marky from 'marky';

View File

@ -2,10 +2,13 @@
// If there are no polyfills, then this is just Promise.resolve() which means // If there are no polyfills, then this is just Promise.resolve() which means
// it will execute in the same tick of the event loop (i.e. near-instant). // it will execute in the same tick of the event loop (i.e. near-instant).
// eslint-disable-next-line import/extensions -- This file is virtual so it thinks it has an extension
import 'vite/modulepreload-polyfill';
import { loadIntlPolyfills } from './intl'; import { loadIntlPolyfills } from './intl';
function importExtraPolyfills() { function importExtraPolyfills() {
return import(/* webpackChunkName: "extra_polyfills" */ './extra_polyfills'); return import('./extra_polyfills');
} }
export function loadPolyfills() { export function loadPolyfills() {

View File

@ -54,11 +54,9 @@ async function loadIntlPluralRulesPolyfills(locale: string) {
return; return;
} }
// Load the polyfill 1st BEFORE loading data // Load the polyfill 1st BEFORE loading data
await import('@formatjs/intl-pluralrules/polyfill-force');
await import( await import(
/* webpackChunkName: "i18n-pluralrules-polyfill" */ '@formatjs/intl-pluralrules/polyfill-force' `../../../../node_modules/@formatjs/intl-pluralrules/locale-data/${unsupportedLocale}.js`
);
await import(
/* webpackChunkName: "i18n-pluralrules-polyfill-[request]" */ `@formatjs/intl-pluralrules/locale-data/${unsupportedLocale}`
); );
} }
@ -70,11 +68,9 @@ async function loadIntlPluralRulesPolyfills(locale: string) {
// } // }
// // Load the polyfill 1st BEFORE loading data // // Load the polyfill 1st BEFORE loading data
// await import( // await import(
// /* webpackChunkName: "i18n-relativetimeformat-polyfill" */
// '@formatjs/intl-relativetimeformat/polyfill-force' // '@formatjs/intl-relativetimeformat/polyfill-force'
// ); // );
// await import( // await import(
// /* webpackChunkName: "i18n-relativetimeformat-polyfill-[request]" */
// `@formatjs/intl-relativetimeformat/locale-data/${unsupportedLocale}` // `@formatjs/intl-relativetimeformat/locale-data/${unsupportedLocale}`
// ); // );
// } // }

View File

@ -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: [

View File

@ -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));

View File

@ -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';

View File

@ -3,17 +3,17 @@ import { IntlProvider } from 'react-intl';
import { MemoryRouter } from 'react-router'; import { MemoryRouter } from 'react-router';
import type { RenderOptions } from '@testing-library/react'; import type { RenderOptions } from '@testing-library/react';
// eslint-disable-next-line import/no-extraneous-dependencies
import { render as rtlRender } from '@testing-library/react'; import { render as rtlRender } from '@testing-library/react';
import { IdentityContext } from './identity_context'; import { IdentityContext } from './identity_context';
beforeEach(() => { beforeAll(() => {
global.requestIdleCallback = jest global.requestIdleCallback = vi.fn((cb: IdleRequestCallback) => {
.fn() // @ts-expect-error IdleRequestCallback expects an argument of type IdleDeadline,
.mockImplementation((fn: () => void) => { // but that doesn't exist in this environment.
fn(); cb();
}); return 0;
});
}); });
function render( function render(
@ -46,7 +46,6 @@ function render(
} }
// re-export everything // re-export everything
// eslint-disable-next-line import/no-extraneous-dependencies
export * from '@testing-library/react'; export * from '@testing-library/react';
// override render method // override render method

View File

@ -1 +0,0 @@
import '@testing-library/jest-dom';

View File

@ -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;
} }

View File

@ -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;

View File

@ -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;

View File

@ -15,13 +15,15 @@ class ActivityPub::Parser::MediaAttachmentParser
end end
def remote_url def remote_url
Addressable::URI.parse(@json['url'])&.normalize&.to_s url = Addressable::URI.parse(@json['url'])&.normalize&.to_s
url unless unsupported_uri_scheme?(url)
rescue Addressable::URI::InvalidURIError rescue Addressable::URI::InvalidURIError
nil nil
end end
def thumbnail_remote_url def thumbnail_remote_url
Addressable::URI.parse(@json['icon'].is_a?(Hash) ? @json['icon']['url'] : @json['icon'])&.normalize&.to_s url = Addressable::URI.parse(@json['icon'].is_a?(Hash) ? @json['icon']['url'] : @json['icon'])&.normalize&.to_s
url unless unsupported_uri_scheme?(url)
rescue Addressable::URI::InvalidURIError rescue Addressable::URI::InvalidURIError
nil nil
end end

View File

@ -29,7 +29,10 @@ class ActivityPub::Parser::StatusParser
end end
def url def url
url_to_href(@object['url'], 'text/html') if @object['url'].present? return if @object['url'].blank?
url = url_to_href(@object['url'], 'text/html')
url unless unsupported_uri_scheme?(url)
end end
def text def text
@ -95,11 +98,11 @@ class ActivityPub::Parser::StatusParser
end end
def favourites_count def favourites_count
@object.dig('likes', 'totalItems') if @object.is_a?(Hash) @object['likes']['totalItems'] if @object.is_a?(Hash) && @object['likes'].is_a?(Hash)
end end
def reblogs_count def reblogs_count
@object.dig('shares', 'totalItems') if @object.is_a?(Hash) @object['shares']['totalItems'] if @object.is_a?(Hash) && @object['shares'].is_a?(Hash)
end end
def quote_policy def quote_policy

View File

@ -4,6 +4,7 @@ require 'singleton'
class ActivityPub::TagManager class ActivityPub::TagManager
include Singleton include Singleton
include JsonLdHelper
include RoutingHelper include RoutingHelper
CONTEXT = 'https://www.w3.org/ns/activitystreams' CONTEXT = 'https://www.w3.org/ns/activitystreams'
@ -17,7 +18,7 @@ class ActivityPub::TagManager
end end
def url_for(target) def url_for(target)
return target.url if target.respond_to?(:local?) && !target.local? return unsupported_uri_scheme?(target.url) ? nil : target.url if target.respond_to?(:local?) && !target.local?
return unless target.respond_to?(:object_type) return unless target.respond_to?(:object_type)

View File

@ -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' }

View File

@ -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'

View File

@ -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'

View File

@ -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 }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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' }

View File

@ -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'

View File

@ -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'

View File

@ -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

View File

@ -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'

View File

@ -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'

View File

@ -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

View File

@ -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) } }

View File

@ -1,80 +0,0 @@
module.exports = (api) => {
const env = api.env();
const reactOptions = {
development: false,
runtime: 'automatic',
};
const envOptions = {
useBuiltIns: "usage",
corejs: { version: "3.30" },
debug: false,
include: [
'transform-numeric-separator',
'transform-optional-chaining',
'transform-nullish-coalescing-operator',
'transform-class-properties',
],
};
const plugins = [
['formatjs'],
'preval',
];
switch (env) {
case 'production':
plugins.push(...[
'lodash',
[
'transform-react-remove-prop-types',
{
mode: 'remove',
removeImport: true,
additionalLibraries: [
'react-immutable-proptypes',
],
},
],
'@babel/transform-react-inline-elements',
[
'@babel/transform-runtime',
{
helpers: true,
regenerator: false,
useESModules: true,
},
],
]);
break;
case 'development':
reactOptions.development = true;
envOptions.debug = true;
// We need Babel to not inject polyfills in dev, as this breaks `preval` files
envOptions.useBuiltIns = false;
envOptions.corejs = undefined;
break;
}
const config = {
presets: [
'@babel/preset-typescript',
['@babel/react', reactOptions],
['@babel/env', envOptions],
],
plugins,
overrides: [
{
test: [/tesseract\.js/, /fuzzysort\.js/],
presets: [
['@babel/env', { ...envOptions, modules: 'commonjs' }],
],
},
],
};
return config;
};

27
bin/vite Executable file
View 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")

View File

@ -1,19 +0,0 @@
#!/usr/bin/env ruby
ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development"
ENV["NODE_ENV"] ||= "development"
require "pathname"
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
Pathname.new(__FILE__).realpath)
require "rubygems"
require "bundler/setup"
require "webpacker"
require "webpacker/webpack_runner"
APP_ROOT = File.expand_path("..", __dir__)
Dir.chdir(APP_ROOT) do
Webpacker::WebpackRunner.run(ARGV)
end

View File

@ -1,19 +0,0 @@
#!/usr/bin/env ruby
ENV["RAILS_ENV"] ||= ENV["RACK_ENV"] || "development"
ENV["NODE_ENV"] ||= "development"
require "pathname"
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile",
Pathname.new(__FILE__).realpath)
require "rubygems"
require "bundler/setup"
require "webpacker"
require "webpacker/dev_server_runner"
APP_ROOT = File.expand_path("..", __dir__)
Dir.chdir(APP_ROOT) do
Webpacker::DevServerRunner.run(ARGV)
end

View File

@ -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'

View File

@ -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

20
config/vite.json Normal file
View File

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

View File

@ -0,0 +1,85 @@
import { createHash } from 'node:crypto';
import { promises as fs } from 'node:fs';
import { resolve } from 'node:path';
import type { Plugin, Manifest } from 'vite';
export type Algorithm = 'sha256' | 'sha384' | 'sha512';
export interface Options {
/**
* Which hashing algorithms to use when calculate the integrity hash for each
* asset in the manifest.
* @default ['sha384']
*/
algorithms?: Algorithm[];
}
declare module 'vite' {
interface ManifestChunk {
integrity: string;
assetIntegrity: Record<string, string>;
}
}
export function manifestSRI(options: Options = {}): Plugin {
const { algorithms = ['sha384'] } = options;
return {
name: 'vite-plugin-manifest-sri',
apply: 'build',
enforce: 'post',
async writeBundle({ dir }) {
await augmentManifest('manifest.json', algorithms, dir ?? '');
},
};
}
async function augmentManifest(
manifestPath: string,
algorithms: string[],
outDir: string,
) {
const resolveInOutDir = (path: string) => resolve(outDir, path);
manifestPath = resolveInOutDir(manifestPath);
const manifest: Manifest | undefined = await fs
.readFile(manifestPath, 'utf-8')
.then((file) => JSON.parse(file) as Manifest);
if (!manifest) {
throw new Error(`Manifest file not found at ${manifestPath}`);
}
await Promise.all(
Object.values(manifest).map(async (chunk) => {
chunk.integrity = integrityForAsset(
await fs.readFile(resolveInOutDir(chunk.file)),
algorithms,
);
if (!chunk.assets && !chunk.css) {
return;
}
chunk.assetIntegrity = {};
await Promise.all(
(chunk.assets ?? []).concat(chunk.css ?? []).map(async (asset) => {
chunk.assetIntegrity[asset] = integrityForAsset(
await fs.readFile(resolveInOutDir(asset)),
algorithms,
);
}),
);
}),
);
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2));
}
function integrityForAsset(source: Buffer, algorithms: string[]) {
return algorithms
.map((algorithm) => calculateIntegrityHash(source, algorithm))
.join(' ');
}
export function calculateIntegrityHash(source: Buffer, algorithm: string) {
const hash = createHash(algorithm).update(source).digest().toString('base64');
return `${algorithm.toLowerCase()}-${hash}`;
}

View 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;
},
};
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

Some files were not shown because too many files have changed in this diff Show More