From fa4243c28c59f332c347e48e0500a292096cc9f1 Mon Sep 17 00:00:00 2001 From: ChaosExAnima Date: Tue, 1 Apr 2025 11:57:11 +0200 Subject: [PATCH] basic changes to get a build working --- .gitignore | 2 + app/javascript/entrypoints/admin.tsx | 1 - app/javascript/entrypoints/application.ts | 8 +- app/javascript/entrypoints/embed.tsx | 1 - app/javascript/entrypoints/error.ts | 1 - app/javascript/entrypoints/index.html | 8 ++ app/javascript/entrypoints/mailer.ts | 2 - app/javascript/entrypoints/public-path.ts | 23 ----- app/javascript/entrypoints/public.tsx | 2 - .../entrypoints/remote_interaction_helper.ts | 2 - app/javascript/entrypoints/share.tsx | 1 - app/javascript/entrypoints/sign_up.ts | 1 - .../features/emoji/emoji_compressed.js | 19 ++-- .../features/emoji/unicode_to_filename.js | 2 +- .../features/emoji/unicode_to_unified_name.js | 2 +- .../mastodon/locales/load_locale.ts | 19 ++-- app/javascript/mastodon/polyfills/intl.ts | 6 +- config/vite/plugin-manifest-sri.ts | 90 +++++++++++++++++++ tsconfig.json | 4 +- vite.config.ts | 54 +++++++++++ yarn.lock | 32 +------ 21 files changed, 187 insertions(+), 93 deletions(-) create mode 100644 app/javascript/entrypoints/index.html delete mode 100644 app/javascript/entrypoints/public-path.ts create mode 100644 config/vite/plugin-manifest-sri.ts create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore index a74317bd7d8..61bcbeb67bb 100644 --- a/.gitignore +++ b/.gitignore @@ -74,3 +74,5 @@ docker-compose.override.yml # Ignore local-only rspec configuration .rspec-local + +/.dist diff --git a/app/javascript/entrypoints/admin.tsx b/app/javascript/entrypoints/admin.tsx index 225cb16330f..8f5c2d9f5e0 100644 --- a/app/javascript/entrypoints/admin.tsx +++ b/app/javascript/entrypoints/admin.tsx @@ -1,4 +1,3 @@ -import './public-path'; import { createRoot } from 'react-dom/client'; import Rails from '@rails/ujs'; diff --git a/app/javascript/entrypoints/application.ts b/app/javascript/entrypoints/application.ts index 1087b1c4cb5..a2b9fce06c0 100644 --- a/app/javascript/entrypoints/application.ts +++ b/app/javascript/entrypoints/application.ts @@ -1,9 +1,7 @@ -import './public-path'; +import { start } from 'mastodon/common'; +import { loadLocale } from 'mastodon/locales'; import main from 'mastodon/main'; - -import { start } from '../mastodon/common'; -import { loadLocale } from '../mastodon/locales'; -import { loadPolyfills } from '../mastodon/polyfills'; +import { loadPolyfills } from 'mastodon/polyfills'; start(); diff --git a/app/javascript/entrypoints/embed.tsx b/app/javascript/entrypoints/embed.tsx index 6c091e4d077..c1cd32e6a2f 100644 --- a/app/javascript/entrypoints/embed.tsx +++ b/app/javascript/entrypoints/embed.tsx @@ -1,4 +1,3 @@ -import './public-path'; import { createRoot } from 'react-dom/client'; import { afterInitialRender } from 'mastodon/hooks/useRenderSignal'; diff --git a/app/javascript/entrypoints/error.ts b/app/javascript/entrypoints/error.ts index db68484f3a8..af9d484de18 100644 --- a/app/javascript/entrypoints/error.ts +++ b/app/javascript/entrypoints/error.ts @@ -1,4 +1,3 @@ -import './public-path'; import ready from '../mastodon/ready'; ready(() => { diff --git a/app/javascript/entrypoints/index.html b/app/javascript/entrypoints/index.html new file mode 100644 index 00000000000..025030ba46d --- /dev/null +++ b/app/javascript/entrypoints/index.html @@ -0,0 +1,8 @@ + + + + + +
+ + diff --git a/app/javascript/entrypoints/mailer.ts b/app/javascript/entrypoints/mailer.ts index a2ad5e73ac2..22b3ef6ecdc 100644 --- a/app/javascript/entrypoints/mailer.ts +++ b/app/javascript/entrypoints/mailer.ts @@ -1,3 +1 @@ import '../styles/mailer.scss'; - -require.context('../icons'); diff --git a/app/javascript/entrypoints/public-path.ts b/app/javascript/entrypoints/public-path.ts deleted file mode 100644 index ac4b9355b95..00000000000 --- a/app/javascript/entrypoints/public-path.ts +++ /dev/null @@ -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('meta[name=cdn-host]'); - -__webpack_public_path__ = formatPublicPath( - cdnHost ? cdnHost.content : '', - process.env.PUBLIC_OUTPUT_PATH, -); diff --git a/app/javascript/entrypoints/public.tsx b/app/javascript/entrypoints/public.tsx index 9374d6b2d1e..86487503db2 100644 --- a/app/javascript/entrypoints/public.tsx +++ b/app/javascript/entrypoints/public.tsx @@ -1,7 +1,5 @@ import { createRoot } from 'react-dom/client'; -import './public-path'; - import { IntlMessageFormat } from 'intl-messageformat'; import type { MessageDescriptor, PrimitiveType } from 'react-intl'; import { defineMessages } from 'react-intl'; diff --git a/app/javascript/entrypoints/remote_interaction_helper.ts b/app/javascript/entrypoints/remote_interaction_helper.ts index 419571c8964..f50203747d8 100644 --- a/app/javascript/entrypoints/remote_interaction_helper.ts +++ b/app/javascript/entrypoints/remote_interaction_helper.ts @@ -8,8 +8,6 @@ and performs no other task. */ -import './public-path'; - import axios from 'axios'; interface JRDLink { diff --git a/app/javascript/entrypoints/share.tsx b/app/javascript/entrypoints/share.tsx index 79262508510..f6fda68a39b 100644 --- a/app/javascript/entrypoints/share.tsx +++ b/app/javascript/entrypoints/share.tsx @@ -1,4 +1,3 @@ -import './public-path'; import { createRoot } from 'react-dom/client'; import { start } from '../mastodon/common'; diff --git a/app/javascript/entrypoints/sign_up.ts b/app/javascript/entrypoints/sign_up.ts index 880738fcb77..21fdce190ee 100644 --- a/app/javascript/entrypoints/sign_up.ts +++ b/app/javascript/entrypoints/sign_up.ts @@ -1,4 +1,3 @@ -import './public-path'; import axios from 'axios'; import ready from '../mastodon/ready'; diff --git a/app/javascript/mastodon/features/emoji/emoji_compressed.js b/app/javascript/mastodon/features/emoji/emoji_compressed.js index 5cd3a1317de..b707f017f52 100644 --- a/app/javascript/mastodon/features/emoji/emoji_compressed.js +++ b/app/javascript/mastodon/features/emoji/emoji_compressed.js @@ -10,18 +10,17 @@ // version: 3 // This json file contains the names of the categories. -const emojiMart5LocalesData = require('@emoji-mart/data/i18n/en.json'); -const emojiMart5Data = require('@emoji-mart/data/sets/15/all.json'); -const { uncompress: emojiMartUncompress } = require('emoji-mart/dist/utils/data'); -const _ = require('lodash'); +import emojiMart5LocalesData from '@emoji-mart/data/i18n/en.json'; +import emojiMart5Data from '@emoji-mart/data/sets/15/all.json'; +import { uncompress as emojiMartUncompress } from 'emoji-mart/dist/utils/data'; +import _ from 'lodash'; - -const emojiMap = require('./emoji_map.json'); +import emojiMap from './emoji_map.json'; // This json file is downloaded from https://github.com/iamcal/emoji-data/ // and is used to correct the sheet coordinates since we're using that repo's sheet -const emojiSheetData = require('./emoji_sheet.json'); -const { unicodeToFilename } = require('./unicode_to_filename'); -const { unicodeToUnifiedName } = require('./unicode_to_unified_name'); +import emojiSheetData from './emoji_sheet.json'; +import { unicodeToFilename } from './unicode_to_filename'; +import { unicodeToUnifiedName } from './unicode_to_unified_name'; // Grabbed from `emoji_utils` to avoid circular dependency function unifiedToNative(unified) { @@ -181,7 +180,7 @@ Object.keys(emojiMartData.emojis).forEach(key => { // JSON.parse/stringify is to emulate what @preval is doing and avoid any // inconsistent behavior in dev mode -module.exports = JSON.parse(JSON.stringify([ +export default JSON.parse(JSON.stringify([ shortCodesToEmojiData, /* * The property `skins` is not found in the current context. diff --git a/app/javascript/mastodon/features/emoji/unicode_to_filename.js b/app/javascript/mastodon/features/emoji/unicode_to_filename.js index c75c4cd7d05..cfe5539c7b9 100644 --- a/app/javascript/mastodon/features/emoji/unicode_to_filename.js +++ b/app/javascript/mastodon/features/emoji/unicode_to_filename.js @@ -1,6 +1,6 @@ // taken from: // https://github.com/twitter/twemoji/blob/47732c7/twemoji-generator.js#L848-L866 -exports.unicodeToFilename = (str) => { +export const unicodeToFilename = (str) => { let result = ''; let charCode = 0; let p = 0; diff --git a/app/javascript/mastodon/features/emoji/unicode_to_unified_name.js b/app/javascript/mastodon/features/emoji/unicode_to_unified_name.js index d29550f1226..15f60aa7c30 100644 --- a/app/javascript/mastodon/features/emoji/unicode_to_unified_name.js +++ b/app/javascript/mastodon/features/emoji/unicode_to_unified_name.js @@ -6,7 +6,7 @@ function padLeft(str, num) { return str; } -exports.unicodeToUnifiedName = (str) => { +export const unicodeToUnifiedName = (str) => { let output = ''; for (let i = 0; i < str.length; i += 2) { diff --git a/app/javascript/mastodon/locales/load_locale.ts b/app/javascript/mastodon/locales/load_locale.ts index d21675b1797..8a6116f3245 100644 --- a/app/javascript/mastodon/locales/load_locale.ts +++ b/app/javascript/mastodon/locales/load_locale.ts @@ -5,6 +5,10 @@ import { isLocaleLoaded, setLocale } from './global_locale'; const localeLoadingSemaphore = new Semaphore(1); +const localeFiles = import.meta.glob<{ default: LocaleData['messages'] }>([ + './*.json', +]); + export async function loadLocale() { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- we want to match empty strings const locale = document.querySelector('html')?.lang || 'en'; @@ -17,13 +21,14 @@ export async function loadLocale() { // if the locale is already set, then do nothing if (isLocaleLoaded()) return; - const localeData = (await import( - /* webpackMode: "lazy" */ - /* webpackChunkName: "locale/[request]" */ - /* webpackInclude: /\.json$/ */ - /* webpackPreload: true */ - `mastodon/locales/${locale}.json` - )) as LocaleData['messages']; + // If there is no locale file, then fallback to english + const localeFile = Object.hasOwn(localeFiles, '`./${locale}.json`') + ? localeFiles[`./${locale}.json`] + : localeFiles[`./en.json`]; + + if (!localeFile) throw new Error('Could not load the locale JSON file'); + + const { default: localeData } = await localeFile(); setLocale({ messages: localeData, locale }); }); diff --git a/app/javascript/mastodon/polyfills/intl.ts b/app/javascript/mastodon/polyfills/intl.ts index b825da66214..b7a06e557af 100644 --- a/app/javascript/mastodon/polyfills/intl.ts +++ b/app/javascript/mastodon/polyfills/intl.ts @@ -54,11 +54,9 @@ async function loadIntlPluralRulesPolyfills(locale: string) { return; } // Load the polyfill 1st BEFORE loading data + await import('@formatjs/intl-pluralrules/polyfill-force'); await import( - /* webpackChunkName: "i18n-pluralrules-polyfill" */ '@formatjs/intl-pluralrules/polyfill-force' - ); - await import( - /* webpackChunkName: "i18n-pluralrules-polyfill-[request]" */ `@formatjs/intl-pluralrules/locale-data/${unsupportedLocale}` + `../../../../node_modules/@formatjs/intl-pluralrules/locale-data/${unsupportedLocale}.js` ); } diff --git a/config/vite/plugin-manifest-sri.ts b/config/vite/plugin-manifest-sri.ts new file mode 100644 index 00000000000..4c768abfa5e --- /dev/null +++ b/config/vite/plugin-manifest-sri.ts @@ -0,0 +1,90 @@ +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[]; + + /** + * Path of the manifest files that should be read and augmented with the + * integrity hash, relative to `outDir`. + * @default ['manifest.json', 'manifest-assets.json'] + */ + manifestPaths?: string[]; +} + +declare module 'vite' { + interface ManifestChunk { + integrity: string; + } +} + +export function manifestSRI(options: Options = {}): Plugin { + const { + algorithms = ['sha384'], + manifestPaths = [ + '.vite/manifest.json', + '.vite/manifest-assets.json', + 'manifest.json', + 'manifest-assets.json', + ], + } = options; + + return { + name: 'vite-plugin-manifest-sri', + apply: 'build', + enforce: 'post', + async writeBundle({ dir }) { + await Promise.all( + manifestPaths.map((path) => + augmentManifest(path, 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) { + await Promise.all( + Object.values(manifest).map(async (chunk) => { + chunk.integrity = integrityForAsset( + await fs.readFile(resolveInOutDir(chunk.file)), + 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}`; +} diff --git a/tsconfig.json b/tsconfig.json index 4eb94e43189..8e9955ce641 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "jsx": "react-jsx", "target": "esnext", - "module": "CommonJS", + "module": "ES2022", "moduleResolution": "node", "allowJs": true, "noEmit": true, @@ -11,7 +11,7 @@ "noUncheckedIndexedAccess": true, "esModuleInterop": true, "skipLibCheck": true, - "types": ["vitest/globals", "@types/webpack-env"], + "types": ["vite/client", "vitest/globals", "@types/webpack-env"], "baseUrl": "./", "incremental": true, "tsBuildInfoFile": "tmp/cache/tsconfig.tsbuildinfo", diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 00000000000..f7fe769e09d --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,54 @@ +import path from 'node:path'; + +import { defineConfig } from 'vite'; + +import { manifestSRI } from './config/vite/plugin-manifest-sri'; + +// eslint-disable-next-line import/no-default-export +export default defineConfig({ + root: './app/javascript/entrypoints', + build: { + commonjsOptions: { transformMixedEsModules: true }, + outDir: path.resolve(__dirname, '.dist'), + emptyOutDir: true, + manifest: 'manifest.json', + rollupOptions: { + output: { + chunkFileNames: (chunkInfo) => { + if ( + chunkInfo.facadeModuleId?.match( + /mastodon\/locales\/[a-zA-Z-]+\.json/, + ) + ) { + // put all locale files in `intl/` + return `intl/[name]-[hash].js`; + } else if ( + chunkInfo.facadeModuleId?.match(/node_modules\/@formatjs\//) + ) { + // use a custom name for formatjs polyfill files + const name = /node_modules\/@formatjs\/([^/]+)\//.exec( + chunkInfo.facadeModuleId, + ); + + if (name?.[1]) return `intl/[name]-${name[1]}-[hash].js`; + } else if (chunkInfo.name === 'index' && chunkInfo.facadeModuleId) { + // Use a custom name for chunks, to avoid having too many of them called "index" + const parts = chunkInfo.facadeModuleId.split('/'); + + const parent = parts.at(-2); + + if (parent) return `${parent}-[name]-[hash].js`; + } + return `[name]-[hash].js`; + }, + }, + }, + }, + plugins: [manifestSRI()], + resolve: { + alias: { + mastodon: path.resolve(__dirname, 'app/javascript/mastodon'), + '@': path.resolve(__dirname, 'app/javascript'), + }, + }, +}); diff --git a/yarn.lock b/yarn.lock index 0a9d4d8be44..2f1682711a9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2533,14 +2533,7 @@ __metadata: languageName: node linkType: hard -"@eslint-community/regexpp@npm:^4.10.0": - version: 4.10.0 - resolution: "@eslint-community/regexpp@npm:4.10.0" - checksum: 10c0/c5f60ef1f1ea7649fa7af0e80a5a79f64b55a8a8fa5086de4727eb4c86c652aedee407a9c143b8995d2c0b2d75c1222bec9ba5d73dbfc1f314550554f0979ef4 - languageName: node - linkType: hard - -"@eslint-community/regexpp@npm:^4.12.1": +"@eslint-community/regexpp@npm:^4.10.0, @eslint-community/regexpp@npm:^4.12.1": version: 4.12.1 resolution: "@eslint-community/regexpp@npm:4.12.1" checksum: 10c0/a03d98c246bcb9109aec2c08e4d10c8d010256538dcb3f56610191607214523d4fb1b00aa81df830b6dffb74c5fa0be03642513a289c567949d3e550ca11cdf6 @@ -4118,16 +4111,7 @@ __metadata: languageName: node linkType: hard -"@types/babel__traverse@npm:*": - version: 7.20.3 - resolution: "@types/babel__traverse@npm:7.20.3" - dependencies: - "@babel/types": "npm:^7.20.7" - checksum: 10c0/295ed9b837e62e17ee43be0df45d90fff5208986bd43af593c9020d152d3b2c55328e038c2f8585926b63cc22f887f28bf3f4c805aa881e2dd0bdd5ead92ece0 - languageName: node - linkType: hard - -"@types/babel__traverse@npm:^7.20.6": +"@types/babel__traverse@npm:*, @types/babel__traverse@npm:^7.20.6": version: 7.20.6 resolution: "@types/babel__traverse@npm:7.20.6" dependencies: @@ -12068,17 +12052,7 @@ __metadata: languageName: node linkType: hard -"micromatch@npm:^4.0.4": - version: 4.0.7 - resolution: "micromatch@npm:4.0.7" - dependencies: - braces: "npm:^3.0.3" - picomatch: "npm:^2.3.1" - checksum: 10c0/58fa99bc5265edec206e9163a1d2cec5fabc46a5b473c45f4a700adce88c2520456ae35f2b301e4410fb3afb27e9521fb2813f6fc96be0a48a89430e0916a772 - languageName: node - linkType: hard - -"micromatch@npm:^4.0.5, micromatch@npm:^4.0.8, micromatch@npm:~4.0.8": +"micromatch@npm:^4.0.4, micromatch@npm:^4.0.5, micromatch@npm:^4.0.8, micromatch@npm:~4.0.8": version: 4.0.8 resolution: "micromatch@npm:4.0.8" dependencies: