mirror of
https://github.com/mastodon/mastodon.git
synced 2025-06-11 23:59:12 +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
|
||||
.rspec-local
|
||||
|
||||
/.dist
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import './public-path';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
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 { start } from '../mastodon/common';
|
||||
import { loadLocale } from '../mastodon/locales';
|
||||
import { loadPolyfills } from '../mastodon/polyfills';
|
||||
import { loadPolyfills } from 'mastodon/polyfills';
|
||||
|
||||
start();
|
||||
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import './public-path';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import { afterInitialRender } from 'mastodon/hooks/useRenderSignal';
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import './public-path';
|
||||
import ready from '../mastodon/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';
|
||||
|
||||
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 './public-path';
|
||||
|
||||
import { IntlMessageFormat } from 'intl-messageformat';
|
||||
import type { MessageDescriptor, PrimitiveType } from 'react-intl';
|
||||
import { defineMessages } from 'react-intl';
|
||||
|
|
|
@ -8,8 +8,6 @@ and performs no other task.
|
|||
|
||||
*/
|
||||
|
||||
import './public-path';
|
||||
|
||||
import axios from 'axios';
|
||||
|
||||
interface JRDLink {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import './public-path';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import { start } from '../mastodon/common';
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import './public-path';
|
||||
import axios from 'axios';
|
||||
|
||||
import ready from '../mastodon/ready';
|
||||
|
|
|
@ -9,14 +9,13 @@
|
|||
// to ensure that the prevaled file is regenerated by Babel
|
||||
// version: 4
|
||||
|
||||
const { NimbleEmojiIndex } = require('emoji-mart');
|
||||
const { uncompress: emojiMartUncompress } = require('emoji-mart/dist/utils/data');
|
||||
import { NimbleEmojiIndex } from 'emoji-mart';
|
||||
import { uncompress as emojiMartUncompress } from 'emoji-mart/dist/utils/data';
|
||||
|
||||
|
||||
let data = require('./emoji_data.json');
|
||||
const emojiMap = require('./emoji_map.json');
|
||||
const { unicodeToFilename } = require('./unicode_to_filename');
|
||||
const { unicodeToUnifiedName } = require('./unicode_to_unified_name');
|
||||
import data from './emoji_data.json';
|
||||
import emojiMap from './emoji_map.json';
|
||||
import { unicodeToFilename } from './unicode_to_filename';
|
||||
import { unicodeToUnifiedName } from './unicode_to_unified_name';
|
||||
|
||||
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
|
||||
// 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.
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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<HTMLElement>('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 });
|
||||
});
|
||||
|
|
|
@ -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`
|
||||
);
|
||||
}
|
||||
|
||||
|
|
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": {
|
||||
"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",
|
||||
|
|
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