Second and final step of #2166. `react-intl` is replaced by `app/common/intl.tsx`, a small i18n binding over Preact context, and with `react-redux` already gone nothing holds the React compatibility alias. React is now absent from the lockfile, the installed tree, the config and the bundles: `react`, `react-dom`, `react-intl`, `@types/react`, `@preact/compat`, `use-sync-external-store` and `intl-messageformat` are all gone, along with the `paths` entries in `tsconfig.json` and three babel-loader excludes. Runtime dependencies go from 15 to 10. `preact/compat` goes too, which matters more than its 3.8 kB. Importing it anywhere installs hooks on preact's shared `options` that remap `onFocus`/`onBlur` to `focusin`/`focusout` for every element and make `@testing-library/preact` rewrite `change` to `input`, the two bugs behind #2166, still live until now. `Button` was wrapped in `forwardRef` with no caller passing one, and `TextareaAutosize` now takes its ref as an ordinary prop. The workaround in `sort-picker.spec.tsx` is gone with them, since `fireEvent.change` reaches a `<select>` again. Gzipped, against master: `remark.mjs` 76.17 kB to 56.47, `last-comments.mjs` 37.97 to 18.26, `deleteme.mjs` 14.51 to 8.42. The limits move with them and keep more relative headroom than master shipped. ### The binding `IntlProvider`, `useIntl`, `createIntl`, `defineMessages`, `FormattedMessage` and `IntlShape`. 32 files change only their import. The export names copy react-intl's deliberately: `formatjs extract` finds messages by recognising `defineMessages`, `FormattedMessage` and `intl.formatMessage` in the AST rather than by import source, so renaming one silently empties the catalogue. `frontend/CLAUDE.md` records that, along with the destructive part: `translation:generate` would then strip the unextracted keys from all 24 catalogues and the check would pass. A message the binding cannot parse falls back to the message in the source: a broken, unhandled or nested tag, a brace that is not a well-formed placeholder, and a placeholder naming a value the caller did not supply. `mk.json` and `th.json` carried broken markup and rendered in English; both are repaired, so a catalogue sweep over every locale can now require well-formed markup with no exceptions listed. `translation:check` gained the validation that would have caught them when they were proposed: a translation's tags have to be well-formed pairs of the names the English string uses, with no attributes, and its placeholders have to be ones the English string provides. Leaving a tag or a placeholder out stays allowed. Run against master's catalogues it reports both. ### enzyme `@types/enzyme` was the last thing pulling `@types/react`, so React could not leave while enzyme stayed. Its three test files move to `@testing-library/preact`, which now has no rival: `@testing-library/preact-hooks` had one import left and its own unmet peer warning. `intersection-observer` was a runtime dependency nothing imported, and the `cheerio` override lost its last dependent with enzyme. Enzyme's `.find(X).prop()` threw unless exactly one node matched, so the converted tests assert node counts explicitly to keep that. ### Verified All 181 message ids formatted across all 24 catalogues through both real react-intl and this binding: 4344 comparisons, no differences. From a wiped `node_modules`: `pnpm install --frozen-lockfile`, `pnpm lint`, `pnpm type-check`, `pnpm test` (392 tests, 42 suites), `pnpm build`, `pnpm size-check`, `pnpm translation-check`.
377 lines
10 KiB
JavaScript
377 lines
10 KiB
JavaScript
require('dotenv').config();
|
|
|
|
const path = require('path');
|
|
const webpack = require('webpack');
|
|
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
|
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
|
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
|
const CopyPlugin = require('copy-webpack-plugin');
|
|
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
|
|
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
|
|
const RefreshPlugin = require('@prefresh/webpack');
|
|
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
|
|
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
|
|
const incstr = require('incstr');
|
|
const babelConfig = require('./.babelrc.js');
|
|
|
|
const NODE_ID = 'remark42';
|
|
const PUBLIC_PATH = '/web/';
|
|
const PORT = process.env.PORT || 9000;
|
|
const REMARK_API_BASE_URL = process.env.REMARK_API_BASE_URL || 'http://127.0.0.1:8080';
|
|
const DEVSERVER_BASE_PATH = process.env.DEVSERVER_BASE_PATH || `http://127.0.0.1:${PORT}`;
|
|
const PUBLIC_FOLDER_PATH = path.resolve(__dirname, 'public');
|
|
const CUSTOM_PROPERTIES_PATH = path.resolve(__dirname, './app/styles/custom-properties.css');
|
|
|
|
const genId = incstr.idGenerator();
|
|
const modulesMap = {};
|
|
|
|
function getLocalIdent(loaderContext, _, localName, options) {
|
|
if (!options.context) {
|
|
options.context = loaderContext.rootContext;
|
|
}
|
|
|
|
const filepath = path.relative(options.context, loaderContext.resourcePath).replace(/\\/g, '/');
|
|
|
|
if (!modulesMap[filepath]) {
|
|
modulesMap[filepath] = { id: genId(), genId: incstr.idGenerator(), classNames: {} };
|
|
}
|
|
|
|
const m = modulesMap[filepath];
|
|
|
|
if (!m.classNames[localName]) {
|
|
m.classNames[localName] = m.genId();
|
|
}
|
|
|
|
return `${m.id}_${m.classNames[localName]}`;
|
|
}
|
|
|
|
/**
|
|
* Generates excludes for babel-loader
|
|
*
|
|
* Exclude is a module that has >=es6 code and resides in node_modules.
|
|
* By default, babel-loader ignores everything from node_modules,
|
|
* so we have to exclude from ignore these modules
|
|
*/
|
|
const exclude = [
|
|
'@github/markdown-toolbar-element',
|
|
'@github/text-expander-element',
|
|
].map((m) => path.resolve(__dirname, 'node_modules', m));
|
|
|
|
const htmlMinifyOptions = {
|
|
minifyCSS: true,
|
|
minifyJS: true,
|
|
removeComments: true,
|
|
removeRedundantAttributes: true,
|
|
removeScriptTypeAttributes: true,
|
|
removeStyleLinkTypeAttributes: true,
|
|
sortAttributes: true,
|
|
sortClassName: true,
|
|
useShortDoctype: true,
|
|
};
|
|
|
|
module.exports = (_, { mode, analyze }) => {
|
|
const isDev = mode === 'development';
|
|
// Use REMARK_URL or predefined host in dev environment
|
|
// In development: We use `http://127.0.0.1:9000` for access to backend and backend is accessible via dev server proxy
|
|
// In production: {% REMARK_URL %} will be replaced by `sed` on start of prod
|
|
const REMARK_URL = isDev ? DEVSERVER_BASE_PATH : '{% REMARK_URL %}';
|
|
|
|
// Add debug lib only for development throw webpack chunks and keep code clear
|
|
const preactDebug = isDev ? ['preact/debug'] : [];
|
|
|
|
const entry = {
|
|
embed: './app/embed.ts',
|
|
counter: './app/counter.ts',
|
|
deleteme: './app/deleteme.ts',
|
|
'last-comments': [...preactDebug, CUSTOM_PROPERTIES_PATH, './app/last-comments.tsx'],
|
|
remark: [...preactDebug, CUSTOM_PROPERTIES_PATH, './app/remark.tsx'],
|
|
};
|
|
|
|
const resolve = {
|
|
extensions: ['.ts', '.tsx', '.js'],
|
|
plugins: [new TsconfigPathsPlugin()],
|
|
};
|
|
|
|
const output = {
|
|
path: PUBLIC_FOLDER_PATH,
|
|
publicPath: PUBLIC_PATH,
|
|
};
|
|
|
|
const getTsRule = (babelConfig = {}) => {
|
|
return {
|
|
test: /\.tsx?$/,
|
|
exclude: /node_modules/,
|
|
use: [
|
|
{
|
|
loader: 'babel-loader',
|
|
options: {
|
|
exclude,
|
|
cacheDirectory: true,
|
|
...babelConfig,
|
|
},
|
|
},
|
|
{
|
|
loader: 'ts-loader',
|
|
options: {
|
|
transpileOnly: true,
|
|
// tsconfig targets the automatic runtime so type checking resolves JSX from
|
|
// preact; the bundle keeps JSX intact here so babel can still strip test ids
|
|
compilerOptions: { jsx: 'preserve' },
|
|
},
|
|
},
|
|
],
|
|
};
|
|
};
|
|
|
|
const cssRule = {
|
|
test: /\.css$/,
|
|
exclude: [/\.module\.css$/, /node_modules/],
|
|
use: [
|
|
isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
|
|
'css-loader',
|
|
{
|
|
loader: 'postcss-loader',
|
|
options: {
|
|
sourceMap: isDev,
|
|
postcssOptions: {
|
|
plugins: [
|
|
[
|
|
'postcss-preset-env',
|
|
{
|
|
browsers: 'defaults, not IE 11, not samsung 12',
|
|
stage: 0,
|
|
features: {
|
|
'custom-properties': CUSTOM_PROPERTIES_PATH,
|
|
},
|
|
},
|
|
],
|
|
'cssnano',
|
|
],
|
|
},
|
|
},
|
|
},
|
|
],
|
|
};
|
|
|
|
const cssModulesRule = {
|
|
test: /\.module\.css$/,
|
|
exclude: /node_modules/,
|
|
use: [
|
|
isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
|
|
{
|
|
loader: 'css-loader',
|
|
options: {
|
|
importLoaders: 1,
|
|
modules: {
|
|
localIdentName: '[name]__[local]_[hash:5]',
|
|
getLocalIdent: isDev ? undefined : getLocalIdent,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
loader: 'postcss-loader',
|
|
options: {
|
|
sourceMap: isDev,
|
|
postcssOptions: {
|
|
plugins: [
|
|
['postcss-preset-env', { stage: 0 }],
|
|
['postcss-custom-properties', { importFrom: CUSTOM_PROPERTIES_PATH }],
|
|
'cssnano',
|
|
],
|
|
},
|
|
},
|
|
},
|
|
],
|
|
};
|
|
|
|
const urlRule = {
|
|
test: /\.(png|jpg|jpeg|gif|svg)$/,
|
|
exclude: /node_modules/,
|
|
use: {
|
|
loader: 'url-loader',
|
|
options: {
|
|
name: '[name].[ext]',
|
|
publicPath: PUBLIC_PATH,
|
|
limit: false,
|
|
},
|
|
},
|
|
};
|
|
|
|
const rules = [cssRule, cssModulesRule, urlRule];
|
|
|
|
const devServer = {
|
|
port: PORT,
|
|
devMiddleware: {
|
|
stats: 'minimal',
|
|
},
|
|
headers: {
|
|
'Access-Control-Allow-Origin': '*',
|
|
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS',
|
|
'Access-Control-Allow-Headers': 'X-Requested-With, content-type, Authorization',
|
|
},
|
|
static: {
|
|
staticOptions: {
|
|
contentBase: PUBLIC_FOLDER_PATH,
|
|
watchOptions: {
|
|
ignored: [PUBLIC_FOLDER_PATH, path.resolve(__dirname, 'node_modules')],
|
|
},
|
|
},
|
|
watch: true,
|
|
},
|
|
allowedHosts: 'all',
|
|
hot: true,
|
|
proxy: [
|
|
{ path: '/api', target: REMARK_API_BASE_URL, changeOrigin: true },
|
|
{ path: '/auth', target: REMARK_API_BASE_URL, changeOrigin: true },
|
|
],
|
|
client: {
|
|
overlay: false,
|
|
},
|
|
};
|
|
|
|
const plugins = [
|
|
...(isDev ? [new CleanWebpackPlugin(), new RefreshPlugin()] : []),
|
|
new webpack.DefinePlugin({
|
|
'process.env.NODE_ENV': JSON.stringify(mode),
|
|
'process.env.REMARK_NODE': JSON.stringify(NODE_ID),
|
|
'process.env.REMARK_URL': isDev ? 'window.location.origin' : JSON.stringify(REMARK_URL),
|
|
}),
|
|
new MiniCssExtractPlugin({
|
|
filename: '[name].css',
|
|
}),
|
|
];
|
|
|
|
const optimization = {
|
|
// doc: https://webpack.js.org/plugins/css-minimizer-webpack-plugin/
|
|
minimizer: [`...`, new CssMinimizerPlugin()],
|
|
};
|
|
|
|
const config = {
|
|
entry,
|
|
devtool: isDev ? 'source-map' : false,
|
|
resolve,
|
|
optimization,
|
|
};
|
|
|
|
const legacyConfig = {
|
|
...config,
|
|
output: {
|
|
...output,
|
|
filename: '[name].js',
|
|
chunkFilename: '[name].js',
|
|
},
|
|
module: {
|
|
rules: [getTsRule(), ...rules],
|
|
},
|
|
plugins: [
|
|
...plugins,
|
|
...(analyze
|
|
? [
|
|
new BundleAnalyzerPlugin({
|
|
analyzerMode: 'static',
|
|
reportFilename: 'report-legacy.html',
|
|
reportTitle: 'Legacy build',
|
|
}),
|
|
]
|
|
: []),
|
|
],
|
|
};
|
|
|
|
const modernConfig = {
|
|
...config,
|
|
output: {
|
|
...output,
|
|
filename: '[name].mjs',
|
|
chunkFilename: '[name].mjs',
|
|
},
|
|
module: {
|
|
rules: [
|
|
getTsRule({
|
|
...babelConfig.env.modern,
|
|
plugins: [...babelConfig.env.modern.plugins, ...(isDev ? ['@prefresh/babel-plugin'] : [])],
|
|
}),
|
|
...rules,
|
|
],
|
|
},
|
|
plugins: [
|
|
...plugins,
|
|
new CopyPlugin({
|
|
patterns: [
|
|
{
|
|
from: path.resolve(__dirname, 'templates/400x400.jpeg'),
|
|
to: PUBLIC_FOLDER_PATH,
|
|
},
|
|
],
|
|
}),
|
|
new ForkTsCheckerWebpackPlugin(),
|
|
new HtmlWebpackPlugin({
|
|
template: path.resolve(__dirname, 'templates/iframe.ejs'),
|
|
filename: 'iframe.html',
|
|
inject: false,
|
|
env: mode,
|
|
minify: htmlMinifyOptions,
|
|
}),
|
|
new HtmlWebpackPlugin({
|
|
template: path.resolve(__dirname, 'templates/demo.ejs'),
|
|
filename: 'index.html',
|
|
inject: false,
|
|
REMARK_URL,
|
|
minify: htmlMinifyOptions,
|
|
}),
|
|
new HtmlWebpackPlugin({
|
|
template: path.resolve(__dirname, 'templates/counter.ejs'),
|
|
filename: 'counter.html',
|
|
inject: false,
|
|
REMARK_URL,
|
|
minify: htmlMinifyOptions,
|
|
}),
|
|
new HtmlWebpackPlugin({
|
|
template: path.resolve(__dirname, 'templates/last-comments.ejs'),
|
|
filename: 'last-comments.html',
|
|
inject: false,
|
|
env: mode,
|
|
REMARK_URL,
|
|
minify: htmlMinifyOptions,
|
|
}),
|
|
new HtmlWebpackPlugin({
|
|
template: path.resolve(__dirname, 'templates/deleteme.ejs'),
|
|
filename: 'deleteme.html',
|
|
inject: false,
|
|
REMARK_URL,
|
|
minify: htmlMinifyOptions,
|
|
}),
|
|
new HtmlWebpackPlugin({
|
|
template: path.resolve(__dirname, 'templates/markdown-help.html'),
|
|
filename: 'markdown-help.html',
|
|
inject: false,
|
|
minify: htmlMinifyOptions,
|
|
}),
|
|
new HtmlWebpackPlugin({
|
|
template: path.resolve(__dirname, 'templates/privacy.html'),
|
|
filename: 'privacy.html',
|
|
inject: false,
|
|
minify: htmlMinifyOptions,
|
|
}),
|
|
...(analyze
|
|
? [
|
|
new BundleAnalyzerPlugin({
|
|
analyzerMode: 'static',
|
|
reportFilename: 'report-modern.html',
|
|
reportTitle: 'Modern build',
|
|
}),
|
|
]
|
|
: []),
|
|
],
|
|
devServer,
|
|
};
|
|
|
|
if (isDev) {
|
|
return modernConfig;
|
|
}
|
|
|
|
return [legacyConfig, modernConfig];
|
|
};
|
|
|
|
module.exports.CUSTOM_PROPERTIES_PATH = CUSTOM_PROPERTIES_PATH;
|
|
module.exports.exclude = exclude;
|