mirror of https://github.com/mastodon/mastodon
Convert from Webpack to Vite (#34450)
Co-authored-by: Renaud Chaput <renchap@gmail.com>pull/34706/head
parent
a5a2c6dc7e
commit
c4f47adb49
@ -1,4 +1,4 @@
|
||||
web: env PORT=3000 RAILS_ENV=development bundle exec puma -C config/puma.rb
|
||||
sidekiq: env PORT=3000 RAILS_ENV=development bundle exec sidekiq
|
||||
stream: env PORT=4000 yarn workspace @mastodon/streaming start
|
||||
webpack: bin/webpack-dev-server
|
||||
vite: yarn dev
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
import { start } from 'mastodon/common';
|
||||
|
||||
start();
|
||||
@ -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';
|
||||
@ -1,3 +0,0 @@
|
||||
import '../styles/mailer.scss';
|
||||
|
||||
require.context('../icons');
|
||||
@ -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,
|
||||
);
|
||||
@ -1,8 +1,6 @@
|
||||
import Rails from '@rails/ujs';
|
||||
|
||||
export function start() {
|
||||
require.context('../images/', true, /\.(jpg|png|svg)$/);
|
||||
|
||||
try {
|
||||
Rails.start();
|
||||
} catch {
|
||||
@ -1,235 +1,235 @@
|
||||
export function EmojiPicker () {
|
||||
return import(/* webpackChunkName: "emoji_picker" */'../../emoji/emoji_picker');
|
||||
return import('../../emoji/emoji_picker');
|
||||
}
|
||||
|
||||
export function Compose () {
|
||||
return import(/* webpackChunkName: "features/compose" */'../../compose');
|
||||
return import('../../compose');
|
||||
}
|
||||
|
||||
export function Notifications () {
|
||||
return import(/* webpackChunkName: "features/notifications" */'../../notifications_v2');
|
||||
return import('../../notifications_v2');
|
||||
}
|
||||
|
||||
export function HomeTimeline () {
|
||||
return import(/* webpackChunkName: "features/home_timeline" */'../../home_timeline');
|
||||
return import('../../home_timeline');
|
||||
}
|
||||
|
||||
export function PublicTimeline () {
|
||||
return import(/* webpackChunkName: "features/public_timeline" */'../../public_timeline');
|
||||
return import('../../public_timeline');
|
||||
}
|
||||
|
||||
export function CommunityTimeline () {
|
||||
return import(/* webpackChunkName: "features/community_timeline" */'../../community_timeline');
|
||||
return import('../../community_timeline');
|
||||
}
|
||||
|
||||
export function Firehose () {
|
||||
return import(/* webpackChunkName: "features/firehose" */'../../firehose');
|
||||
return import('../../firehose');
|
||||
}
|
||||
|
||||
export function HashtagTimeline () {
|
||||
return import(/* webpackChunkName: "features/hashtag_timeline" */'../../hashtag_timeline');
|
||||
return import('../../hashtag_timeline');
|
||||
}
|
||||
|
||||
export function DirectTimeline() {
|
||||
return import(/* webpackChunkName: "features/direct_timeline" */'../../direct_timeline');
|
||||
return import('../../direct_timeline');
|
||||
}
|
||||
|
||||
export function ListTimeline () {
|
||||
return import(/* webpackChunkName: "features/list_timeline" */'../../list_timeline');
|
||||
return import('../../list_timeline');
|
||||
}
|
||||
|
||||
export function Lists () {
|
||||
return import(/* webpackChunkName: "features/lists" */'../../lists');
|
||||
return import('../../lists');
|
||||
}
|
||||
|
||||
export function Status () {
|
||||
return import(/* webpackChunkName: "features/status" */'../../status');
|
||||
return import('../../status');
|
||||
}
|
||||
|
||||
export function GettingStarted () {
|
||||
return import(/* webpackChunkName: "features/getting_started" */'../../getting_started');
|
||||
return import('../../getting_started');
|
||||
}
|
||||
|
||||
export function KeyboardShortcuts () {
|
||||
return import(/* webpackChunkName: "features/keyboard_shortcuts" */'../../keyboard_shortcuts');
|
||||
return import('../../keyboard_shortcuts');
|
||||
}
|
||||
|
||||
export function PinnedStatuses () {
|
||||
return import(/* webpackChunkName: "features/pinned_statuses" */'../../pinned_statuses');
|
||||
return import('../../pinned_statuses');
|
||||
}
|
||||
|
||||
export function AccountTimeline () {
|
||||
return import(/* webpackChunkName: "features/account_timeline" */'../../account_timeline');
|
||||
return import('../../account_timeline');
|
||||
}
|
||||
|
||||
export function AccountGallery () {
|
||||
return import(/* webpackChunkName: "features/account_gallery" */'../../account_gallery');
|
||||
return import('../../account_gallery');
|
||||
}
|
||||
|
||||
export function AccountFeatured() {
|
||||
return import(/* webpackChunkName: "features/account_featured" */'../../account_featured');
|
||||
return import('../../account_featured');
|
||||
}
|
||||
|
||||
export function Followers () {
|
||||
return import(/* webpackChunkName: "features/followers" */'../../followers');
|
||||
return import('../../followers');
|
||||
}
|
||||
|
||||
export function Following () {
|
||||
return import(/* webpackChunkName: "features/following" */'../../following');
|
||||
return import('../../following');
|
||||
}
|
||||
|
||||
export function Reblogs () {
|
||||
return import(/* webpackChunkName: "features/reblogs" */'../../reblogs');
|
||||
return import('../../reblogs');
|
||||
}
|
||||
|
||||
export function Favourites () {
|
||||
return import(/* webpackChunkName: "features/favourites" */'../../favourites');
|
||||
return import('../../favourites');
|
||||
}
|
||||
|
||||
export function FollowRequests () {
|
||||
return import(/* webpackChunkName: "features/follow_requests" */'../../follow_requests');
|
||||
return import('../../follow_requests');
|
||||
}
|
||||
|
||||
export function FavouritedStatuses () {
|
||||
return import(/* webpackChunkName: "features/favourited_statuses" */'../../favourited_statuses');
|
||||
return import('../../favourited_statuses');
|
||||
}
|
||||
|
||||
export function FollowedTags () {
|
||||
return import(/* webpackChunkName: "features/followed_tags" */'../../followed_tags');
|
||||
return import('../../followed_tags');
|
||||
}
|
||||
|
||||
export function BookmarkedStatuses () {
|
||||
return import(/* webpackChunkName: "features/bookmarked_statuses" */'../../bookmarked_statuses');
|
||||
return import('../../bookmarked_statuses');
|
||||
}
|
||||
|
||||
export function Blocks () {
|
||||
return import(/* webpackChunkName: "features/blocks" */'../../blocks');
|
||||
return import('../../blocks');
|
||||
}
|
||||
|
||||
export function DomainBlocks () {
|
||||
return import(/* webpackChunkName: "features/domain_blocks" */'../../domain_blocks');
|
||||
return import('../../domain_blocks');
|
||||
}
|
||||
|
||||
export function Mutes () {
|
||||
return import(/* webpackChunkName: "features/mutes" */'../../mutes');
|
||||
return import('../../mutes');
|
||||
}
|
||||
|
||||
export function MuteModal () {
|
||||
return import(/* webpackChunkName: "modals/mute_modal" */'../components/mute_modal');
|
||||
return import('../components/mute_modal');
|
||||
}
|
||||
|
||||
export function BlockModal () {
|
||||
return import(/* webpackChunkName: "modals/block_modal" */'../components/block_modal');
|
||||
return import('../components/block_modal');
|
||||
}
|
||||
|
||||
export function DomainBlockModal () {
|
||||
return import(/* webpackChunkName: "modals/domain_block_modal" */'../components/domain_block_modal');
|
||||
return import('../components/domain_block_modal');
|
||||
}
|
||||
|
||||
export function ReportModal () {
|
||||
return import(/* webpackChunkName: "modals/report_modal" */'../components/report_modal');
|
||||
return import('../components/report_modal');
|
||||
}
|
||||
|
||||
export function IgnoreNotificationsModal () {
|
||||
return import(/* webpackChunkName: "modals/domain_block_modal" */'../components/ignore_notifications_modal');
|
||||
return import('../components/ignore_notifications_modal');
|
||||
}
|
||||
|
||||
export function MediaGallery () {
|
||||
return import(/* webpackChunkName: "status/media_gallery" */'../../../components/media_gallery');
|
||||
return import('../../../components/media_gallery');
|
||||
}
|
||||
|
||||
export function Video () {
|
||||
return import(/* webpackChunkName: "features/video" */'../../video');
|
||||
return import('../../video');
|
||||
}
|
||||
|
||||
export function EmbedModal () {
|
||||
return import(/* webpackChunkName: "modals/embed_modal" */'../components/embed_modal');
|
||||
return import('../components/embed_modal');
|
||||
}
|
||||
|
||||
export function ListAdder () {
|
||||
return import(/*webpackChunkName: "features/list_adder" */'../../list_adder');
|
||||
return import('../../list_adder');
|
||||
}
|
||||
|
||||
export function Tesseract () {
|
||||
return import(/*webpackChunkName: "tesseract" */'tesseract.js');
|
||||
return import('tesseract.js');
|
||||
}
|
||||
|
||||
export function Audio () {
|
||||
return import(/* webpackChunkName: "features/audio" */'../../audio');
|
||||
return import('../../audio');
|
||||
}
|
||||
|
||||
export function Directory () {
|
||||
return import(/* webpackChunkName: "features/directory" */'../../directory');
|
||||
return import('../../directory');
|
||||
}
|
||||
|
||||
export function OnboardingProfile () {
|
||||
return import(/* webpackChunkName: "features/onboarding" */'../../onboarding/profile');
|
||||
return import('../../onboarding/profile');
|
||||
}
|
||||
|
||||
export function OnboardingFollows () {
|
||||
return import(/* webpackChunkName: "features/onboarding" */'../../onboarding/follows');
|
||||
return import('../../onboarding/follows');
|
||||
}
|
||||
|
||||
export function CompareHistoryModal () {
|
||||
return import(/*webpackChunkName: "modals/compare_history_modal" */'../components/compare_history_modal');
|
||||
return import('../components/compare_history_modal');
|
||||
}
|
||||
|
||||
export function Explore () {
|
||||
return import(/* webpackChunkName: "features/explore" */'../../explore');
|
||||
return import('../../explore');
|
||||
}
|
||||
|
||||
export function Search () {
|
||||
return import(/* webpackChunkName: "features/explore" */'../../search');
|
||||
return import('../../search');
|
||||
}
|
||||
|
||||
export function FilterModal () {
|
||||
return import(/*webpackChunkName: "modals/filter_modal" */'../components/filter_modal');
|
||||
return import('../components/filter_modal');
|
||||
}
|
||||
|
||||
export function InteractionModal () {
|
||||
return import(/*webpackChunkName: "modals/interaction_modal" */'../../interaction_modal');
|
||||
return import('../../interaction_modal');
|
||||
}
|
||||
|
||||
export function SubscribedLanguagesModal () {
|
||||
return import(/*webpackChunkName: "modals/subscribed_languages_modal" */'../../subscribed_languages_modal');
|
||||
return import('../../subscribed_languages_modal');
|
||||
}
|
||||
|
||||
export function ClosedRegistrationsModal () {
|
||||
return import(/*webpackChunkName: "modals/closed_registrations_modal" */'../../closed_registrations_modal');
|
||||
return import('../../closed_registrations_modal');
|
||||
}
|
||||
|
||||
export function About () {
|
||||
return import(/*webpackChunkName: "features/about" */'../../about');
|
||||
return import('../../about');
|
||||
}
|
||||
|
||||
export function PrivacyPolicy () {
|
||||
return import(/*webpackChunkName: "features/privacy_policy" */'../../privacy_policy');
|
||||
return import('../../privacy_policy');
|
||||
}
|
||||
|
||||
export function TermsOfService () {
|
||||
return import(/*webpackChunkName: "features/terms_of_service" */'../../terms_of_service');
|
||||
return import('../../terms_of_service');
|
||||
}
|
||||
|
||||
export function NotificationRequests () {
|
||||
return import(/*webpackChunkName: "features/notifications/requests" */'../../notifications/requests');
|
||||
return import('../../notifications/requests');
|
||||
}
|
||||
|
||||
export function NotificationRequest () {
|
||||
return import(/*webpackChunkName: "features/notifications/request" */'../../notifications/request');
|
||||
return import('../../notifications/request');
|
||||
}
|
||||
|
||||
export function LinkTimeline () {
|
||||
return import(/*webpackChunkName: "features/link_timeline" */'../../link_timeline');
|
||||
return import('../../link_timeline');
|
||||
}
|
||||
|
||||
export function AnnualReportModal () {
|
||||
return import(/*webpackChunkName: "modals/annual_report_modal" */'../components/annual_report_modal');
|
||||
return import('../components/annual_report_modal');
|
||||
}
|
||||
|
||||
export function ListEdit () {
|
||||
return import(/*webpackChunkName: "features/lists" */'../../lists/new');
|
||||
return import('../../lists/new');
|
||||
}
|
||||
|
||||
export function ListMembers () {
|
||||
return import(/* webpackChunkName: "features/lists" */'../../lists/members');
|
||||
return import('../../lists/members');
|
||||
}
|
||||
|
||||
@ -1,41 +0,0 @@
|
||||
/* @preval */
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const { defineMessages } = require('react-intl');
|
||||
|
||||
const messages = defineMessages({
|
||||
mentioned_you: { id: 'notification.mentioned_you', defaultMessage: '{name} mentioned you' },
|
||||
});
|
||||
|
||||
const filtered = {};
|
||||
const filenames = fs.readdirSync(path.resolve(__dirname, '../locales'));
|
||||
|
||||
filenames.forEach(filename => {
|
||||
if (!filename.match(/\.json$/)) return;
|
||||
|
||||
const content = fs.readFileSync(path.resolve(__dirname, `../locales/${filename}`), 'utf-8');
|
||||
const full = JSON.parse(content);
|
||||
const locale = filename.split('.')[0];
|
||||
|
||||
filtered[locale] = {
|
||||
'notification.favourite': full['notification.favourite'] || '',
|
||||
'notification.follow': full['notification.follow'] || '',
|
||||
'notification.follow_request': full['notification.follow_request'] || '',
|
||||
'notification.mention': full[messages.mentioned_you.id] || '',
|
||||
'notification.reblog': full['notification.reblog'] || '',
|
||||
'notification.poll': full['notification.poll'] || '',
|
||||
'notification.status': full['notification.status'] || '',
|
||||
'notification.update': full['notification.update'] || '',
|
||||
'notification.admin.sign_up': full['notification.admin.sign_up'] || '',
|
||||
|
||||
'status.show_more': full['status.show_more'] || '',
|
||||
'status.reblog': full['status.reblog'] || '',
|
||||
'status.favourite': full['status.favourite'] || '',
|
||||
|
||||
'notifications.group': full['notifications.group'] || '',
|
||||
};
|
||||
});
|
||||
|
||||
module.exports = JSON.parse(JSON.stringify(filtered));
|
||||
@ -1,7 +1,11 @@
|
||||
export function isDevelopment() {
|
||||
return process.env.NODE_ENV === 'development';
|
||||
if (typeof process !== 'undefined')
|
||||
return process.env.NODE_ENV === 'development';
|
||||
else return import.meta.env.DEV;
|
||||
}
|
||||
|
||||
export function isProduction() {
|
||||
return process.env.NODE_ENV === 'production';
|
||||
if (typeof process !== 'undefined')
|
||||
return process.env.NODE_ENV === 'production';
|
||||
else return import.meta.env.PROD;
|
||||
}
|
||||
|
||||
@ -0,0 +1,59 @@
|
||||
declare module 'virtual:mastodon-emoji-compressed' {
|
||||
import type { BaseEmoji, EmojiData, NimbleEmojiIndex } from 'emoji-mart';
|
||||
import type { Category, Data, Emoji } from 'emoji-mart/dist-es/utils/data';
|
||||
/*
|
||||
* The 'search' property, although not defined in the [`Emoji`]{@link node_modules/@types/emoji-mart/dist-es/utils/data.d.ts#Emoji} type,
|
||||
* is used in the application.
|
||||
* This could be due to an oversight by the library maintainer.
|
||||
* The `search` property is defined and used [here]{@link node_modules/emoji-mart/dist/utils/data.js#uncompress}.
|
||||
*/
|
||||
export type Search = string;
|
||||
/*
|
||||
* The 'skins' property does not exist in the application data.
|
||||
* This could be a potential area of refactoring or error handling.
|
||||
* The non-existence of 'skins' property is evident at [this location]{@link app/javascript/mastodon/features/emoji/emoji_compressed.js:121}.
|
||||
*/
|
||||
type Skins = null;
|
||||
|
||||
type Filename = string;
|
||||
type UnicodeFilename = string;
|
||||
export type FilenameData = [
|
||||
filename: Filename,
|
||||
unicodeFilename?: UnicodeFilename,
|
||||
][];
|
||||
export type ShortCodesToEmojiDataKey =
|
||||
| EmojiData['id']
|
||||
| BaseEmoji['native']
|
||||
| keyof NimbleEmojiIndex['emojis'];
|
||||
|
||||
type SearchData = [
|
||||
BaseEmoji['native'],
|
||||
Emoji['short_names'],
|
||||
Search,
|
||||
Emoji['unified'],
|
||||
];
|
||||
|
||||
export type ShortCodesToEmojiData = Record<
|
||||
ShortCodesToEmojiDataKey,
|
||||
[FilenameData, SearchData]
|
||||
>;
|
||||
type EmojisWithoutShortCodes = FilenameData;
|
||||
|
||||
type EmojiCompressed = [
|
||||
ShortCodesToEmojiData,
|
||||
Skins,
|
||||
Category[],
|
||||
Data['aliases'],
|
||||
EmojisWithoutShortCodes,
|
||||
Data,
|
||||
];
|
||||
|
||||
/*
|
||||
* `emoji_compressed.js` uses `babel-plugin-preval`, which makes it difficult to convert to TypeScript.
|
||||
* As a temporary solution, we are allowing a default export here to apply the TypeScript type `EmojiCompressed` to the JS file export.
|
||||
* - {@link app/javascript/mastodon/features/emoji/emoji_compressed.js}
|
||||
*/
|
||||
declare const emojiCompressed: EmojiCompressed;
|
||||
|
||||
export default emojiCompressed; // eslint-disable-line import/no-default-export
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
- content_for :header_tags do
|
||||
%meta{ name: 'robots', content: 'noindex' }/
|
||||
|
||||
= javascript_pack_tag 'remote_interaction_helper', crossorigin: 'anonymous'
|
||||
= vite_typescript_tag 'remote_interaction_helper.ts', crossorigin: 'anonymous'
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
- content_for :header_tags do
|
||||
= render_initial_state
|
||||
= javascript_pack_tag 'share', crossorigin: 'anonymous'
|
||||
= vite_typescript_tag 'share.tsx', crossorigin: 'anonymous'
|
||||
|
||||
#mastodon-compose{ data: { props: Oj.dump(default_props) } }
|
||||
|
||||
@ -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;
|
||||
};
|
||||
@ -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")
|
||||
@ -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
|
||||
@ -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
|
||||
@ -1,17 +1,23 @@
|
||||
{
|
||||
"all": {
|
||||
"sourceCodeDir": "app/javascript",
|
||||
"additionalEntrypoints": ["~/{icons,images}/**/*", "~/styles/*.scss"],
|
||||
"additionalEntrypoints": [
|
||||
"~/{fonts,icons,images}/**/*",
|
||||
"~/styles/entrypoints/*.scss"
|
||||
],
|
||||
"watchAdditionalPaths": []
|
||||
},
|
||||
"production": {
|
||||
"publicOutputDir": "packs"
|
||||
},
|
||||
"development": {
|
||||
"autoBuild": true,
|
||||
"publicOutputDir": "vite-dev",
|
||||
"publicOutputDir": "packs-dev",
|
||||
"port": 3036
|
||||
},
|
||||
"test": {
|
||||
"autoBuild": true,
|
||||
"publicOutputDir": "vite-test",
|
||||
"publicOutputDir": "packs-test",
|
||||
"port": 3037
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,27 @@
|
||||
import type { Plugin } from 'vite';
|
||||
|
||||
export function MastodonEmojiCompressed(): Plugin {
|
||||
const virtualModuleId = 'virtual:mastodon-emoji-compressed';
|
||||
const resolvedVirtualModuleId = '\0' + virtualModuleId;
|
||||
|
||||
return {
|
||||
name: 'mastodon-emoji-compressed',
|
||||
resolveId(id) {
|
||||
if (id === virtualModuleId) {
|
||||
return resolvedVirtualModuleId;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
async load(id) {
|
||||
if (id === resolvedVirtualModuleId) {
|
||||
const { default: emojiCompressed } = await import(
|
||||
'../../app/javascript/mastodon/features/emoji/emoji_compressed.mjs'
|
||||
);
|
||||
return `export default ${JSON.stringify(emojiCompressed)};`;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,102 @@
|
||||
/* 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 { defineMessages } from 'react-intl';
|
||||
|
||||
import type { Plugin, ResolvedConfig } from 'vite';
|
||||
|
||||
const translations = defineMessages({
|
||||
mentioned_you: {
|
||||
id: 'notification.mentioned_you',
|
||||
defaultMessage: '{name} mentioned you',
|
||||
},
|
||||
});
|
||||
|
||||
const CUSTOM_TRANSLATIONS = {
|
||||
'notification.mention': translations.mentioned_you.id,
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
Object.entries(CUSTOM_TRANSLATIONS).forEach(([key, value]) => {
|
||||
const translation = full[value];
|
||||
if (translation) filteredLocale[key] = translation;
|
||||
});
|
||||
|
||||
filteredLocales[locale] = filteredLocale;
|
||||
});
|
||||
|
||||
return `const locales = ${JSON.stringify(filteredLocales)}; \n export default locales;`;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
};
|
||||
}
|
||||
@ -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,
|
||||
};
|
||||
@ -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),
|
||||
},
|
||||
});
|
||||
@ -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'),
|
||||
}),
|
||||
],
|
||||
});
|
||||
@ -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',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@ -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),
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@ -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,
|
||||
};
|
||||
@ -1,8 +0,0 @@
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports = {};
|
||||
} else {
|
||||
module.exports = {
|
||||
test: /\.js$/,
|
||||
loader: 'mark-loader',
|
||||
};
|
||||
}
|
||||
@ -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,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@ -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]',
|
||||
},
|
||||
},
|
||||
};
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
@ -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
|
||||
@ -1,34 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Disable this task as we use pnpm
|
||||
|
||||
require 'semantic_range'
|
||||
|
||||
Rake::Task['webpacker:check_yarn'].clear
|
||||
|
||||
namespace :webpacker do
|
||||
desc 'Verifies if Yarn is installed'
|
||||
task check_yarn: :environment do
|
||||
begin
|
||||
yarn_version = `yarn --version`.strip
|
||||
raise Errno::ENOENT if yarn_version.blank?
|
||||
|
||||
yarn_range = '>=4 <5'
|
||||
is_valid = begin
|
||||
SemanticRange.satisfies?(yarn_version, yarn_range)
|
||||
rescue
|
||||
false
|
||||
end
|
||||
|
||||
unless is_valid
|
||||
warn "Mastodon and Webpacker requires Yarn \"#{yarn_range}\" and you are using #{yarn_version}"
|
||||
warn 'Exiting!'
|
||||
exit!
|
||||
end
|
||||
rescue Errno::ENOENT
|
||||
warn 'Yarn not installed. Please see the Mastodon documentation to install the correct version.'
|
||||
warn 'Exiting!'
|
||||
exit!
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,124 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module ViteRuby::ManifestIntegrityExtension
|
||||
def path_and_integrity_for(name, **)
|
||||
entry = lookup!(name, **)
|
||||
|
||||
{ path: entry.fetch('file'), integrity: entry.fetch('integrity', nil) }
|
||||
end
|
||||
|
||||
# Find a manifest entry by the *final* file name
|
||||
def integrity_hash_for_file(file_name)
|
||||
@integrity_cache ||= {}
|
||||
@integrity_cache[file_name] ||= begin
|
||||
entry = manifest.find { |_key, entry| entry['file'] == file_name }
|
||||
|
||||
entry[1].fetch('integrity', nil) if entry
|
||||
end
|
||||
end
|
||||
|
||||
def resolve_entries_with_integrity(*names, **options)
|
||||
entries = names.map { |name| lookup!(name, **options) }
|
||||
script_paths = entries.map do |entry|
|
||||
{
|
||||
file: entry.fetch('file'),
|
||||
# TODO: Secure this so we require the integrity hash outside of dev
|
||||
integrity: entry['integrity'],
|
||||
}
|
||||
end
|
||||
|
||||
imports = dev_server_running? ? [] : entries.flat_map { |entry| entry['imports'] }.compact
|
||||
|
||||
{
|
||||
scripts: script_paths,
|
||||
imports: imports.filter_map { |entry| { file: entry.fetch('file'), integrity: entry.fetch('integrity') } }.uniq,
|
||||
stylesheets: dev_server_running? ? [] : (entries + imports).flat_map { |entry| entry['css'] }.compact.uniq,
|
||||
}
|
||||
end
|
||||
|
||||
# We need to override this method to not include the manifest, as in our case it is too large and will cause a JSON max nesting error rather than raising the expected exception
|
||||
def missing_entry_error(name, **)
|
||||
raise ViteRuby::MissingEntrypointError.new(
|
||||
file_name: resolve_entry_name(name, **),
|
||||
last_build: builder.last_build_metadata,
|
||||
manifest: '',
|
||||
config: config
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
ViteRuby::Manifest.prepend ViteRuby::ManifestIntegrityExtension
|
||||
|
||||
module ViteRails::TagHelpers::IntegrityExtension
|
||||
def vite_javascript_tag(*names,
|
||||
type: 'module',
|
||||
asset_type: :javascript,
|
||||
skip_preload_tags: false,
|
||||
skip_style_tags: false,
|
||||
crossorigin: 'anonymous',
|
||||
media: 'screen',
|
||||
**options)
|
||||
entries = vite_manifest.resolve_entries_with_integrity(*names, type: asset_type)
|
||||
|
||||
''.html_safe.tap do |tags|
|
||||
entries.fetch(:scripts).each do |script|
|
||||
tags << javascript_include_tag(
|
||||
script[:file],
|
||||
integrity: script[:integrity],
|
||||
crossorigin: crossorigin,
|
||||
type: type,
|
||||
extname: false,
|
||||
**options
|
||||
)
|
||||
end
|
||||
|
||||
unless skip_preload_tags
|
||||
entries.fetch(:imports).each do |import|
|
||||
tags << vite_preload_tag(import[:file], integrity: import[:integrity], crossorigin: crossorigin, **options)
|
||||
end
|
||||
end
|
||||
|
||||
options[:extname] = false if Rails::VERSION::MAJOR >= 7
|
||||
|
||||
unless skip_style_tags
|
||||
entries.fetch(:stylesheets).each do |stylesheet|
|
||||
# This is for stylesheets imported from Javascript. The entry for the JS entrypoint only contains the final CSS file name, so we need to look it up in the manifest
|
||||
tags << stylesheet_link_tag(
|
||||
stylesheet,
|
||||
integrity: vite_manifest.integrity_hash_for_file(stylesheet),
|
||||
media: media,
|
||||
**options
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def vite_stylesheet_tag(*names, **options)
|
||||
''.html_safe.tap do |tags|
|
||||
names.each do |name|
|
||||
entry = vite_manifest.path_and_integrity_for(name, type: :stylesheet)
|
||||
|
||||
options[:extname] = false if Rails::VERSION::MAJOR >= 7
|
||||
|
||||
tags << stylesheet_link_tag(entry[:path], integrity: entry[:integrity], **options)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def vite_preload_file_tag(name,
|
||||
asset_type: :javascript,
|
||||
crossorigin: 'anonymous', **options)
|
||||
''.html_safe.tap do |tags|
|
||||
entries = vite_manifest.resolve_entries_with_integrity(name, type: asset_type)
|
||||
|
||||
entries.fetch(:scripts).each do |script|
|
||||
tags << vite_preload_tag(script[:file], integrity: script[:integrity], crossorigin: crossorigin, **options)
|
||||
end
|
||||
end
|
||||
rescue ViteRuby::MissingEntrypointError
|
||||
# Ignore this error, it is not critical if the file is not preloaded
|
||||
end
|
||||
end
|
||||
|
||||
ViteRails::TagHelpers.prepend ViteRails::TagHelpers::IntegrityExtension
|
||||
@ -1,27 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Webpacker::HelperExtensions
|
||||
def javascript_pack_tag(name, **options)
|
||||
src, integrity = current_webpacker_instance.manifest.lookup!(name, type: :javascript, with_integrity: true)
|
||||
javascript_include_tag(src, options.merge(integrity: integrity))
|
||||
end
|
||||
|
||||
def stylesheet_pack_tag(name, **options)
|
||||
src, integrity = current_webpacker_instance.manifest.lookup!(name, type: :stylesheet, with_integrity: true)
|
||||
stylesheet_link_tag(src, options.merge(integrity: integrity))
|
||||
end
|
||||
|
||||
def preload_pack_asset(name, **options)
|
||||
src, integrity = current_webpacker_instance.manifest.lookup!(name, with_integrity: true)
|
||||
|
||||
# This attribute will only work if the assets are on a different domain.
|
||||
# And Webpack will (correctly) only add it in this case, so we need to conditionally set it here
|
||||
# otherwise the preloaded request and the real request will have different crossorigin values
|
||||
# and the preloaded file wont be loaded
|
||||
crossorigin = 'anonymous' if Rails.configuration.action_controller.asset_host.present?
|
||||
|
||||
preload_link_tag(src, options.merge(integrity: integrity, crossorigin: crossorigin))
|
||||
end
|
||||
end
|
||||
|
||||
Webpacker::Helper.prepend(Webpacker::HelperExtensions)
|
||||
@ -1,17 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Webpacker::ManifestExtensions
|
||||
def lookup(name, pack_type = {})
|
||||
asset = super
|
||||
|
||||
if pack_type[:with_integrity] && asset.respond_to?(:dig)
|
||||
[asset['src'], asset['integrity']]
|
||||
elsif asset.respond_to?(:dig)
|
||||
asset['src']
|
||||
else
|
||||
asset
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Webpacker::Manifest.prepend(Webpacker::ManifestExtensions)
|
||||
@ -1,15 +0,0 @@
|
||||
const postcssPresetEnv = require('postcss-preset-env');
|
||||
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = ({ env }) => ({
|
||||
plugins: [
|
||||
postcssPresetEnv({
|
||||
features: {
|
||||
'logical-properties-and-values': false
|
||||
}
|
||||
}),
|
||||
env === 'production' ? require('cssnano') : '',
|
||||
],
|
||||
});
|
||||
|
||||
module.exports = config;
|
||||
@ -1,58 +1,154 @@
|
||||
/// <reference types="vitest" />
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
import { optimizeLodashImports } from '@optimize-lodash/rollup-plugin';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import RailsPlugin from 'vite-plugin-rails';
|
||||
import { PluginOption } from 'vite';
|
||||
import svgr from 'vite-plugin-svgr';
|
||||
import { defineConfig, configDefaults } from 'vitest/config';
|
||||
|
||||
const sourceCodeDir = 'app/javascript';
|
||||
const items = fs.readdirSync(sourceCodeDir);
|
||||
const directories = items.filter((item) =>
|
||||
fs.lstatSync(path.join(sourceCodeDir, item)).isDirectory(),
|
||||
);
|
||||
const aliasesFromJavascriptRoot: Record<string, string> = {};
|
||||
directories.forEach((directory) => {
|
||||
aliasesFromJavascriptRoot[directory] = path.resolve(
|
||||
__dirname,
|
||||
sourceCodeDir,
|
||||
directory,
|
||||
);
|
||||
});
|
||||
|
||||
export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
...aliasesFromJavascriptRoot,
|
||||
import { visualizer } from 'rollup-plugin-visualizer';
|
||||
import RailsPlugin from 'vite-plugin-rails';
|
||||
import { VitePWA } from 'vite-plugin-pwa';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import yaml from 'js-yaml';
|
||||
|
||||
import { defineConfig, UserConfigFnPromise, UserConfig } from 'vite';
|
||||
import postcssPresetEnv from 'postcss-preset-env';
|
||||
|
||||
import { MastodonServiceWorkerLocales } from './config/vite/plugin-sw-locales';
|
||||
import { MastodonEmojiCompressed } from './config/vite/plugin-emoji-compressed';
|
||||
|
||||
const jsRoot = path.resolve(__dirname, 'app/javascript');
|
||||
const themesFile = path.resolve(__dirname, 'config/themes.yml');
|
||||
|
||||
export const config: UserConfigFnPromise = async ({ mode, command }) => {
|
||||
const entrypoints: Record<string, string> = {}; // All JS entrypoints are taken care of by Vite Ruby
|
||||
|
||||
// Get all files mentioned in the themes.yml file.
|
||||
const themesString = await fs.readFile(themesFile, 'utf8');
|
||||
const themes = yaml.load(themesString, {
|
||||
filename: 'themes.yml',
|
||||
schema: yaml.FAILSAFE_SCHEMA,
|
||||
});
|
||||
|
||||
if (!themes || typeof themes !== 'object') {
|
||||
throw new Error('Invalid themes.yml file');
|
||||
}
|
||||
|
||||
for (const themePath of Object.values(themes)) {
|
||||
if (
|
||||
typeof themePath !== 'string' ||
|
||||
themePath.split('.').length !== 2 || // Ensure it has exactly one period
|
||||
!themePath.endsWith('css')
|
||||
) {
|
||||
console.warn(`Invalid theme path "${themePath}" in themes.yml, skipping`);
|
||||
continue;
|
||||
}
|
||||
entrypoints[path.basename(themePath)] = path.resolve(jsRoot, themePath);
|
||||
}
|
||||
|
||||
return {
|
||||
root: jsRoot,
|
||||
css: {
|
||||
postcss: {
|
||||
plugins: [
|
||||
postcssPresetEnv({
|
||||
features: {
|
||||
'logical-properties-and-values': false,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
RailsPlugin(),
|
||||
react({
|
||||
include: ['**/*.jsx', '**/*.tsx'],
|
||||
babel: {
|
||||
plugins: ['formatjs', 'preval', 'transform-react-remove-prop-types'],
|
||||
server: {
|
||||
headers: {
|
||||
// This is needed in dev environment because we load the worker from `/dev-sw/dev-sw.js`,
|
||||
// but it needs to be scoped to the whole domain
|
||||
'Service-Worker-Allowed': '/',
|
||||
},
|
||||
}),
|
||||
svgr(),
|
||||
],
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
include: [
|
||||
...configDefaults.include,
|
||||
'**/__tests__/**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
|
||||
],
|
||||
exclude: [
|
||||
...configDefaults.exclude,
|
||||
'**/node_modules/**',
|
||||
'vendor/**',
|
||||
'config/**',
|
||||
'log/**',
|
||||
'public/**',
|
||||
'tmp/**',
|
||||
},
|
||||
build: {
|
||||
commonjsOptions: { transformMixedEsModules: true },
|
||||
chunkSizeWarningLimit: 1 * 1024 * 1024, // 1MB
|
||||
sourcemap: true,
|
||||
rollupOptions: {
|
||||
input: entrypoints,
|
||||
output: {
|
||||
chunkFileNames({ facadeModuleId, name }) {
|
||||
if (!facadeModuleId) {
|
||||
return '[name]-[hash].js';
|
||||
}
|
||||
if (/mastodon\/locales\/[a-zA-Z\-]+\.json/.exec(facadeModuleId)) {
|
||||
// put all locale files in `intl/`
|
||||
return 'intl/[name]-[hash].js';
|
||||
} else if (/node_modules\/@formatjs\//.exec(facadeModuleId)) {
|
||||
// use a custom name for formatjs polyfill files
|
||||
const newName = /node_modules\/@formatjs\/([^/]+)\//.exec(
|
||||
facadeModuleId,
|
||||
);
|
||||
|
||||
if (newName?.[1]) {
|
||||
return `intl/[name]-${newName[1]}-[hash].js`;
|
||||
}
|
||||
} else if (name === 'index') {
|
||||
// Use a custom name for chunks, to avoid having too many of them called "index"
|
||||
const parts = facadeModuleId.split('/');
|
||||
|
||||
const parent = parts.at(-2);
|
||||
|
||||
if (parent) {
|
||||
return `${parent}-[name]-[hash].js`;
|
||||
}
|
||||
}
|
||||
return '[name]-[hash].js';
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
tsconfigPaths(),
|
||||
RailsPlugin({
|
||||
compress: mode === 'production' && command === 'build',
|
||||
sri: {
|
||||
manifestPaths: ['.vite/manifest.json', '.vite/manifest-assets.json'],
|
||||
},
|
||||
}),
|
||||
react({
|
||||
babel: {
|
||||
plugins: ['formatjs', 'transform-react-remove-prop-types'],
|
||||
},
|
||||
}),
|
||||
MastodonServiceWorkerLocales(),
|
||||
MastodonEmojiCompressed(),
|
||||
VitePWA({
|
||||
srcDir: 'mastodon/service_worker',
|
||||
// We need to use injectManifest because we use our own service worker
|
||||
strategies: 'injectManifest',
|
||||
manifest: false,
|
||||
injectRegister: false,
|
||||
injectManifest: {
|
||||
// Do not inject a manifest, we don't use precache
|
||||
injectionPoint: undefined,
|
||||
buildPlugins: {
|
||||
vite: [
|
||||
// Provide a virtual import with only the locales used in the ServiceWorker
|
||||
MastodonServiceWorkerLocales(),
|
||||
MastodonEmojiCompressed(),
|
||||
],
|
||||
},
|
||||
},
|
||||
// Force the output location, because we have a symlink in `public/sw.js`
|
||||
outDir: path.resolve(__dirname, 'public/packs'),
|
||||
devOptions: {
|
||||
enabled: true,
|
||||
type: 'module',
|
||||
},
|
||||
}),
|
||||
svgr(),
|
||||
// Old library types need to be converted
|
||||
optimizeLodashImports() as PluginOption,
|
||||
!!process.env.ANALYZE_BUNDLE_SIZE && (visualizer() as PluginOption),
|
||||
],
|
||||
globals: true,
|
||||
},
|
||||
});
|
||||
} satisfies UserConfig;
|
||||
};
|
||||
|
||||
export default defineConfig(config);
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
import { configDefaults, defineConfig } from 'vitest/config';
|
||||
|
||||
import { config as viteConfig } from './vite.config.mjs';
|
||||
|
||||
export default defineConfig(async (context) => {
|
||||
return {
|
||||
...(await viteConfig(context)),
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
include: [
|
||||
...configDefaults.include,
|
||||
'**/__tests__/**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
|
||||
],
|
||||
exclude: [
|
||||
...configDefaults.exclude,
|
||||
'**/node_modules/**',
|
||||
'vendor/**',
|
||||
'config/**',
|
||||
'log/**',
|
||||
'public/**',
|
||||
'tmp/**',
|
||||
],
|
||||
globals: true,
|
||||
},
|
||||
};
|
||||
});
|
||||
Loading…
Reference in New Issue