mirror of
https://github.com/mastodon/mastodon.git
synced 2025-06-13 00:29:17 +00:00
Migrate from Jest to Vitest
This commit is contained in:
parent
23238ddd95
commit
bc8f481eb8
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -74,3 +74,5 @@ docker-compose.override.yml
|
||||||
|
|
||||||
# Ignore local-only rspec configuration
|
# Ignore local-only rspec configuration
|
||||||
.rspec-local
|
.rspec-local
|
||||||
|
|
||||||
|
/.dist
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import './public-path';
|
|
||||||
import ready from '../mastodon/ready';
|
import ready from '../mastodon/ready';
|
||||||
|
|
||||||
ready(() => {
|
ready(() => {
|
||||||
|
|
8
app/javascript/entrypoints/index.html
Normal file
8
app/javascript/entrypoints/index.html
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<script type="module" src="./application.ts"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="mastodon"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -1,3 +1 @@
|
||||||
import '../styles/mailer.scss';
|
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,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';
|
||||||
|
|
|
@ -8,8 +8,6 @@ and performs no other task.
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import './public-path';
|
|
||||||
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
interface JRDLink {
|
interface JRDLink {
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 });
|
||||||
});
|
});
|
||||||
|
|
|
@ -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}`
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
90
config/vite/plugin-manifest-sri.ts
Normal file
90
config/vite/plugin-manifest-sri.ts
Normal 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}`;
|
||||||
|
}
|
|
@ -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
54
vite.config.ts
Normal 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'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user