Migrate from Jest to Vitest

This commit is contained in:
ChaosExAnima 2025-04-14 16:34:32 +02:00
parent 23238ddd95
commit bc8f481eb8
No known key found for this signature in database
GPG Key ID: 8F2B333100FB6117
21 changed files with 1195 additions and 96 deletions

2
.gitignore vendored
View File

@ -74,3 +74,5 @@ docker-compose.override.yml
# Ignore local-only rspec configuration # Ignore local-only rspec configuration
.rspec-local .rspec-local
/.dist

View File

@ -1,4 +1,3 @@
import './public-path';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import Rails from '@rails/ujs'; import Rails from '@rails/ujs';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,5 @@
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import './public-path';
import { IntlMessageFormat } from 'intl-messageformat'; import { IntlMessageFormat } from 'intl-messageformat';
import type { MessageDescriptor, PrimitiveType } from 'react-intl'; import type { MessageDescriptor, PrimitiveType } from 'react-intl';
import { defineMessages } from 'react-intl'; import { defineMessages } from 'react-intl';

View File

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

View File

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

View File

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

View File

@ -9,14 +9,13 @@
// to ensure that the prevaled file is regenerated by Babel // to ensure that the prevaled file is regenerated by Babel
// version: 4 // version: 4
const { NimbleEmojiIndex } = require('emoji-mart'); import { NimbleEmojiIndex } from 'emoji-mart';
const { uncompress: emojiMartUncompress } = require('emoji-mart/dist/utils/data'); import { uncompress as emojiMartUncompress } from 'emoji-mart/dist/utils/data';
import data from './emoji_data.json';
let data = require('./emoji_data.json'); import emojiMap from './emoji_map.json';
const emojiMap = require('./emoji_map.json'); import { unicodeToFilename } from './unicode_to_filename';
const { unicodeToFilename } = require('./unicode_to_filename'); import { unicodeToUnifiedName } from './unicode_to_unified_name';
const { unicodeToUnifiedName } = require('./unicode_to_unified_name');
emojiMartUncompress(data); emojiMartUncompress(data);
@ -117,7 +116,7 @@ Object.keys(emojiIndex.emojis).forEach(key => {
// JSON.parse/stringify is to emulate what @preval is doing and avoid any // JSON.parse/stringify is to emulate what @preval is doing and avoid any
// inconsistent behavior in dev mode // inconsistent behavior in dev mode
module.exports = JSON.parse(JSON.stringify([ export default JSON.parse(JSON.stringify([
shortCodesToEmojiData, shortCodesToEmojiData,
/* /*
* The property `skins` is not found in the current context. * The property `skins` is not found in the current context.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@
"compilerOptions": { "compilerOptions": {
"jsx": "react-jsx", "jsx": "react-jsx",
"target": "esnext", "target": "esnext",
"module": "CommonJS", "module": "ES2022",
"moduleResolution": "node", "moduleResolution": "node",
"allowJs": true, "allowJs": true,
"noEmit": true, "noEmit": true,
@ -11,7 +11,7 @@
"noUncheckedIndexedAccess": true, "noUncheckedIndexedAccess": true,
"esModuleInterop": true, "esModuleInterop": true,
"skipLibCheck": true, "skipLibCheck": true,
"types": ["vitest/globals", "@types/webpack-env"], "types": ["vite/client", "vitest/globals", "@types/webpack-env"],
"baseUrl": "./", "baseUrl": "./",
"incremental": true, "incremental": true,
"tsBuildInfoFile": "tmp/cache/tsconfig.tsbuildinfo", "tsBuildInfoFile": "tmp/cache/tsconfig.tsbuildinfo",

54
vite.config.ts Normal file
View File

@ -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'),
},
},
});

1047
yarn.lock

File diff suppressed because it is too large Load Diff