Update preact to 10.29.8 (#2163)
* Update preact to 10.29.8 Also moves TypeScript to 5.9, which preact 10.29 typings require, and the compat and testing library pins that go with it. Type checking resolves JSX from preact via the automatic runtime; the bundle keeps the classic transform so babel still strips test ids. * Move babel to the automatic JSX runtime and refresh frontend notes Leaving babel on the classic h pragma while tsconfig used the automatic runtime meant a tsx file without an h import would type-check and lint clean, then throw at runtime, since eslint-config-preact disables react/react-in-jsx-scope and no-undef is off. * Address review findings on the preact upgrade Forward the textarea ref with useImperativeHandle so it clears on unmount and lands during commit rather than after paint. Pair typescript-eslint with the TypeScript it now has to parse. Use the preact namespace types rather than the deprecated JSX aliases, and drop the redundant type re-declarations the element-specific interfaces already provide. * Drive the focus tests through real DOM focus and blur Dispatching a synthetic focusin hard-coded preact/compat's internal alias for onFocus. Calling focus() and blur() exercises the sequence a browser produces and stays correct if that mapping changes. * Raise the two bundle limits the preact upgrade pushes past CI measures remark.mjs at 78024 bytes against a limit size-limit reads as 78000, so it failed by 24. last-comments.mjs had 36 bytes of headroom and would have tripped on the next change. * Regenerate the lockfile after the rebase The rebase resolution left it missing the @typescript-eslint entries, so every CI job failed at pnpm install --frozen-lockfile.
This commit is contained in:
+20
-3
@@ -19,8 +19,7 @@ When bumping pnpm/node, also re-check `frontend/apps/remark42/package.json`'s `e
|
||||
## pnpm 10's stricter `node-linker` layout needs explicit pins
|
||||
|
||||
A few deps needed pinning specifically because of pnpm 10's hoisting changes, not because of the deps themselves:
|
||||
- `preact` pinned via `pnpm.overrides` (10.6.2) — newer breaks the build under the stricter layout
|
||||
- `react-intl` 6.0.5 and `@testing-library/preact` 3.2.2 — newer versions' type declarations break the preact-compat alias setup
|
||||
- `react-intl` 6.0.5 — newer versions' type declarations break the preact-compat alias setup
|
||||
- `@types/minimatch` 5.1.2 — 6.x is an empty stub that the hoisted layout picks up instead of the real types
|
||||
- `cheerio` 1.0.0-rc.12 — 1.2 is ESM-only and breaks under jest 28
|
||||
|
||||
@@ -32,12 +31,30 @@ Unlike the polyfilled fetch in node 16 and 18, it rejects relative request URLs,
|
||||
silent: requests simply never match. Any test harness that mocks fetch needs absolute base URLs and
|
||||
a jsdom base URL set.
|
||||
|
||||
## JSX runs on the automatic runtime, in three places that must agree
|
||||
|
||||
`preact` 10.29 types a component's return as `ComponentChildren`, which only satisfies a JSX check on
|
||||
TypeScript 5.1+ via `JSX.ElementType`, and it scopes the `JSX` namespace to `preact/jsx-runtime` rather
|
||||
than declaring it globally. So the type layer has to use the automatic runtime:
|
||||
|
||||
- `tsconfig.json`: `jsx: react-jsx` with `jsxImportSource: preact`
|
||||
- `.babelrc.js`: `@babel/preset-react` with `runtime: 'automatic'`, `importSource: 'preact'`
|
||||
- `jest.config.ts`: `@swc/jest` with `transform.react.runtime: 'automatic'`, `importSource: 'preact'`
|
||||
|
||||
`ts-loader` in `webpack.config.js` overrides `jsx` back to `preserve`. That is deliberate: JSX has to
|
||||
survive as JSX until babel runs, or `babel-plugin-jsx-remove-data-test-id` has nothing to strip and
|
||||
`data-testid` attributes ship to production. Verify with `grep -c data-testid public/*.mjs` after a
|
||||
production build; it must be 0.
|
||||
|
||||
Keep all three in step. If babel alone were left on the classic `pragma: 'h'` transform, a new `.tsx`
|
||||
without `import { h }` would type-check and lint clean, then throw at runtime, because
|
||||
`eslint-config-preact` sets `react/react-in-jsx-scope` to 0 and the local config turns `no-undef` off.
|
||||
|
||||
## Held-back majors
|
||||
|
||||
These were deliberately not bumped because each is a config-migration or bundle-changing major, not a drop-in update — don't bump them opportunistically inside an unrelated dependency PR:
|
||||
|
||||
- `eslint` 8 (9/10 need flat-config migration), `stylelint` 14 (16 has breaking rule changes), `babel` 7, `jest` 28 (30 needs config changes)
|
||||
- `typescript` 4.7 in `apps/remark42` specifically (the api package is on a newer TS — they're intentionally decoupled)
|
||||
- `react`/`react-dom` (the app uses `preact` aliased as `react`/`react-dom` via `preact/compat` — don't "fix" this by installing real react)
|
||||
- `redux`/`react-redux`, `tailwindcss` 3.4 (v4 is a full config rewrite), `@11ty/eleventy` 2 (v3 is an ESM migration) in `site/`
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ const getPresetEnv = (options) => ['@babel/preset-env', options];
|
||||
const preactPreset = [
|
||||
'@babel/preset-react',
|
||||
{
|
||||
pragma: 'h',
|
||||
pragmaFrag: 'Fragment',
|
||||
runtime: 'automatic',
|
||||
importSource: 'preact',
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ module.exports = [
|
||||
},
|
||||
{
|
||||
path: 'public/remark.mjs',
|
||||
limit: '76 KB',
|
||||
limit: '80 KB',
|
||||
},
|
||||
{
|
||||
path: 'public/remark.css',
|
||||
@@ -13,7 +13,7 @@ module.exports = [
|
||||
},
|
||||
{
|
||||
path: 'public/last-comments.mjs',
|
||||
limit: '39 KB',
|
||||
limit: '40 KB',
|
||||
},
|
||||
{
|
||||
path: 'public/last-comments.css',
|
||||
@@ -21,7 +21,7 @@ module.exports = [
|
||||
},
|
||||
{
|
||||
path: 'public/deleteme.mjs',
|
||||
limit: '14 KB',
|
||||
limit: '15 KB',
|
||||
},
|
||||
{
|
||||
path: 'public/counter.mjs',
|
||||
|
||||
@@ -6,7 +6,7 @@ const handleBtnKeyPress = (event: KeyboardEvent, handler?: (e: KeyboardEvent | M
|
||||
};
|
||||
|
||||
export const getHandleClickProps = (handler?: (e: KeyboardEvent | MouseEvent) => void) => ({
|
||||
role: 'button',
|
||||
role: 'button' as const,
|
||||
onClick: handler,
|
||||
onKeyPress: (event: KeyboardEvent) => handleBtnKeyPress(event, handler),
|
||||
...(handler ? { tabIndex: 0 } : {}),
|
||||
|
||||
@@ -10,7 +10,7 @@ describe('copy to clipboard', () => {
|
||||
|
||||
it('should call `clipboard.write` for new browser', async () => {
|
||||
const clipboardWrite = jest.fn(() => Promise.resolve());
|
||||
window.ClipboardItem = jest.fn();
|
||||
Object.defineProperty(window, 'ClipboardItem', { value: jest.fn(), writable: true });
|
||||
|
||||
Object.defineProperty(navigator, 'clipboard', {
|
||||
value: {
|
||||
|
||||
@@ -265,7 +265,8 @@ describe('<Auth/>', () => {
|
||||
const input = screen.getByPlaceholderText('Username');
|
||||
|
||||
fireEvent.change(input, { target: { value } });
|
||||
fireEvent.blur(input);
|
||||
input.focus();
|
||||
input.blur();
|
||||
|
||||
expect(input).toHaveValue(expected);
|
||||
});
|
||||
@@ -285,7 +286,8 @@ describe('<Auth/>', () => {
|
||||
const input = screen.getByPlaceholderText('Username');
|
||||
|
||||
fireEvent.change(input, { target: { value } });
|
||||
fireEvent.blur(input);
|
||||
input.focus();
|
||||
input.blur();
|
||||
|
||||
expect(input).toHaveValue(expected);
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { h, JSX, VNode } from 'preact';
|
||||
import { h, VNode, type ButtonHTMLAttributes } from 'preact';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import styles from './button.module.css';
|
||||
|
||||
type Props = Omit<JSX.HTMLAttributes<HTMLButtonElement>, 'size'> & {
|
||||
type Props = Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'size'> & {
|
||||
size?: 'xs' | 'sm';
|
||||
kind?: 'transparent' | 'link' | 'hollow';
|
||||
suffix?: VNode;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import clsx from 'clsx';
|
||||
import { h, JSX } from 'preact';
|
||||
import { h, type ButtonHTMLAttributes } from 'preact';
|
||||
import { forwardRef } from 'preact/compat';
|
||||
|
||||
import type { Theme } from 'common/types';
|
||||
@@ -17,12 +17,11 @@ const sizeStyles: Record<string, string> = {
|
||||
large: styles.sizeLarge,
|
||||
};
|
||||
|
||||
export type ButtonProps = Omit<JSX.HTMLAttributes, 'size' | 'className'> & {
|
||||
export type ButtonProps = Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'size' | 'className'> & {
|
||||
kind?: 'primary' | 'secondary' | 'link';
|
||||
size?: 'middle' | 'large';
|
||||
theme?: Theme;
|
||||
mix?: string | string[];
|
||||
type?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { h, JSX } from 'preact';
|
||||
import { h, type InputHTMLAttributes } from 'preact';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import styles from './input.module.css';
|
||||
|
||||
type Props = JSX.HTMLAttributes<HTMLInputElement> & {
|
||||
type Props = InputHTMLAttributes<HTMLInputElement> & {
|
||||
invalid?: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/preact';
|
||||
import { screen, waitFor } from '@testing-library/preact';
|
||||
|
||||
import { render } from 'tests/utils';
|
||||
import { Select } from './select';
|
||||
@@ -31,7 +31,7 @@ describe('<Select/>', () => {
|
||||
it('should highlight select on focus', async () => {
|
||||
render(<Select items={items} selected={items[0]} />);
|
||||
|
||||
fireEvent.focus(screen.getByRole('combobox'));
|
||||
screen.getByRole<HTMLSelectElement>('combobox').focus();
|
||||
await waitFor(() => {
|
||||
const rootElement = screen.getByTestId('select-root');
|
||||
expect(rootElement).toHaveClass('select_focused');
|
||||
|
||||
@@ -37,7 +37,10 @@ describe('<SortPicker />', () => {
|
||||
|
||||
expect(select).toBeInTheDocument();
|
||||
|
||||
fireEvent.change(select, { target: { value: nextOption } });
|
||||
// @testing-library/preact rewrites change to input for every element once it sees a
|
||||
// compat vnode, so fireEvent.change never reaches a select handler; dispatch directly
|
||||
select.value = nextOption;
|
||||
fireEvent(select, new Event('change', { bubbles: true }));
|
||||
|
||||
await waitFor(() => expect(updateSorting).toHaveBeenCalledWith(nextOption));
|
||||
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import { h, JSX } from 'preact';
|
||||
import { h, JSX, type TextareaHTMLAttributes } from 'preact';
|
||||
import { forwardRef } from 'preact/compat';
|
||||
import { useEffect, useRef } from 'preact/hooks';
|
||||
import { useEffect, useImperativeHandle, useRef } from 'preact/hooks';
|
||||
|
||||
function autoResize(textarea: HTMLTextAreaElement) {
|
||||
textarea.style.height = '';
|
||||
textarea.style.height = `${textarea.scrollHeight}px`;
|
||||
}
|
||||
|
||||
type Props = Omit<JSX.HTMLAttributes<HTMLTextAreaElement>, 'onInput'> & {
|
||||
type Props = Omit<TextareaHTMLAttributes<HTMLTextAreaElement>, 'onInput'> & {
|
||||
onInput?(evt: JSX.TargetedEvent<HTMLTextAreaElement, Event>): void;
|
||||
};
|
||||
|
||||
export const TextareaAutosize = forwardRef<HTMLTextAreaElement, Props>(({ onInput, value, ...props }, externalRef) => {
|
||||
const localRef = useRef<HTMLTextAreaElement>(null);
|
||||
const ref = externalRef || localRef;
|
||||
const ref = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
useImperativeHandle(externalRef, () => ref.current as HTMLTextAreaElement, []);
|
||||
|
||||
const handleInput: JSX.GenericEventHandler<HTMLTextAreaElement> = (evt) => {
|
||||
if (!ref.current) {
|
||||
@@ -31,7 +32,7 @@ export const TextareaAutosize = forwardRef<HTMLTextAreaElement, Props>(({ onInpu
|
||||
if (ref.current) {
|
||||
autoResize(ref.current);
|
||||
}
|
||||
}, [value, ref]);
|
||||
}, [value]);
|
||||
|
||||
return <textarea {...props} data-testid={props.id} onInput={handleInput} value={value} ref={ref} dir="auto" />;
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { h, FunctionComponent } from 'preact';
|
||||
import { h, FunctionComponent, type AriaRole } from 'preact';
|
||||
import { shallowEqual } from 'react-redux';
|
||||
import { useCallback } from 'preact/hooks';
|
||||
import clsx from 'clsx';
|
||||
@@ -54,7 +54,7 @@ export const Thread: FunctionComponent<Props> = ({ id, level, getPreview }) => {
|
||||
level === 6 && styles.level6,
|
||||
theme === 'dark' && styles.themeDark
|
||||
)}
|
||||
role={['listitem'].concat(!collapsed && !!repliesCount ? 'list' : []).join(' ')}
|
||||
role={(!collapsed && !!repliesCount ? 'listitem list' : 'listitem') as AriaRole}
|
||||
aria-expanded={!collapsed}
|
||||
>
|
||||
<InView>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, StateUpdater } from 'preact/hooks';
|
||||
import { useState, StateUpdater, Dispatch } from 'preact/hooks';
|
||||
|
||||
function useSessionStorage<T>(key: string, initialValue?: T): [T, StateUpdater<T>] {
|
||||
function useSessionStorage<T>(key: string, initialValue?: T): [T, Dispatch<StateUpdater<T>>] {
|
||||
const [storedValue, setStoredValue] = useState<T>(() => {
|
||||
const item = sessionStorage.getItem(key);
|
||||
if (item === null) {
|
||||
|
||||
@@ -35,9 +35,9 @@
|
||||
"intersection-observer": "^0.12.2",
|
||||
"lodash-es": "^4.18.1",
|
||||
"node-emoji": "^1.11.0",
|
||||
"preact": "10.6.2",
|
||||
"react": "npm:@preact/compat@^17.1.1",
|
||||
"react-dom": "npm:@preact/compat@^17.1.1",
|
||||
"preact": "10.29.8",
|
||||
"react": "npm:@preact/compat@^18.3.2",
|
||||
"react-dom": "npm:@preact/compat@^18.3.2",
|
||||
"react-intl": "6.0.5",
|
||||
"react-redux": "^8.0.2",
|
||||
"redux": "^4.2.0",
|
||||
@@ -58,7 +58,7 @@
|
||||
"@swc/core": "1.2.205",
|
||||
"@swc/jest": "^0.2.21",
|
||||
"@testing-library/jest-dom": "^5.16.4",
|
||||
"@testing-library/preact": "3.2.2",
|
||||
"@testing-library/preact": "3.2.4",
|
||||
"@testing-library/preact-hooks": "^1.1.0",
|
||||
"@types/enzyme": "^3.10.19",
|
||||
"@types/eslint": "^8.4.5",
|
||||
@@ -70,8 +70,8 @@
|
||||
"@types/redux-mock-store": "^1.5.0",
|
||||
"@types/testing-library__jest-dom": "^5.14.5",
|
||||
"@types/webpack-env": "^1.18.8",
|
||||
"@typescript-eslint/eslint-plugin": "^5.30.3",
|
||||
"@typescript-eslint/parser": "^5.30.3",
|
||||
"@typescript-eslint/eslint-plugin": "^8.62.1",
|
||||
"@typescript-eslint/parser": "^8.62.1",
|
||||
"babel-loader": "^8.2.5",
|
||||
"babel-plugin-jsx-remove-data-test-id": "^3.0.0",
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
@@ -121,7 +121,7 @@
|
||||
"ts-loader": "^9.6.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths-webpack-plugin": "^3.5.2",
|
||||
"typescript": "^4.7.4",
|
||||
"typescript": "^5.9.3",
|
||||
"url-loader": "^4.1.1",
|
||||
"webpack": "^5.108.3",
|
||||
"webpack-bundle-analyzer": "^4.5.0",
|
||||
|
||||
@@ -4,9 +4,8 @@
|
||||
"target": "es6",
|
||||
"module": "esnext",
|
||||
"lib": ["es2019", "dom"],
|
||||
"jsx": "preserve",
|
||||
"jsxFactory": "h",
|
||||
"jsxFragmentFactory": "Fragment",
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "preact",
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"allowJs": true,
|
||||
|
||||
@@ -117,6 +117,9 @@ module.exports = (_, { mode, analyze }) => {
|
||||
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' },
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -87,7 +87,7 @@
|
||||
"svgo": ">=4.0.2 <5.0.0",
|
||||
"undici@>=7.0.0 <8.0.0": ">=7.29.0 <8.0.0",
|
||||
"body-parser@<1.20.6": ">=1.20.6 <2.0.0",
|
||||
"preact": "10.6.2",
|
||||
"preact": "10.29.8",
|
||||
"@types/minimatch": "5.1.2",
|
||||
"cheerio": "1.0.0-rc.12"
|
||||
}
|
||||
|
||||
Generated
+383
-136
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user