diff --git a/build/sw_plugin.js b/build/sw_plugin.js index ee952d229..39cc31405 100644 --- a/build/sw_plugin.js +++ b/build/sw_plugin.js @@ -3,7 +3,6 @@ import { dirname, resolve } from 'node:path' import { fileURLToPath } from 'node:url' import * as esbuild from 'esbuild' import { build } from 'vite' -import { exactRegex } from '@rolldown/pluginutils' import { generateServiceWorkerMessages, @@ -16,15 +15,22 @@ const getSWMessagesAsText = async () => { } const projectRoot = dirname(dirname(fileURLToPath(import.meta.url))) +const swEnvName = 'virtual:pleroma-fe/service_worker_env' +const swEnvNameResolved = '\0' + swEnvName const getDevSwEnv = () => `self.serviceWorkerOption = { assets: [] };` const getProdSwEnv = ({ assets }) => `self.serviceWorkerOption = { assets: ${JSON.stringify(assets)} };` -export const devSwPlugin = ({ swSrc, swDest }) => { +export const devSwPlugin = ({ swSrc, swDest, transformSW, alias }) => { const swFullSrc = resolve(projectRoot, swSrc) + const esbuildAlias = {} + Object.entries(alias).forEach(([source, dest]) => { + esbuildAlias[source] = dest.startsWith('/') ? projectRoot + dest : dest + }) return { - name: 'dev-sw-plugin', apply: 'serve', + name: 'dev-sw-plugin', + apply: 'serve', configResolved() { /* no-op */ }, @@ -32,35 +38,174 @@ export const devSwPlugin = ({ swSrc, swDest }) => { const name = id.startsWith('/') ? id.slice(1) : id if (name === swDest) { return swFullSrc + } else if (name === swEnvName) { + return swEnvNameResolved } return null }, async load(id) { if (id === swFullSrc) { return readFile(swFullSrc, 'utf-8') + } else if (id === swEnvNameResolved) { + return getDevSwEnv() + } + return null + }, + /** + * vite does not bundle the service worker + * during dev, and firefox does not support ESM as service worker + * https://bugzilla.mozilla.org/show_bug.cgi?id=1360870 + */ + async transform(code, id) { + if (id === swFullSrc && transformSW) { + const res = await esbuild.build({ + entryPoints: [swSrc], + bundle: true, + write: false, + outfile: 'sw-pleroma.js', + alias: esbuildAlias, + plugins: [ + { + name: 'vite-like-root-resolve', + setup(b) { + b.onResolve({ filter: new RegExp(/^\//) }, (args) => ({ + path: resolve(projectRoot, args.path.slice(1)), + })) + }, + }, + { + name: 'sw-messages', + setup(b) { + b.onResolve( + { filter: new RegExp('^' + swMessagesName + '$') }, + (args) => ({ + path: args.path, + namespace: 'sw-messages', + }), + ) + b.onLoad( + { filter: /.*/, namespace: 'sw-messages' }, + async () => ({ + contents: await getSWMessagesAsText(), + }), + ) + }, + }, + { + name: 'sw-env', + setup(b) { + b.onResolve( + { filter: new RegExp('^' + swEnvName + '$') }, + (args) => ({ + path: args.path, + namespace: 'sw-env', + }), + ) + b.onLoad({ filter: /.*/, namespace: 'sw-env' }, () => ({ + contents: getDevSwEnv(), + })) + }, + }, + ], + }) + const text = res.outputFiles[0].text + return text + } + }, + } +} + +// Idea taken from +// https://github.com/vite-pwa/vite-plugin-pwa/blob/main/src/plugins/build.ts +// rollup does not support compiling to iife if we want to code-split; +// however, we must compile the service worker to iife because of browser support. +// Run another vite build just for the service worker targeting iife at +// the end of the build. +export const buildSwPlugin = ({ swSrc, swDest }) => { + let config + return { + name: 'build-sw-plugin', + enforce: 'post', + apply: 'build', + configResolved(resolvedConfig) { + config = { + define: resolvedConfig.define, + resolve: resolvedConfig.resolve, + plugins: [swMessagesPlugin()], + publicDir: false, + build: { + ...resolvedConfig.build, + lib: { + entry: swSrc, + formats: ['iife'], + name: 'sw_pleroma', + }, + emptyOutDir: false, + rolldownOptions: { + output: { + entryFileNames: swDest, + }, + }, + }, + configFile: false, + } + }, + generateBundle: { + order: 'post', + sequential: true, + async handler(_, bundle) { + const assets = Object.keys(bundle) + .filter((name) => !/\.map$/.test(name)) + .map((name) => '/' + name) + config.plugins.push({ + name: 'build-sw-env-plugin', + resolveId(id) { + if (id === swEnvName) { + return swEnvNameResolved + } + return null + }, + load(id) { + if (id === swEnvNameResolved) { + return getProdSwEnv({ assets }) + } + return null + }, + }) + }, + }, + closeBundle: { + order: 'post', + sequential: true, + async handler() { + console.info('Building service worker for production') + await build(config) + }, + }, + } +} + +const swMessagesName = 'virtual:pleroma-fe/service_worker_messages' +const swMessagesNameResolved = '\0' + swMessagesName + +export const swMessagesPlugin = () => { + return { + name: 'sw-messages-plugin', + resolveId(id) { + if (id === swMessagesName) { + Object.values(i18nFiles).forEach((f) => { + this.addWatchFile(f) + }) + return swMessagesNameResolved + } else { + return null + } + }, + async load(id) { + if (id === swMessagesNameResolved) { + return await getSWMessagesAsText() } return null }, } } - -export const swMessagesPlugin = () => { - const swMessagesName = 'virtual:pleroma-fe/service_worker_messages' - const swMessagesNameResolved = '\0' + swMessagesName - - return { - name: 'sw-messages-plugin', - resolveId: { - filter: { id: exactRegex(swMessagesName) }, - handler() { - return swMessagesNameResolved - } - }, - load: { - filter: { id: exactRegex(swMessagesNameResolved) }, - async handler () { - return await getSWMessagesAsText() - } - }, - } -} diff --git a/package.json b/package.json index 4376bcdcf..9bead54f9 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,6 @@ "lodash": "4.17.21", "msw": "2.10.5", "nightwatch": "3.12.2", - "oxc": "^1.0.1", "playwright": "1.57.0", "postcss": "8.5.6", "postcss-html": "^1.5.0", @@ -117,7 +116,6 @@ "stylelint-config-standard": "38.0.0", "vite": "^8.0.0", "vite-plugin-eslint2": "^5.1.0", - "vite-plugin-pwa": "^1.3.0", "vite-plugin-stylelint": "^6.1.0", "vitest": "^3.0.7", "vue-eslint-parser": "10.2.0" diff --git a/src/boot/routes.js b/src/boot/routes.js index b6ff1e193..5c87cf586 100644 --- a/src/boot/routes.js +++ b/src/boot/routes.js @@ -1,16 +1,17 @@ +import { defineAsyncComponent } from 'vue' + +import About from 'src/components/about/about.vue' import BookmarkTimeline from 'src/components/bookmark_timeline/bookmark_timeline.vue' import BubbleTimeline from 'src/components/bubble_timeline/bubble_timeline.vue' import ConversationPage from 'src/components/conversation-page/conversation-page.vue' import DMs from 'src/components/dm_timeline/dm_timeline.vue' import FriendsTimeline from 'src/components/friends_timeline/friends_timeline.vue' +import NavPanel from 'src/components/nav_panel/nav_panel.vue' import PublicAndExternalTimeline from 'src/components/public_and_external_timeline/public_and_external_timeline.vue' import PublicTimeline from 'src/components/public_timeline/public_timeline.vue' +import QuotesTimeline from 'src/components/quotes_timeline/quotes_timeline.vue' import RemoteUserResolver from 'src/components/remote_user_resolver/remote_user_resolver.vue' import TagTimeline from 'src/components/tag_timeline/tag_timeline.vue' -import { defineAsyncComponent } from 'vue' - -import NavPanel from 'src/components/nav_panel/nav_panel.vue' -import QuotesTimeline from 'src/components/quotes_timeline/quotes_timeline.vue' import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' @@ -141,14 +142,14 @@ export default (store) => { name: 'login', path: '/login', component: defineAsyncComponent( - () => import( 'src/components/auth_form/auth_form.js'), + () => import('src/components/auth_form/auth_form.js'), ), }, { name: 'shout-panel', path: '/shout-panel', component: defineAsyncComponent( - () => import( 'src/components/shout_panel/shout_panel.vue'), + () => import('src/components/shout_panel/shout_panel.vue'), ), props: () => ({ floating: false }), }, @@ -164,7 +165,7 @@ export default (store) => { name: 'search', path: '/search', component: defineAsyncComponent( - () => import( 'src/components/search/search.vue'), + () => import('src/components/search/search.vue'), ), props: (route) => ({ query: route.query.query }), }, @@ -172,29 +173,24 @@ export default (store) => { name: 'who-to-follow', path: '/who-to-follow', component: defineAsyncComponent( - () => import( 'src/components/who_to_follow/who_to_follow.vue'), + () => import('src/components/who_to_follow/who_to_follow.vue'), ), beforeEnter: validateAuthenticatedRoute, }, - { - name: 'about', - path: '/about', - component: defineAsyncComponent( - () => import( 'src/components/about/about.vue'), - ), - }, + { name: 'about', path: '/about', component: About }, { name: 'announcements', path: '/announcements', component: defineAsyncComponent( - () => import( 'src/components/announcements_page/announcements_page.vue'), + () => + import('src/components/announcements_page/announcements_page.vue'), ), }, { name: 'drafts', path: '/drafts', component: defineAsyncComponent( - () => import( 'src/components/drafts/drafts.vue'), + () => import('src/components/drafts/drafts.vue'), ), }, { @@ -215,28 +211,28 @@ export default (store) => { name: 'lists', path: '/lists', component: defineAsyncComponent( - () => import( 'src/components/lists/lists.vue'), + () => import('src/components/lists/lists.vue'), ), }, { name: 'lists-timeline', path: '/lists/:id', component: defineAsyncComponent( - () => import( 'src/components/lists_timeline/lists_timeline.vue'), + () => import('src/components/lists_timeline/lists_timeline.vue'), ), }, { name: 'lists-edit', path: '/lists/:id/edit', component: defineAsyncComponent( - () => import( 'src/components/lists_edit/lists_edit.vue'), + () => import('src/components/lists_edit/lists_edit.vue'), ), }, { name: 'lists-new', path: '/lists/new', component: defineAsyncComponent( - () => import( 'src/components/lists_edit/lists_edit.vue'), + () => import('src/components/lists_edit/lists_edit.vue'), ), }, { @@ -250,14 +246,17 @@ export default (store) => { name: 'bookmark-folders', path: '/bookmark_folders', component: defineAsyncComponent( - () => import( 'src/components/bookmark_folders/bookmark_folders.vue'), + () => import('src/components/bookmark_folders/bookmark_folders.vue'), ), }, { name: 'bookmark-folder-new', path: '/bookmarks/new-folder', component: defineAsyncComponent( - () => import( 'src/components/bookmark_folder_edit/bookmark_folder_edit.vue'), + () => + import( + 'src/components/bookmark_folder_edit/bookmark_folder_edit.vue' + ), ), }, { @@ -269,7 +268,10 @@ export default (store) => { name: 'bookmark-folder-edit', path: '/bookmarks/:id/edit', component: defineAsyncComponent( - () => import( 'src/components/bookmark_folder_edit/bookmark_folder_edit.vue'), + () => + import( + 'src/components/bookmark_folder_edit/bookmark_folder_edit.vue' + ), ), }, ] @@ -280,7 +282,7 @@ export default (store) => { name: 'chat', path: '/users/:username/chats/:recipient_id', component: defineAsyncComponent( - () => import( 'src/components/chat/chat.vue'), + () => import('src/components/chat/chat.vue'), ), meta: { dontScroll: false }, beforeEnter: validateAuthenticatedRoute, @@ -289,7 +291,7 @@ export default (store) => { name: 'chats', path: '/users/:username/chats', component: defineAsyncComponent( - () => import( 'src/components/chat_list/chat_list.vue'), + () => import('src/components/chat_list/chat_list.vue'), ), meta: { dontScroll: false }, beforeEnter: validateAuthenticatedRoute, diff --git a/src/components/emoji_input/emoji_input.vue b/src/components/emoji_input/emoji_input.vue index 8b69f3d9d..526f646ab 100644 --- a/src/components/emoji_input/emoji_input.vue +++ b/src/components/emoji_input/emoji_input.vue @@ -37,7 +37,7 @@ :title="$t('emoji.add_emoji')" @click.prevent="togglePicker" > - + import('src/components/status/status.vue'), + ), }, name: 'Quote', props: { diff --git a/src/components/status/status.js b/src/components/status/status.js index af177d298..005956ce7 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -10,14 +10,16 @@ import { highlightClass, highlightStyle, } from '../../services/user_highlighter/user_highlighter.js' -import AvatarList from 'src/components/avatar_list/avatar_list.vue' -import EmojiReactions from 'src/components/emoji_reactions/emoji_reactions.vue' -import StatusContent from 'src/components/status_content/status_content.vue' -import Timeago from 'src/components/timeago/timeago.vue' -import UserAvatar from 'src/components/user_avatar/user_avatar.vue' -import UserLink from 'src/components/user_link/user_link.vue' -import UserListPopover from 'src/components/user_list_popover/user_list_popover.vue' -import UserPopover from 'src/components/user_popover/user_popover.vue' +import AvatarList from '../avatar_list/avatar_list.vue' +import EmojiReactions from '../emoji_reactions/emoji_reactions.vue' +import Quote from '../quote/quote.vue' +import StatusContent from '../status_content/status_content.vue' +import StatusPopover from '../status_popover/status_popover.vue' +import Timeago from '../timeago/timeago.vue' +import UserAvatar from '../user_avatar/user_avatar.vue' +import UserLink from '../user_link/user_link.vue' +import UserListPopover from '../user_list_popover/user_list_popover.vue' +import UserPopover from '../user_popover/user_popover.vue' import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' @@ -121,9 +123,7 @@ const Status = { UserAvatar, AvatarList, Timeago, - StatusPopover: defineAsyncComponent( - () => import( 'src/components/status_popover/status_popover.vue') - ), + StatusPopover, UserListPopover, EmojiReactions, StatusContent, @@ -132,9 +132,7 @@ const Status = { MentionsLine, UserPopover, UserLink, - Quote: defineAsyncComponent( - () => import('src/components/quote/quote.vue') - ), + Quote, StatusActionButtons, }, props: [ diff --git a/src/components/status_action_buttons/action_button.js b/src/components/status_action_buttons/action_button.js index 2abf2f941..5f7942162 100644 --- a/src/components/status_action_buttons/action_button.js +++ b/src/components/status_action_buttons/action_button.js @@ -1,15 +1,15 @@ +import EmojiPicker from 'src/components/emoji_picker/emoji_picker.vue' import Popover from 'src/components/popover/popover.vue' +import StatusBookmarkFolderMenu from 'src/components/status_bookmark_folder_menu/status_bookmark_folder_menu.vue' import { useInstanceStore } from 'src/stores/instance.js' import { useInstanceCapabilitiesStore } from 'src/stores/instance_capabilities.js' import { useMergedConfigStore } from 'src/stores/merged_config.js' -import { defineAsyncComponent } from 'vue' import { library } from '@fortawesome/fontawesome-svg-core' import { faBookmark as faBookmarkRegular, faStar as faStarRegular, - faFaceSmileBeam, } from '@fortawesome/free-regular-svg-icons' import { faBookmark, @@ -24,6 +24,7 @@ import { faReply, faRetweet, faShareAlt, + faSmileBeam, faStar, faThumbtack, faTimes, @@ -44,7 +45,7 @@ library.add( faRetweet, faStar, faStarRegular, - faFaceSmileBeam, + faSmileBeam, faBookmark, faBookmarkRegular, @@ -68,12 +69,8 @@ export default { 'outerClose', ], components: { - StatusBookmarkFolderMenu: defineAsyncComponent( - () => import( 'src/components/status_bookmark_folder_menu/status_bookmark_folder_menu.vue'), - ), - EmojiPicker: defineAsyncComponent( - () => import( 'src/components/emoji_picker/emoji_picker.vue'), - ), + StatusBookmarkFolderMenu, + EmojiPicker, Popover, }, data: () => ({ diff --git a/src/components/status_action_buttons/action_button_container.vue b/src/components/status_action_buttons/action_button_container.vue index da0beed79..348b158a7 100644 --- a/src/components/status_action_buttons/action_button_container.vue +++ b/src/components/status_action_buttons/action_button_container.vue @@ -79,7 +79,7 @@ :button="button" :status="status" v-bind="$attrs" - @emoji-picker-shown="e => $emit('emojiPickerShown', e)" + @emojiPickerShown="e => $emit('emojiPickerShown', e)" /> true, active: ({ emojiPickerShown }) => emojiPickerShown, toggleable: true, diff --git a/src/components/status_action_buttons/status_action_buttons.vue b/src/components/status_action_buttons/status_action_buttons.vue index bdb95e83b..7017a8cf0 100644 --- a/src/components/status_action_buttons/status_action_buttons.vue +++ b/src/components/status_action_buttons/status_action_buttons.vue @@ -20,7 +20,7 @@ :get-component="getComponent" :close="() => { /* no-op */ }" :do-action="doAction" - @emoji-picker-shown="onEmojiPickerShown" + @emojiPickerShown="onEmojiPickerShown" />