From 19fca0cfd9a78b95bb11c34f5af0d6b81b1ee288 Mon Sep 17 00:00:00 2001 From: Phil Date: Wed, 12 Aug 2026 11:41:17 +0200 Subject: [PATCH 01/48] feat: remove solid-js adapter package Drop @tiny-intl/solid-js from the monorepo. Its primary API never worked: memoizeCallback in useIntl.tsx called fn.call(args), passing the argument array as `this` and forwarding no arguments, so every primitive returned by useIntl() (t, tc, n, dt, rt, sort, collator, list) returned '[undefined]' or its numeric equivalent instead of a translation. That has been the published behaviour since the adapter shipped. BREAKING CHANGE: @tiny-intl/solid-js is removed from this repository and will no longer be published. Consumers of @tiny-intl/solid-js should stop depending on it; published versions up to 1.2.0 remain installable from npm but are no longer maintained. No other package in this repo depended on it. --- packages/solid-js/.gitignore | 25 --- packages/solid-js/README.md | 255 --------------------------- packages/solid-js/package.json | 44 ----- packages/solid-js/src/Translate.tsx | 114 ------------ packages/solid-js/src/index.ts | 2 - packages/solid-js/src/useIntl.tsx | 86 --------- packages/solid-js/src/vite-env.d.ts | 1 - packages/solid-js/tsconfig.json | 22 --- packages/solid-js/tsconfig.node.json | 9 - packages/solid-js/vite.config.ts | 40 ----- 10 files changed, 598 deletions(-) delete mode 100644 packages/solid-js/.gitignore delete mode 100644 packages/solid-js/README.md delete mode 100644 packages/solid-js/package.json delete mode 100644 packages/solid-js/src/Translate.tsx delete mode 100644 packages/solid-js/src/index.ts delete mode 100644 packages/solid-js/src/useIntl.tsx delete mode 100644 packages/solid-js/src/vite-env.d.ts delete mode 100644 packages/solid-js/tsconfig.json delete mode 100644 packages/solid-js/tsconfig.node.json delete mode 100644 packages/solid-js/vite.config.ts diff --git a/packages/solid-js/.gitignore b/packages/solid-js/.gitignore deleted file mode 100644 index 0eae589..0000000 --- a/packages/solid-js/.gitignore +++ /dev/null @@ -1,25 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -lib -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/packages/solid-js/README.md b/packages/solid-js/README.md deleted file mode 100644 index 39f55fc..0000000 --- a/packages/solid-js/README.md +++ /dev/null @@ -1,255 +0,0 @@ -# @tiny-intl/solid-js - -[@tiny-intl/solid](./packages/solid) ![Solid Adater Size](https://deno.bundlejs.com/badge?q=@tiny-intl/solid-js&treeshake=%5B*%5D&config=%7B%22esbuild%22:%7B%22external%22:%5B%22solid-js%22%5D%7D%7D) - -A tiny library to translate or transform strings, dates and numbers based on native Intl. - -## Installation - -```bash -npm install @tiny-intl/core @tiny-intl/solid-js -``` - -## Usage - -```json -// en-US.json -{ - "messages": { - "hello": "Hello {{name}}!", - "document": { - "one": "Document", - "other": "Documents" - // supports also zero, two, few, many - } - } -} -``` - -```jsx -import { createTinyIntl, detectBrowserLocale } from '@tiny-intl/core'; -import { TinyIntlContext } from '@tiny-intl/solid-js'; - -const intl = createTinyIntl({ - fallbackLocale: 'en-US', - supportedLocales: ['en-US', 'de-DE'], - loadDict: async (nextLoc) => { - const dict = (await import(`./locales/${nextLoc}.json`)).default; - return dict.messages; - }, - detectLocale: ({ supportedLocales, fallbackLocale }) => { - // or any custom logic - return detectBrowserLocale({ supportedLocales, fallbackLocale }); - }, -}); - -const App = () => { - const [i18nMounted, setI18nMounted] = createSignal(false); - - intl.mount().then(() => { - setI18nMounted(true); - }); - - return ( - - - - - - ); -}; -``` - -### Translating strings - -This package uses native plural rules from Intl [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules). - -```jsx -import { useIntl, Translate } from '@tiny-intl/solid-js'; - -// via primitive -const MyComponent = () => { - const { t, tc } = useIntl(); - - return ( -
- {/* Hello John! */} - {t('hello', { name: 'John' })()} - {/* Documents */} - {tc('document', 2)()} -
- ); -}; - -// via component -const MyComponent = () => { - return ( -
- {/* Hello John! */} - - {/* Documents */} - -
- ); -}; -``` - -### Formatting dates - -Look at [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat) for more options. - -```jsx -import { useIntl } from '@tiny-intl/solid-js'; - -// via primitive -const MyComponent = () => { - const { d } = useIntl(); - - return ( -
- {/* Tuesday, April 6, 2021 */} - {d(new Date(), { dateStyle: 'full' })()} -
- ); -}; - -// via component -const MyComponent = () => { - return ( -
- {/* Tuesday, April 6, 2021 */} - -
- ); -}; -``` - -### Relative Time Formatting - -Look at [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat) for more options. - -```jsx -import { useIntl } from '@tiny-intl/solid-js'; - -// via primitive -const MyComponent = () => { - const { rt } = useIntl(); - const date = new Date('2023-10-18T21:44:00.000z'); - - return ( -
- {/* 1 day ago */} - {rt(date)} -
- ); -}; - -// via component -const MyComponent = () => { - return ( -
- {/* 1 day ago */} - -
- ); -}; -``` - -### Formatting numbers - -Look at [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat) for more options. - -```jsx -import { useIntl } from '@tiny-intl/solid-js'; - -// via primitive -const MyComponent = () => { - const { n } = useIntl(); - - return ( -
- {/* €123,456.79 */} - {n(123456.789, { style: 'currency', currency: 'EUR' })()} -
- ); -}; - -// via component -const MyComponent = () => { - return ( -
- {/* €123,456.79 */} - -
- ); -}; -``` - -### List Formatting - -Look at [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat) for more options. - -> [!NOTE] -> Only available via primitive. - -```jsx -import { useIntl } from '@tiny-intl/solid-js'; - -// via primitive -const MyComponent = () => { - const { list } = useIntl(); - - return ( -
- {/* a, b, and c */} - {list(['a', 'b', 'c'])} - {/* a, b, or c */} - {list(['a', 'b', 'c'], 'OR')} - {/* a b c */} - {list(['a', 'b', 'c'], { type: 'unit', style: 'narrow' })} -
- ); -}; -``` - -### Sorting - -Look at [MDN](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator) for more options. - -> [!NOTE] -> Only available via primitive. - -```jsx -import { useIntl } from '@tiny-intl/solid-js'; - -// via primitive -const MyComponent = () => { - const { sort, collator } = useIntl(); - - return ( -
- {/* en-US: ['a', 'ä', 'Z', 'z'], swedish: ['a', 'Z', 'z', 'ä'] */} - {sort(['Z', 'a', 'z', 'ä'], { caseFirst: 'upper' })} - {/* [{ name: 'a' }, { name: 'Z' }] */} - {[{ name: 'Z' }, { name: 'a' }].sort((a, b) => collator(a.name, b.name))} -
- ); -}; -``` - -### Change locale - -```jsx -import { useIntl } from '@tiny-intl/solid-js'; - -const MyComponent = () => { - const { change, getLocale } = useIntl(); - - return ( -
- {getLocale()} - -
- ); -}; -``` diff --git a/packages/solid-js/package.json b/packages/solid-js/package.json deleted file mode 100644 index e8bbde0..0000000 --- a/packages/solid-js/package.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "name": "@tiny-intl/solid-js", - "version": "1.2.0", - "repository": { - "type": "git", - "url": "https://github.com/gridventures/tiny-intl", - "directory": "packages/solid-js" - }, - "author": "Phil Schaffarzyk ", - "license": "MIT", - "type": "module", - "files": [ - "lib" - ], - "main": "./lib/index.umd.cjs", - "module": "./lib/index.js", - "types": "./lib/types/index.d.ts", - "exports": { - ".": { - "import": "./lib/index.js", - "require": "./lib/index.umd.cjs", - "types": "./lib/types/index.d.ts" - } - }, - "scripts": { - "dev": "vite", - "build": "tsc && vite build", - "preview": "vite preview" - }, - "dependencies": { - "@tiny-intl/core": "^1.2.0" - }, - "peerDependencies": { - "solid-js": ">=1.5.0" - }, - "devDependencies": { - "rollup-plugin-visualizer": "^5.9.0", - "solid-js": "^1.8.3", - "typescript": "^5.2.2", - "vite": "^4.5.2", - "vite-plugin-dts": "3.6.0", - "vite-plugin-solid": "^2.6.1" - } -} diff --git a/packages/solid-js/src/Translate.tsx b/packages/solid-js/src/Translate.tsx deleted file mode 100644 index e7f1c2b..0000000 --- a/packages/solid-js/src/Translate.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import type { TinyIntlTranslateTemplate } from '@tiny-intl/core'; -import type { JSX } from 'solid-js'; - -import { Show, createMemo, createSignal, onCleanup, onMount, useContext } from 'solid-js'; - -import { TinyIntlContext } from './useIntl'; - -export type TranslateProps = { - children?: (value: string | null) => JSX.Element; -} & ( - | { - // Case: string translation - name: string; - count?: number; - date?: undefined; - number?: undefined; - options?: TinyIntlTranslateTemplate; - relative?: undefined; - unit?: undefined; - } - | { - // Case: dateFormat - name?: undefined; - count?: undefined; - data?: undefined; - date?: Date | string | number; - number?: undefined; - options?: Intl.DateTimeFormatOptions; - relative?: undefined; - } - | { - // Case: relativeTimeFormat - name?: undefined; - count?: undefined; - data?: undefined; - date?: Date | string | number; - number?: undefined; - options?: Intl.RelativeTimeFormatOptions; - relative?: true; - } - | { - // Case: numberFormat - name?: undefined; - count?: undefined; - data?: undefined; - date?: undefined; - number?: number; - options?: Intl.NumberFormatOptions; - relative?: undefined; - } -); - -export function Translate(props: TranslateProps) { - const intl = useContext(TinyIntlContext); - - if (!intl) { - throw new Error('useIntl must be used within a TinyIntlContext.Provider'); - } - - const [changed, setChanged] = createSignal(0); - - const translateFn = createMemo( - () => { - changed(); - - if (props.count) { - return intl.tc(props.name, props.count, props.options); - } - - if (props.date && !props.relative) { - return intl.dt(props.date, props.options); - } - - if (props.date && props.relative) { - return intl.rt(props.date, props.options); - } - - if (props.number) { - return intl.n(props.number, props.options); - } - - if (props.name) { - return intl.t(props.name, props.options); - } - - return null; - }, - null, - { - equals: (a, b) => a === b, - }, - ); - - onMount(() => { - const dispose = intl.subscribe(() => { - setChanged((prev) => prev + 1); - }); - onCleanup(() => { - dispose(); - }); - }); - - return ( - - {(childFn) => childFn(translateFn())} - - ); -} - -export const Trans = Translate; diff --git a/packages/solid-js/src/index.ts b/packages/solid-js/src/index.ts deleted file mode 100644 index a63b186..0000000 --- a/packages/solid-js/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './Translate'; -export * from './useIntl'; diff --git a/packages/solid-js/src/useIntl.tsx b/packages/solid-js/src/useIntl.tsx deleted file mode 100644 index b82c673..0000000 --- a/packages/solid-js/src/useIntl.tsx +++ /dev/null @@ -1,86 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import type { TinyIntl } from '@tiny-intl/core'; -import type { Accessor } from 'solid-js'; - -import { - createContext, - useContext, - createEffect, - createSignal, - createMemo, - onCleanup, -} from 'solid-js'; - -export const TinyIntlContext = createContext | undefined>(undefined); - -const memoizeCallback = - any>(fn: T, trackDep: () => Accessor) => - (...args: Parameters): Accessor> => { - const result = createMemo( - () => { - trackDep()(); - return fn.call(args); - }, - null, - { - equals: (a, b) => a === b, - }, - ); - return result; - }; - -export function useIntl() { - const intl = useContext(TinyIntlContext); - - if (!intl) { - throw new Error('useIntl must be used within a TinyIntlContext.Provider'); - } - - const [changed, setChanged] = createSignal(0); - - const getLocale = createMemo( - () => { - changed(); - return intl.locale; - }, - '', - { - equals: (a, b) => a === b, - }, - ); - const t = memoizeCallback(intl.t, () => changed); - const tc = memoizeCallback(intl.tc, () => changed); - const n = memoizeCallback(intl.n, () => changed); - const dt = memoizeCallback(intl.dt, () => changed); - const rt = memoizeCallback(intl.rt, () => changed); - const sort = memoizeCallback(intl.sort, () => changed); - const collator = memoizeCallback(intl.collator, () => changed); - const list = memoizeCallback(intl.list, () => changed); - - createEffect(() => { - const dispose = intl.subscribe(() => { - setChanged((prev) => prev + 1); - }); - onCleanup(() => { - dispose(); - }); - }); - - return { - ...intl, - t, - tc, - n, - dt, - /** - * @deprecated use dt instead. Will be removed in stable release. - */ - d: dt, - rt, - sort, - collator, - list, - change: intl.change, - getLocale, - }; -} diff --git a/packages/solid-js/src/vite-env.d.ts b/packages/solid-js/src/vite-env.d.ts deleted file mode 100644 index 11f02fe..0000000 --- a/packages/solid-js/src/vite-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/packages/solid-js/tsconfig.json b/packages/solid-js/tsconfig.json deleted file mode 100644 index 3795de4..0000000 --- a/packages/solid-js/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "useDefineForClassFields": true, - "lib": ["DOM", "DOM.Iterable", "ESNext"], - "allowJs": false, - "skipLibCheck": true, - "esModuleInterop": false, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "module": "ESNext", - "moduleResolution": "Node", - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "preserve", - "jsxImportSource": "solid-js", - }, - "include": ["src"], - "references": [{ "path": "./tsconfig.node.json" }] -} diff --git a/packages/solid-js/tsconfig.node.json b/packages/solid-js/tsconfig.node.json deleted file mode 100644 index 9d31e2a..0000000 --- a/packages/solid-js/tsconfig.node.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "compilerOptions": { - "composite": true, - "module": "ESNext", - "moduleResolution": "Node", - "allowSyntheticDefaultImports": true - }, - "include": ["vite.config.ts"] -} diff --git a/packages/solid-js/vite.config.ts b/packages/solid-js/vite.config.ts deleted file mode 100644 index af518b9..0000000 --- a/packages/solid-js/vite.config.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { resolve } from 'path'; - -import { visualizer } from 'rollup-plugin-visualizer'; -import { defineConfig } from 'vite'; -import dts from 'vite-plugin-dts'; -import solid from 'vite-plugin-solid'; - -export default defineConfig({ - plugins: [ - dts({ - outDir: resolve(__dirname, 'lib', 'types'), - insertTypesEntry: true, - }), - solid(), - visualizer({ - filename: 'dist/stats.html', - gzipSize: true, - brotliSize: true, - }), - ], - build: { - outDir: resolve(__dirname, 'lib'), - lib: { - // Could also be a dictionary or array of multiple entry points - entry: resolve(__dirname, 'src', 'index.ts'), - name: 'tinyIntl', - // the proper extensions will be added - fileName: 'index', - formats: ['es', 'cjs', 'umd'], - }, - rollupOptions: { - external: ['solid-js'], - output: { - globals: { - 'solid-js': 'solid', - }, - }, - }, - }, -}); From d82a83ad5039422692a392807dfc696d1866c235 Mon Sep 17 00:00:00 2001 From: Phil Date: Wed, 12 Aug 2026 11:41:30 +0200 Subject: [PATCH 02/48] chore: drop solid-js lint dependencies Remove the ESLint override that targeted packages/solid-js and the two devDependencies that existed only to lint it: @gridventures/eslint-config-solid-js and eslint-plugin-solid (pulled in transitively). Follows the removal of the solid-js package itself. --- .eslintrc.cjs | 9 - package-lock.json | 518 ---------------------------------------------- package.json | 2 - 3 files changed, 529 deletions(-) diff --git a/.eslintrc.cjs b/.eslintrc.cjs index a09c094..f3ef3cb 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -69,15 +69,6 @@ module.exports = { }, }, - { - files: ['packages/solid-js/**/*.ts', 'packages/solid-js/**/*.tsx'], - - extends: [ - '@gridventures/eslint-config-solid-js/typescript', - '@gridventures/eslint-config-base/prettier', - ], - }, - { files: ['**/*.config.*'], diff --git a/package-lock.json b/package-lock.json index 5836267..5c90eb8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,11 +16,9 @@ "@commitlint/config-conventional": "^17.4.4", "@gridventures/eslint-config-base": "^1.3.3", "@gridventures/eslint-config-react": "^1.4.0", - "@gridventures/eslint-config-solid-js": "^1.4.1", "@gridventures/eslint-config-typescript": "^1.3.3", "@types/node": "^20.10.3", "eslint": "^8.35.0", - "eslint-plugin-solid": "^0.13.0", "husky": "^8.0.3", "lerna": "^7.4.1", "lint-staged": "^15.0.1", @@ -273,38 +271,6 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.23.5.tgz", - "integrity": "sha512-QELlRWxSpgdwdJzSJn4WAhKC+hvw/AtHbbrIoncKHkhKKR/luAlKkgBDcri1EzWAo8f8VvYVryEHN4tax/V67A==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.23.0", - "@babel/helper-member-expression-to-functions": "^7.23.0", - "@babel/helper-optimise-call-expression": "^7.22.5", - "@babel/helper-replace-supers": "^7.22.20", - "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", - "@babel/helper-split-export-declaration": "^7.22.6", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, "node_modules/@babel/helper-environment-visitor": { "version": "7.22.20", "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", @@ -339,18 +305,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.23.0.tgz", - "integrity": "sha512-6gfrPwh7OuT6gZyJZvd6WbTfrqAo7vm4xCzAXOusKqq/vWdKXphTpj5klHKNmRUU6/QRGlBsyU9mAIPaWHlqJA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.23.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-module-imports": { "version": "7.22.15", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", @@ -382,18 +336,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", - "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-plugin-utils": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", @@ -403,23 +345,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.20.tgz", - "integrity": "sha512-qsW0In3dbwQUbK8kejJ4R7IHVGwHJlV6lpG6UA7a9hSa2YEiAib+N1T2kr6PEeUT+Fl7najmSOS6SmAwCHK6Tw==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-member-expression-to-functions": "^7.22.15", - "@babel/helper-optimise-call-expression": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, "node_modules/@babel/helper-simple-access": { "version": "7.22.5", "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", @@ -432,18 +357,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", - "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", - "dev": true, - "dependencies": { - "@babel/types": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-split-export-declaration": { "version": "7.22.6", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", @@ -609,38 +522,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.23.3.tgz", - "integrity": "sha512-9EiNjVJOMwCO+43TqoTrgQ8jMwcAd0sWyXi9RPfIsLTj4R2MADDDQXELhffaUx/uJv2AYcxBgPwH6j4TIA4ytQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.23.3.tgz", - "integrity": "sha512-aVS0F65LKsdNOtcz6FRCpE4OgsP2OFnW46qNxNIX9h3wuzaNcSQsJysuMwqSibC98HPrf2vCgtxKNwS0DAlgcA==", - "dev": true, - "dependencies": { - "@babel/helper-module-transforms": "^7.23.3", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-simple-access": "^7.22.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-transform-react-jsx": { "version": "7.23.4", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.23.4.tgz", @@ -705,43 +586,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-typescript": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.23.5.tgz", - "integrity": "sha512-2fMkXEJkrmwgu2Bsv1Saxgj30IXZdJ+84lQcKKI7sm719oXs0BBw2ZENKdJdR1PjWndgLCEBNXJOri0fk7RYQA==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.22.5", - "@babel/helper-create-class-features-plugin": "^7.23.5", - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/plugin-syntax-typescript": "^7.23.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-typescript": { - "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.23.3.tgz", - "integrity": "sha512-17oIGVlqz6CchO9RFYn5U6ZpWRZIngayYCtrPRSgANSwC2V1Jb+iP74nVxzzXJte8b8BYxrL1yY96xfhTBrNNQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.22.5", - "@babel/helper-validator-option": "^7.22.15", - "@babel/plugin-syntax-jsx": "^7.23.3", - "@babel/plugin-transform-modules-commonjs": "^7.23.3", - "@babel/plugin-transform-typescript": "^7.23.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/runtime": { "version": "7.23.5", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.5.tgz", @@ -1565,18 +1409,6 @@ "@gridventures/eslint-config-typescript": "^1.3.3" } }, - "node_modules/@gridventures/eslint-config-solid-js": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@gridventures/eslint-config-solid-js/-/eslint-config-solid-js-1.4.1.tgz", - "integrity": "sha512-YehiEqBY9Wql6PtgMjSNqIrG4rUlKc7FtyjKmX1/MxahV19WoQJjRK82HquSpyvLPEcZaYFZLYxFoxM0eUBoIw==", - "dev": true, - "dependencies": { - "@gridventures/eslint-config-base": "^1.3.3" - }, - "optionalDependencies": { - "@gridventures/eslint-config-typescript": "^1.3.3" - } - }, "node_modules/@gridventures/eslint-config-typescript": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@gridventures/eslint-config-typescript/-/eslint-config-typescript-1.3.3.tgz", @@ -3428,10 +3260,6 @@ "resolved": "packages/react", "link": true }, - "node_modules/@tiny-intl/solid-js": { - "resolved": "packages/solid-js", - "link": true - }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -4598,34 +4426,6 @@ "dequal": "^2.0.3" } }, - "node_modules/babel-plugin-jsx-dom-expressions": { - "version": "0.37.9", - "resolved": "https://registry.npmjs.org/babel-plugin-jsx-dom-expressions/-/babel-plugin-jsx-dom-expressions-0.37.9.tgz", - "integrity": "sha512-6w+zs2i14fVanj4e1hXCU5cp+x0U0LJ5jScknpMZZUteHhwFRGJflHMVJ+xAcW7ku41FYjr7DgtK9mnc2SXlJg==", - "dev": true, - "dependencies": { - "@babel/helper-module-imports": "7.18.6", - "@babel/plugin-syntax-jsx": "^7.18.6", - "@babel/types": "^7.20.7", - "html-entities": "2.3.3", - "validate-html-nesting": "^1.2.1" - }, - "peerDependencies": { - "@babel/core": "^7.20.12" - } - }, - "node_modules/babel-plugin-jsx-dom-expressions/node_modules/@babel/helper-module-imports": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", - "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/babel-plugin-transform-hook-names": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/babel-plugin-transform-hook-names/-/babel-plugin-transform-hook-names-1.0.2.tgz", @@ -4635,18 +4435,6 @@ "@babel/core": "^7.12.10" } }, - "node_modules/babel-preset-solid": { - "version": "1.8.6", - "resolved": "https://registry.npmjs.org/babel-preset-solid/-/babel-preset-solid-1.8.6.tgz", - "integrity": "sha512-Ened42CHjU4EFkvNeS042/3Pm21yvMWn8p4G4ddzQTlKaMwSGGD1VciA/e7EshBVHJCcBj9vHiUd/r3A4qLPZA==", - "dev": true, - "dependencies": { - "babel-plugin-jsx-dom-expressions": "^0.37.9" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -6532,125 +6320,6 @@ "semver": "bin/semver.js" } }, - "node_modules/eslint-plugin-solid": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-solid/-/eslint-plugin-solid-0.13.0.tgz", - "integrity": "sha512-Sutd+DxEGu9+Z9ITtHKXRAClxVe1a6C1XQZSuN8iBsMy0IAVEc6Tca1UYgc7tD2ZrRRjZKB9mohBOaZl5NJLgg==", - "dev": true, - "dependencies": { - "@typescript-eslint/utils": "^6.4.0", - "is-html": "^2.0.0", - "jsx-ast-utils": "^3.3.3", - "kebab-case": "^1.0.2", - "known-css-properties": "^0.24.0", - "style-to-object": "^0.3.0" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/eslint-plugin-solid/node_modules/@typescript-eslint/scope-manager": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.13.1.tgz", - "integrity": "sha512-BW0kJ7ceiKi56GbT2KKzZzN+nDxzQK2DS6x0PiSMPjciPgd/JRQGMibyaN2cPt2cAvuoH0oNvn2fwonHI+4QUQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.13.1", - "@typescript-eslint/visitor-keys": "6.13.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/eslint-plugin-solid/node_modules/@typescript-eslint/types": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.13.1.tgz", - "integrity": "sha512-gjeEskSmiEKKFIbnhDXUyiqVma1gRCQNbVZ1C8q7Zjcxh3WZMbzWVfGE9rHfWd1msQtPS0BVD9Jz9jded44eKg==", - "dev": true, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/eslint-plugin-solid/node_modules/@typescript-eslint/typescript-estree": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.13.1.tgz", - "integrity": "sha512-sBLQsvOC0Q7LGcUHO5qpG1HxRgePbT6wwqOiGLpR8uOJvPJbfs0mW3jPA3ujsDvfiVwVlWUDESNXv44KtINkUQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.13.1", - "@typescript-eslint/visitor-keys": "6.13.1", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/eslint-plugin-solid/node_modules/@typescript-eslint/utils": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.13.1.tgz", - "integrity": "sha512-ouPn/zVoan92JgAegesTXDB/oUp6BP1v8WpfYcqh649ejNc9Qv+B4FF2Ff626kO1xg0wWwwG48lAJ4JuesgdOw==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.13.1", - "@typescript-eslint/types": "6.13.1", - "@typescript-eslint/typescript-estree": "6.13.1", - "semver": "^7.5.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - } - }, - "node_modules/eslint-plugin-solid/node_modules/@typescript-eslint/visitor-keys": { - "version": "6.13.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.13.1.tgz", - "integrity": "sha512-NDhQUy2tg6XGNBGDRm1XybOHSia8mcXmlbKWoQP+nm1BIIMxa55shyJfZkHpEBN62KNPLrocSM2PdPcaLgDKMQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.13.1", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -7778,30 +7447,12 @@ "node": ">=10" } }, - "node_modules/html-entities": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz", - "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==", - "dev": true - }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, - "node_modules/html-tags": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", - "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/http-cache-semantics": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", @@ -8095,12 +7746,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/inline-style-parser": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.1.1.tgz", - "integrity": "sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==", - "dev": true - }, "node_modules/inquirer": { "version": "8.2.6", "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", @@ -8361,18 +8006,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-html": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-html/-/is-html-2.0.0.tgz", - "integrity": "sha512-S+OpgB5i7wzIue/YSE5hg0e5ZYfG3hhpNh9KGl6ayJ38p7ED6wxQLd1TV91xHpcTvw90KMJ9EwN3F/iNflHBVg==", - "dev": true, - "dependencies": { - "html-tags": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/is-interactive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", @@ -8630,18 +8263,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-what": { - "version": "4.1.16", - "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", - "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", - "dev": true, - "engines": { - "node": ">=12.13" - }, - "funding": { - "url": "https://github.com/sponsors/mesqueeb" - } - }, "node_modules/is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -8982,12 +8603,6 @@ "node": ">=4.0" } }, - "node_modules/kebab-case": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/kebab-case/-/kebab-case-1.0.2.tgz", - "integrity": "sha512-7n6wXq4gNgBELfDCpzKc+mRrZFs7D+wgfF5WRFLNAr4DA/qtr9Js8uOAVAfHhuLMfAcQ0pRKqbpjx+TcJVdE1Q==", - "dev": true - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -9015,12 +8630,6 @@ "graceful-fs": "^4.1.11" } }, - "node_modules/known-css-properties": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.24.0.tgz", - "integrity": "sha512-RTSoaUAfLvpR357vWzAz/50Q/BmHfmE6ETSWfutT0AJiw10e6CmcdYRQJlLRd95B53D0Y2aD1jSxD3V3ySF+PA==", - "dev": true - }, "node_modules/kolorist": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", @@ -10418,21 +10027,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/merge-anything": { - "version": "5.1.7", - "resolved": "https://registry.npmjs.org/merge-anything/-/merge-anything-5.1.7.tgz", - "integrity": "sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==", - "dev": true, - "dependencies": { - "is-what": "^4.1.8" - }, - "engines": { - "node": ">=12.13" - }, - "funding": { - "url": "https://github.com/sponsors/mesqueeb" - } - }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -13553,15 +13147,6 @@ "node": ">=10" } }, - "node_modules/seroval": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/seroval/-/seroval-0.15.1.tgz", - "integrity": "sha512-OPVtf0qmeC7RW+ScVX+7aOS+xoIM7pWcZ0jOWg2aTZigCydgRB04adfteBRbecZnnrO1WuGQ+C3tLeBBzX2zSQ==", - "dev": true, - "engines": { - "node": ">=10" - } - }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -13976,30 +13561,6 @@ "node": ">= 10" } }, - "node_modules/solid-js": { - "version": "1.8.7", - "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.8.7.tgz", - "integrity": "sha512-9dzrSVieh2zj3SnJ02II6xZkonR6c+j/91b7XZUNcC6xSaldlqjjGh98F1fk5cRJ8ZTkzqF5fPIWDxEOs6QZXA==", - "dev": true, - "dependencies": { - "csstype": "^3.1.0", - "seroval": "^0.15.1" - } - }, - "node_modules/solid-refresh": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/solid-refresh/-/solid-refresh-0.5.3.tgz", - "integrity": "sha512-Otg5it5sjOdZbQZJnvo99TEBAr6J7PQ5AubZLNU6szZzg3RQQ5MX04oteBIIGDs0y2Qv8aXKm9e44V8z+UnFdw==", - "dev": true, - "dependencies": { - "@babel/generator": "^7.21.1", - "@babel/helper-module-imports": "^7.18.6", - "@babel/types": "^7.21.2" - }, - "peerDependencies": { - "solid-js": "^1.3" - } - }, "node_modules/sort-keys": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", @@ -14351,15 +13912,6 @@ "node": ">=4" } }, - "node_modules/style-to-object": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-0.3.0.tgz", - "integrity": "sha512-CzFnRRXhzWIdItT3OmF8SQfWyahHhjq3HwcMNCNLn+N7klOOqPjMeG/4JSu77D7ypZdGvSzvkrbyeTMizz2VrA==", - "dev": true, - "dependencies": { - "inline-style-parser": "0.1.1" - } - }, "node_modules/sucrase": { "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", @@ -14664,18 +14216,6 @@ "node": ">=8" } }, - "node_modules/ts-api-utils": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", - "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", - "dev": true, - "engines": { - "node": ">=16.13.0" - }, - "peerDependencies": { - "typescript": ">=4.2.0" - } - }, "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", @@ -15837,12 +15377,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/validate-html-nesting": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/validate-html-nesting/-/validate-html-nesting-1.2.2.tgz", - "integrity": "sha512-hGdgQozCsQJMyfK5urgFcWEqsSSrK63Awe0t/IMR0bZ0QMtnuaiHzThW81guu3qx9abLi99NEuiaN6P9gVYsNg==", - "dev": true - }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -16000,39 +15534,6 @@ } } }, - "node_modules/vite-plugin-solid": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/vite-plugin-solid/-/vite-plugin-solid-2.7.2.tgz", - "integrity": "sha512-GV2SMLAibOoXe76i02AsjAg7sbm/0lngBlERvJKVN67HOrJsHcWgkt0R6sfGLDJuFkv2aBe14Zm4vJcNME+7zw==", - "dev": true, - "dependencies": { - "@babel/core": "^7.23.0", - "@babel/preset-typescript": "^7.23.0", - "@types/babel__core": "^7.20.2", - "babel-preset-solid": "^1.8.0", - "merge-anything": "^5.1.7", - "solid-refresh": "^0.5.3", - "vitefu": "^0.2.4" - }, - "peerDependencies": { - "solid-js": "^1.7.2", - "vite": "^3.0.0 || ^4.0.0" - } - }, - "node_modules/vitefu": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz", - "integrity": "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==", - "dev": true, - "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" - }, - "peerDependenciesMeta": { - "vite": { - "optional": true - } - } - }, "node_modules/vitest": { "version": "0.34.6", "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.6.tgz", @@ -16651,25 +16152,6 @@ "react": ">=18.0.0", "react-dom": ">=18.0.0" } - }, - "packages/solid-js": { - "name": "@tiny-intl/solid-js", - "version": "1.2.0", - "license": "MIT", - "dependencies": { - "@tiny-intl/core": "^1.2.0" - }, - "devDependencies": { - "rollup-plugin-visualizer": "^5.9.0", - "solid-js": "^1.8.3", - "typescript": "^5.2.2", - "vite": "^4.5.2", - "vite-plugin-dts": "3.6.0", - "vite-plugin-solid": "^2.6.1" - }, - "peerDependencies": { - "solid-js": ">=1.5.0" - } } } } diff --git a/package.json b/package.json index 6ab0ca7..d6c5fe1 100644 --- a/package.json +++ b/package.json @@ -19,11 +19,9 @@ "@commitlint/config-conventional": "^17.4.4", "@gridventures/eslint-config-base": "^1.3.3", "@gridventures/eslint-config-react": "^1.4.0", - "@gridventures/eslint-config-solid-js": "^1.4.1", "@gridventures/eslint-config-typescript": "^1.3.3", "@types/node": "^20.10.3", "eslint": "^8.35.0", - "eslint-plugin-solid": "^0.13.0", "husky": "^8.0.3", "lerna": "^7.4.1", "lint-staged": "^15.0.1", From 8c12b37076b94dfec35e630425bad78b3a59a3c7 Mon Sep 17 00:00:00 2001 From: Phil Date: Wed, 12 Aug 2026 11:41:40 +0200 Subject: [PATCH 03/48] docs: remove solid-js from the package list Drop the @tiny-intl/solid README entry (its link target, ./packages/solid, was already wrong -- the directory was packages/solid-js) and add a note that @tiny-intl/solid-js was removed, with published versions up to 1.2.0 remaining installable but no longer maintained. --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index eb7773a..1a25fac 100644 --- a/README.md +++ b/README.md @@ -7,4 +7,7 @@ A tiny library to translate or transform strings, dates and numbers based on nat - [@tiny-intl/core](./packages/core) ![Core Size](https://deno.bundlejs.com/badge?q=@tiny-intl/core&treeshake=%5B*%5D) - [@tiny-intl/react](./packages/react) ![React Adapter Size](https://deno.bundlejs.com/badge?q=@tiny-intl/react&treeshake=%5B*%5D&config=%7B%22esbuild%22:%7B%22external%22:%5B%22react%22%5D%7D%7D) - [@tiny-intl/preact](./packages/preact) ![Preact Adapter Size](https://deno.bundlejs.com/badge?q=@tiny-intl/preact&treeshake=%5B*%5D&config=%7B%22esbuild%22:%7B%22external%22:%5B%22preact%22,%22preact/compat%22%5D%7D%7D) -- [@tiny-intl/solid](./packages/solid) ![Solid Adater Size](https://deno.bundlejs.com/badge?q=@tiny-intl/solid-js&treeshake=%5B*%5D&config=%7B%22esbuild%22:%7B%22external%22:%5B%22solid-js%22%5D%7D%7D) + +> [!NOTE] +> `@tiny-intl/solid-js` was removed in the next release. Published versions up +> to 1.2.0 remain installable from npm but are no longer maintained. From 51588cd8a7fd51bdef4d9e2b315fe33553f4ace1 Mon Sep 17 00:00:00 2001 From: Phil Date: Wed, 12 Aug 2026 11:41:51 +0200 Subject: [PATCH 04/48] chore: remove solid-js commit scope The solid-js scope in the commitlint scope-enum no longer has a package behind it now that packages/solid-js is gone. Committed last so that no earlier commit in this series needed the solid-js scope while it was still a valid enum value. --- commitlint.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commitlint.config.js b/commitlint.config.js index 7dfaf90..ec72210 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -5,7 +5,7 @@ module.exports = { // If you want to implement custom scopes uncomment this rules: { 'scope-enum': async () => { - return [2, 'always', ['core', 'react', 'solid-js', 'preact']]; + return [2, 'always', ['core', 'react', 'preact']]; }, }, }; From a0a5747c64f241aafbd1cb664fc11fcc7ddf9a82 Mon Sep 17 00:00:00 2001 From: Phil Date: Wed, 12 Aug 2026 11:46:10 +0200 Subject: [PATCH 05/48] fix(core): read Intl formatter caches with the key they are written with Each of n(), dt(), rt(), collator() and list() wrote its formatter into the cache under cacheKey but read it back with cache.get(locale), so every lookup missed and a new Intl.* formatter was constructed on every call. Read with cacheKey instead, matching the write. Safe because change() clears all five caches on every locale change. Also drops the duplicate numberFormatCache.clear() in change() and normalises collator's newCacheKey(options || {}) to newCacheKey(options) so a no-options call keys as '_' like the other four. Co-Authored-By: Claude Opus 5 --- packages/core/src/createTinyIntl.ts | 13 ++- packages/core/tests/index.test.ts | 136 ++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+), 7 deletions(-) diff --git a/packages/core/src/createTinyIntl.ts b/packages/core/src/createTinyIntl.ts index a6314d7..a132c6d 100644 --- a/packages/core/src/createTinyIntl.ts +++ b/packages/core/src/createTinyIntl.ts @@ -92,7 +92,6 @@ export function createTinyIntl( numberFormatCache.clear(); dateTimeFormatCache.clear(); relativeTimeFormatCache.clear(); - numberFormatCache.clear(); listFormatCache.clear(); collatorCache.clear(); if (staticDict) { @@ -128,7 +127,7 @@ export function createTinyIntl( function n(number: number, options?: Intl.NumberFormatOptions): string { const cacheKey = newCacheKey(options); - let formatter = numberFormatCache.get(locale); + let formatter = numberFormatCache.get(cacheKey); if (!formatter) { formatter = new Intl.NumberFormat(locale, options); numberFormatCache.set(cacheKey, formatter); @@ -139,7 +138,7 @@ export function createTinyIntl( function dt(date: Date | string | number, options?: Intl.DateTimeFormatOptions): string { const dateValue = new Date(date); const cacheKey = newCacheKey(options); - let formatter = dateTimeFormatCache.get(locale); + let formatter = dateTimeFormatCache.get(cacheKey); if (!formatter) { formatter = new Intl.DateTimeFormat(locale, options); dateTimeFormatCache.set(cacheKey, formatter); @@ -159,7 +158,7 @@ export function createTinyIntl( return ''; } const cacheKey = newCacheKey(rtOptions); - let formatter = relativeTimeFormatCache.get(locale); + let formatter = relativeTimeFormatCache.get(cacheKey); if (!formatter) { formatter = new Intl.RelativeTimeFormat(locale, rtOptions); relativeTimeFormatCache.set(cacheKey, formatter); @@ -172,8 +171,8 @@ export function createTinyIntl( console.warn('Intl.Collator is not supported in this browser'); return (x: string, y: string) => x.localeCompare(y); } - const cacheKey = newCacheKey(options || {}); - let formatter = collatorCache.get(locale); + const cacheKey = newCacheKey(options); + let formatter = collatorCache.get(cacheKey); if (!formatter) { formatter = new Intl.Collator(locale, options); collatorCache.set(cacheKey, formatter); @@ -197,7 +196,7 @@ export function createTinyIntl( } const intlOptions = typeof options === 'string' ? ({ type, style: 'long' } as const) : options; const cacheKey = newCacheKey(intlOptions); - let formatter = listFormatCache.get(locale); + let formatter = listFormatCache.get(cacheKey); if (!formatter) { formatter = new Intl.ListFormat(locale, intlOptions); listFormatCache.set(cacheKey, formatter); diff --git a/packages/core/tests/index.test.ts b/packages/core/tests/index.test.ts index f9dad1d..c5ea672 100644 --- a/packages/core/tests/index.test.ts +++ b/packages/core/tests/index.test.ts @@ -173,6 +173,142 @@ describe('@tiny-intl/core', () => { expect(intl.list(['a', 'b', 'c'], { type: 'disjunction' })).toBe('a, b, or c'); }); + // Forces a genuine locale change (twice, landing back on en-US) so every + // formatter cache is guaranteed empty before we start counting constructor + // calls. A plain `await intl.change('en-US')` is not reliable here because + // if the shared `intl` instance already sits on 'en-US' (the common case, + // since afterEach resets to it) that call hits the `change()` early return + // and clears nothing, leaving caches polluted by whichever earlier test + // ran last. + async function resetFormatterCaches() { + await intl.change('de-DE'); + await intl.change('en-US'); + } + + // `vi.spyOn(Intl, 'RelativeTimeFormat' | 'ListFormat')` blows up with + // "Constructor ... requires 'new'" in this environment (unlike NumberFormat, + // DateTimeFormat and Collator, which spy fine) — vitest's spy wrapper loses + // the internal slot these two constructors check for. As a fallback, swap + // the constructor for a subclass that counts `super()` calls, then restore + // it — the same direct-reassignment technique already used below in + // 'fallbacks if newer Intl features are not supported'. + function countingConstructor unknown>(original: T) { + const state = { calls: 0 }; + class Counting extends (original as new (...args: never[]) => unknown) { + constructor(...args: never[]) { + super(...args); + state.calls += 1; + } + } + return { ctor: Counting as unknown as T, state }; + } + + it('reuses cached Intl.NumberFormat instances', async ({ expect }) => { + await resetFormatterCaches(); + + const numberFormatSpy = vi.spyOn(Intl, 'NumberFormat'); + intl.n(1); + intl.n(2); + intl.n(3); + expect(numberFormatSpy).toHaveBeenCalledTimes(1); + + intl.n(4, { style: 'percent' }); + intl.n(5, { style: 'percent' }); + expect(numberFormatSpy).toHaveBeenCalledTimes(2); + numberFormatSpy.mockRestore(); + }); + + it('reuses cached Intl.DateTimeFormat instances', async ({ expect }) => { + await resetFormatterCaches(); + + const dateTimeFormatSpy = vi.spyOn(Intl, 'DateTimeFormat'); + intl.dt('2021-01-01'); + intl.dt('2021-06-15'); + intl.dt('2022-12-31'); + expect(dateTimeFormatSpy).toHaveBeenCalledTimes(1); + + intl.dt('2021-01-01', { dateStyle: 'full' }); + intl.dt('2021-06-15', { dateStyle: 'full' }); + expect(dateTimeFormatSpy).toHaveBeenCalledTimes(2); + dateTimeFormatSpy.mockRestore(); + }); + + it('reuses cached Intl.RelativeTimeFormat instances', async ({ expect }) => { + await resetFormatterCaches(); + + const oldDateNow = Date.now; + Date.now = () => new Date('2020-01-01').getTime(); + + /* eslint-disable @typescript-eslint/ban-ts-comment */ + const originalRelativeTimeFormat = Intl.RelativeTimeFormat; + const { ctor, state } = countingConstructor(originalRelativeTimeFormat); + // @ts-ignore + Intl.RelativeTimeFormat = ctor; + + intl.rt('2021-01-01'); + intl.rt('2021-02-01'); + intl.rt('2021-03-01'); + expect(state.calls).toBe(1); + + intl.rt('2021-01-01', { style: 'long' }); + intl.rt('2021-02-01', { style: 'long' }); + expect(state.calls).toBe(2); + + // @ts-ignore + Intl.RelativeTimeFormat = originalRelativeTimeFormat; + /* eslint-enable @typescript-eslint/ban-ts-comment */ + Date.now = oldDateNow; + }); + + it('reuses cached Intl.ListFormat instances', async ({ expect }) => { + await resetFormatterCaches(); + + /* eslint-disable @typescript-eslint/ban-ts-comment */ + const originalListFormat = Intl.ListFormat; + const { ctor, state } = countingConstructor(originalListFormat); + // @ts-ignore + Intl.ListFormat = ctor; + + intl.list(['a', 'b', 'c']); + intl.list(['d', 'e']); + intl.list(['f']); + expect(state.calls).toBe(1); + + intl.list(['a', 'b', 'c'], { type: 'disjunction' }); + intl.list(['d', 'e'], { type: 'disjunction' }); + expect(state.calls).toBe(2); + + // @ts-ignore + Intl.ListFormat = originalListFormat; + /* eslint-enable @typescript-eslint/ban-ts-comment */ + }); + + it('reuses cached Intl.Collator instances', async ({ expect }) => { + await resetFormatterCaches(); + + const collatorSpy = vi.spyOn(Intl, 'Collator'); + intl.collator(); + intl.collator(); + intl.collator(); + expect(collatorSpy).toHaveBeenCalledTimes(1); + + intl.collator({ caseFirst: 'upper' }); + intl.collator({ caseFirst: 'upper' }); + expect(collatorSpy).toHaveBeenCalledTimes(2); + collatorSpy.mockRestore(); + }); + + it('rebuilds Intl formatters after a locale change', async ({ expect }) => { + await resetFormatterCaches(); + + const spy = vi.spyOn(Intl, 'NumberFormat'); + intl.n(1000); + await intl.change('de-DE'); + intl.n(1000); + expect(spy).toHaveBeenCalledTimes(2); + spy.mockRestore(); + }); + it('fallbacks if newer Intl features are not supported', async ({ expect }) => { await intl.change('de-DE'); /* eslint-disable @typescript-eslint/ban-ts-comment */ From 62b25ecfb8cbb7f0ba41d9beb432ebf1885beeae Mon Sep 17 00:00:00 2001 From: Phil Date: Wed, 12 Aug 2026 11:57:49 +0200 Subject: [PATCH 06/48] test(react): add vitest harness and useIntl/Translate tests @tiny-intl/react had zero automated tests despite being published to npm. Add a vitest + jsdom + @testing-library/react setup (versions pinned to stay compatible with the repo's vitest 0.34 / vite 4) and cover the provider guard, translation, locale-change re-rendering, and number/date formatting for useIntl() and Translate. Also relax no-shadow for packages/*/tests/**, since Testing Library's `screen` export legitimately shadows the browser global of the same name. --- .eslintrc.cjs | 11 + package-lock.json | 1057 +++++++++++++++++++++-- packages/react/package.json | 10 +- packages/react/tests/Translate.test.tsx | 112 +++ packages/react/tests/useIntl.test.tsx | 115 +++ packages/react/vite.config.ts | 6 +- 6 files changed, 1242 insertions(+), 69 deletions(-) create mode 100644 packages/react/tests/Translate.test.tsx create mode 100644 packages/react/tests/useIntl.test.tsx diff --git a/.eslintrc.cjs b/.eslintrc.cjs index f3ef3cb..c63eb4b 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -76,5 +76,16 @@ module.exports = { 'import/no-extraneous-dependencies': 'off', }, }, + + { + // Testing Library's `screen` export shadows the browser global of the + // same name; test files legitimately import it, so relax the rule + // only here rather than for the whole codebase. + files: ['packages/*/tests/**'], + + rules: { + 'no-shadow': 'off', + }, + }, ], }; diff --git a/package-lock.json b/package-lock.json index 5c90eb8..97a3ccf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3248,6 +3248,171 @@ "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", "dev": true }, + "node_modules/@testing-library/dom": { + "version": "9.3.4", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", + "integrity": "sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.1.3", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/@testing-library/dom/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", + "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "deep-equal": "^2.0.5" + } + }, + "node_modules/@testing-library/dom/node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/preact": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@testing-library/preact/-/preact-3.2.4.tgz", + "integrity": "sha512-F+kJ243LP6VmEK1M809unzTE/ijg+bsMNuiRN0JEDIJBELKKDNhdgC/WrUSZ7klwJvtlO3wQZ9ix+jhObG07Fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@testing-library/dom": "^8.11.1" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "preact": ">=10 || ^10.0.0-alpha.0 || ^10.0.0-beta.0" + } + }, + "node_modules/@testing-library/preact/node_modules/@testing-library/dom": { + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.1.tgz", + "integrity": "sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.1.3", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@testing-library/preact/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/preact/node_modules/aria-query": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", + "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "deep-equal": "^2.0.5" + } + }, + "node_modules/@testing-library/preact/node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@testing-library/preact/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "14.3.1", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.3.1.tgz", + "integrity": "sha512-H99XjUhWQw0lTgyMN05W3xQG1Nh4lq574D8keFf1dDoNTJgp66VbJozRaczoF+wsiaPJNt/TcnfpLGufGxSrZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@testing-library/dom": "^9.0.0", + "@types/react-dom": "^18.0.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/@tiny-intl/core": { "resolved": "packages/core", "link": true @@ -3345,6 +3510,13 @@ "integrity": "sha512-ebDJ9b0e702Yr7pWgB0jzm+CX4Srzz8RcXtLJDJB+BSccqMa36uyH/zUsSYao5+BD1ytv3k3rPYCq4mAE1hsXA==", "dev": true }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -3469,6 +3641,16 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, "node_modules/@types/scheduler": { "version": "0.16.8", "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", @@ -3699,6 +3881,7 @@ "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-0.34.6.tgz", "integrity": "sha512-fivy/OK2d/EsJFoEoxHFEnNGTg+MmdZBAVK9Ka4qhXR2K3J0DS08vcGVwzDtXSuUMabLv4KtPcpSKkcMXFDViw==", "dev": true, + "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.1", "@bcoe/v8-coverage": "^0.2.3", @@ -3972,6 +4155,14 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "deprecated": "Use your platform's native atob() and btoa() methods instead", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -4692,14 +4883,50 @@ } }, "node_modules/call-bind": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", "dev": true, + "license": "MIT", "dependencies": { - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5418,6 +5645,19 @@ "node": ">= 8" } }, + "node_modules/cssstyle": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-3.0.0.tgz", + "integrity": "sha512-N4u2ABATi3Qplzf0hWbVCdjenim8F3ojEXpBDF5hBpjzW182MjNGLqfmQ0SkSPeQ+V86ZXgeH8aXj6kayd4jgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "rrweb-cssom": "^0.6.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/csstype": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", @@ -5439,6 +5679,58 @@ "node": ">=8" } }, + "node_modules/data-urls": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-4.0.0.tgz", + "integrity": "sha512-/mMTei/JXPqvFqQtfyTowxmJVwr2PVAeCcDxyFf6LhoOu/09TX2OX3kb2wzi4DMXcfj4OItwDOnhl5oziPnT6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/data-urls/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz", + "integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/dateformat": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", @@ -5505,6 +5797,13 @@ "node": ">=0.10.0" } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, "node_modules/dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -5523,6 +5822,39 @@ "node": ">=6" } }, + "node_modules/deep-equal": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", + "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.0", + "call-bind": "^1.0.5", + "es-get-iterator": "^1.1.3", + "get-intrinsic": "^1.2.2", + "is-arguments": "^1.1.1", + "is-array-buffer": "^3.0.2", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "isarray": "^2.0.5", + "object-is": "^1.1.5", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.5.1", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -5542,17 +5874,21 @@ } }, "node_modules/define-data-property": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/define-lazy-prop": { @@ -5662,6 +5998,37 @@ "node": ">=6.0.0" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/domexception": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", + "integrity": "sha512-A2is4PLG+eeSfoTMA95/s4pvAoSo2mKtiM5jlHkAVewmiO8ISFTFKZjH7UAM1Atli/OT/7JHOrJRJiMKUZKYBw==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "license": "MIT", + "dependencies": { + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/domexception/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, "node_modules/dot-prop": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", @@ -5695,6 +6062,21 @@ "node": ">=12" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -5778,6 +6160,19 @@ "node": ">=8.6" } }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -5867,6 +6262,47 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-get-iterator": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", + "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "has-symbols": "^1.0.3", + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/es-iterator-helpers": { "version": "1.0.15", "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.15.tgz", @@ -5889,6 +6325,19 @@ "safe-array-concat": "^1.0.1" } }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-set-tostringtag": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", @@ -6942,15 +7391,25 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", - "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", "dev": true, + "license": "MIT", "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7078,6 +7537,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -7286,12 +7759,13 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3" + "license": "MIT", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7358,12 +7832,13 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.2" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7382,10 +7857,11 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -7394,12 +7870,13 @@ } }, "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, + "license": "MIT", "dependencies": { - "has-symbols": "^1.0.2" + "has-symbols": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -7415,10 +7892,11 @@ "dev": true }, "node_modules/hasown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "dev": true, + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -7447,6 +7925,19 @@ "node": ">=10" } }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -7773,14 +8264,15 @@ } }, "node_modules/internal-slot": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", - "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "dev": true, + "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.2", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -7799,12 +8291,29 @@ "node": ">= 12" } }, - "node_modules/ip-address/node_modules/sprintf-js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", - "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", - "dev": true - }, + "node_modules/ip-address/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true + }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-array-buffer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", @@ -8102,6 +8611,13 @@ "node": ">=0.10.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -8458,6 +8974,86 @@ "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", "dev": true }, + "node_modules/jsdom": { + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-22.1.0.tgz", + "integrity": "sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.6", + "cssstyle": "^3.0.0", + "data-urls": "^4.0.0", + "decimal.js": "^10.4.3", + "domexception": "^4.0.0", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.1", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.4", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.6.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.1.2", + "w3c-xmlserializer": "^4.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^2.0.0", + "whatwg-mimetype": "^3.0.0", + "whatwg-url": "^12.0.1", + "ws": "^8.13.0", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/jsdom/node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-12.0.1.tgz", + "integrity": "sha512-Ed/LrqB8EPlGxjS+TrsXcpUond1mhccS3pchLhzSgPCnTimUCKj3IZE75pAs5m6heB2U2TMerKFUXheyHY+VDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -9780,6 +10376,16 @@ "node": ">=10" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.30.5", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", @@ -9861,6 +10467,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/meow": { "version": "8.1.2", "resolved": "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz", @@ -10961,6 +11577,13 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/nwsapi": { + "version": "2.2.24", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.24.tgz", + "integrity": "sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==", + "dev": true, + "license": "MIT" + }, "node_modules/nx": { "version": "16.10.0", "resolved": "https://registry.npmjs.org/nx/-/nx-16.10.0.tgz", @@ -11131,10 +11754,31 @@ } }, "node_modules/object-inspect": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -11782,6 +12426,19 @@ "parse-path": "^7.0.0" } }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/patch-package": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/patch-package/-/patch-package-8.0.0.tgz", @@ -12328,6 +12985,19 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "dev": true }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -12337,6 +13007,13 @@ "node": ">=6" } }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true, + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -12870,6 +13547,13 @@ "node": ">=0.10.0" } }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -13024,6 +13708,13 @@ "node": ">= 8" } }, + "node_modules/rrweb-cssom": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz", + "integrity": "sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==", + "dev": true, + "license": "MIT" + }, "node_modules/run-async": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", @@ -13123,6 +13814,19 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", @@ -13154,15 +13858,18 @@ "dev": true }, "node_modules/set-function-length": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", - "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dev": true, + "license": "MIT", "dependencies": { - "define-data-property": "^1.1.1", - "get-intrinsic": "^1.2.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -13216,14 +13923,76 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -13674,6 +14443,20 @@ "integrity": "sha512-aFZ19IgVmhdB2uX599ve2kE6BIE3YMnQ6Gp6BURhW/oIzpXGKr878TQfAQZn1+i0Flcc/UKUy1gOlcfaUBCryg==", "dev": true }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -14028,6 +14811,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/tar": { "version": "6.1.11", "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", @@ -14192,6 +14982,32 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -15307,6 +16123,17 @@ "punycode": "^2.1.0" } }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, "node_modules/util": { "version": "0.10.4", "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", @@ -15539,6 +16366,7 @@ "resolved": "https://registry.npmjs.org/vitest/-/vitest-0.34.6.tgz", "integrity": "sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==", "dev": true, + "license": "MIT", "dependencies": { "@types/chai": "^4.3.5", "@types/chai-subset": "^1.3.3", @@ -15638,6 +16466,19 @@ "typescript": "*" } }, + "node_modules/w3c-xmlserializer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-4.0.0.tgz", + "integrity": "sha512-d+BFHzbiCx6zGfz0HyQ6Rg69w9k19nviJspaj4yNscGjrHu94sVP+aRm75yEbCh+r2/yR+7q6hux9LVtbuTGBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/wcwidth": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", @@ -15653,6 +16494,43 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "dev": true }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/whatwg-mimetype": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", + "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -15930,6 +16808,45 @@ "node": ">=6" } }, + "node_modules/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -16073,11 +16990,15 @@ }, "devDependencies": { "@preact/preset-vite": "^2.6.0", + "@testing-library/preact": "^3.2.4", + "@vitest/coverage-v8": "^0.34.6", + "jsdom": "^22.1.0", "preact": "^10.19.1", "rollup-plugin-visualizer": "^5.10.0", "typescript": "^5.2.2", "vite": "^4.5.2", - "vite-plugin-dts": "^3.6.4" + "vite-plugin-dts": "^3.6.4", + "vitest": "^0.34.6" }, "peerDependencies": { "preact": "10.x" @@ -16139,14 +17060,18 @@ "@tiny-intl/core": "^1.2.0" }, "devDependencies": { + "@testing-library/react": "^14.3.1", "@types/react": "^18.0.28", "@vitejs/plugin-react": "^4.1.0", + "@vitest/coverage-v8": "^0.34.6", + "jsdom": "^22.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", "rollup-plugin-visualizer": "^5.9.0", "typescript": "^5.2.2", "vite": "^4.5.2", - "vite-plugin-dts": "3.6.0" + "vite-plugin-dts": "3.6.0", + "vitest": "^0.34.6" }, "peerDependencies": { "react": ">=18.0.0", diff --git a/packages/react/package.json b/packages/react/package.json index 8dc662e..1b54479 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -25,7 +25,9 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", - "preview": "vite preview" + "preview": "vite preview", + "test": "vitest", + "coverage": "vitest run --coverage" }, "peerDependencies": { "react": ">=18.0.0", @@ -35,13 +37,17 @@ "@tiny-intl/core": "^1.2.0" }, "devDependencies": { + "@testing-library/react": "^14.3.1", "@types/react": "^18.0.28", "@vitejs/plugin-react": "^4.1.0", + "@vitest/coverage-v8": "^0.34.6", + "jsdom": "^22.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", "rollup-plugin-visualizer": "^5.9.0", "typescript": "^5.2.2", "vite": "^4.5.2", - "vite-plugin-dts": "3.6.0" + "vite-plugin-dts": "3.6.0", + "vitest": "^0.34.6" } } diff --git a/packages/react/tests/Translate.test.tsx b/packages/react/tests/Translate.test.tsx new file mode 100644 index 0000000..842d25f --- /dev/null +++ b/packages/react/tests/Translate.test.tsx @@ -0,0 +1,112 @@ +import type { TinyIntl, TinyIntlDict } from '@tiny-intl/core'; + +import { cleanup, render, screen } from '@testing-library/react'; +import { createTinyIntl, detectLocale } from '@tiny-intl/core'; +import { afterEach, describe, it } from 'vitest'; + +import { Translate } from '../src/Translate'; +import { TinyIntlContext } from '../src/useIntl'; + +const loadDict = (locale: string): TinyIntlDict => { + if (locale === 'en-US' || locale === 'sv-SE') { + return { + hello: 'Hello, {{name}}!', + inbox: 'Inbox', + document: { + zero: 'Documents', + one: 'Document', + other: 'Documents', + }, + plusXDocumentsSelected: { + zero: '{{title}} and {{count}} more documents selected', + one: '{{title}} and {{count}} more document selected', + other: '{{title}} and {{count}} more documents selected', + }, + }; + } + if (locale === 'de-DE') { + return { + hello: 'Hallo, {{name}}!', + inbox: 'Posteingang', + document: { + zero: 'Dokumente', + one: 'Dokument', + other: 'Dokumente', + }, + plusXDocumentsSelected: { + zero: '{{title}} ausgewählt', + one: '{{title}} und ein weiteres Dokument ausgewählt', + other: '{{title}} und {{count}} weitere Dokumente ausgewählt', + }, + }; + } + return {}; +}; + +async function createIntl() { + const intl = createTinyIntl<'en-US' | 'de-DE' | 'sv-SE'>({ + loadDict: (locale) => loadDict(locale), + fallbackLocale: 'en-US', + supportedLocales: ['en-US', 'de-DE', 'sv-SE'], + detectLocale: (params) => detectLocale('en-US', params), + }); + await intl.mount(); + return intl; +} + +describe('@tiny-intl/react', () => { + let intl: TinyIntl<'en-US' | 'sv-SE' | 'de-DE'>; + + afterEach(cleanup); + + it('renders a plain string translation', async ({ expect }) => { + intl = await createIntl(); + render( + + + , + ); + expect(screen.getByText('Inbox')).toBeTruthy(); + }); + + it('renders the plural form when count is set', async ({ expect }) => { + intl = await createIntl(); + render( + + + , + ); + expect(screen.getByText('Documents')).toBeTruthy(); + }); + + it('renders a formatted number', async ({ expect }) => { + intl = await createIntl(); + render( + + + , + ); + expect(screen.getByText('1,000')).toBeTruthy(); + }); + + it('renders a formatted date', async ({ expect }) => { + intl = await createIntl(); + render( + + + , + ); + expect(screen.getByText('Friday, January 1, 2021')).toBeTruthy(); + }); + + it('passes the value to a function-as-children', async ({ expect }) => { + intl = await createIntl(); + render( + + {(value) => {value}} + , + ); + const el = screen.getByText('Inbox'); + expect(el.tagName).toBe('STRONG'); + }); +}); diff --git a/packages/react/tests/useIntl.test.tsx b/packages/react/tests/useIntl.test.tsx new file mode 100644 index 0000000..5979b73 --- /dev/null +++ b/packages/react/tests/useIntl.test.tsx @@ -0,0 +1,115 @@ +import type { TinyIntl, TinyIntlDict } from '@tiny-intl/core'; + +import { act, cleanup, render, renderHook, screen } from '@testing-library/react'; +import { createTinyIntl, detectLocale } from '@tiny-intl/core'; +import { afterEach, describe, it } from 'vitest'; + +import { TinyIntlContext, useIntl } from '../src/useIntl'; + +const loadDict = (locale: string): TinyIntlDict => { + if (locale === 'en-US' || locale === 'sv-SE') { + return { + hello: 'Hello, {{name}}!', + inbox: 'Inbox', + document: { + zero: 'Documents', + one: 'Document', + other: 'Documents', + }, + plusXDocumentsSelected: { + zero: '{{title}} and {{count}} more documents selected', + one: '{{title}} and {{count}} more document selected', + other: '{{title}} and {{count}} more documents selected', + }, + }; + } + if (locale === 'de-DE') { + return { + hello: 'Hallo, {{name}}!', + inbox: 'Posteingang', + document: { + zero: 'Dokumente', + one: 'Dokument', + other: 'Dokumente', + }, + plusXDocumentsSelected: { + zero: '{{title}} ausgewählt', + one: '{{title}} und ein weiteres Dokument ausgewählt', + other: '{{title}} und {{count}} weitere Dokumente ausgewählt', + }, + }; + } + return {}; +}; + +async function createIntl() { + const intl = createTinyIntl<'en-US' | 'de-DE' | 'sv-SE'>({ + loadDict: (locale) => loadDict(locale), + fallbackLocale: 'en-US', + supportedLocales: ['en-US', 'de-DE', 'sv-SE'], + detectLocale: (params) => detectLocale('en-US', params), + }); + await intl.mount(); + return intl; +} + +describe('@tiny-intl/react', () => { + let intl: TinyIntl<'en-US' | 'sv-SE' | 'de-DE'>; + + afterEach(cleanup); + + it('throws outside a provider', ({ expect }) => { + expect(() => renderHook(() => useIntl())).toThrow( + 'useIntl must be used within a TinyIntlContext.Provider', + ); + }); + + it('t() returns the translated string for the mounted locale', async ({ expect }) => { + intl = await createIntl(); + const { result } = renderHook(() => useIntl(), { + wrapper: ({ children }) => ( + {children} + ), + }); + expect(result.current.t('inbox')).toBe('Inbox'); + }); + + it('re-renders with the German string after intl.change', async ({ expect }) => { + intl = await createIntl(); + + function Inbox() { + const { t } = useIntl(); + return {t('inbox')}; + } + + render( + + + , + ); + + expect(screen.getByText('Inbox')).toBeTruthy(); + + await act(async () => { + await intl.change('de-DE'); + }); + + expect(screen.getByText('Posteingang')).toBeTruthy(); + }); + + it('n() and dt() format per locale', async ({ expect }) => { + intl = await createIntl(); + await act(async () => { + await intl.change('de-DE'); + }); + + const { result } = renderHook(() => useIntl(), { + wrapper: ({ children }) => ( + {children} + ), + }); + + expect(result.current.n(1000)).toBe('1.000'); + expect(result.current.dt('2021-01-01', { dateStyle: 'full' })).toBe('Freitag, 1. Januar 2021'); + }); +}); diff --git a/packages/react/vite.config.ts b/packages/react/vite.config.ts index ba1588c..11aef7a 100644 --- a/packages/react/vite.config.ts +++ b/packages/react/vite.config.ts @@ -2,8 +2,8 @@ import { resolve } from 'path'; import react from '@vitejs/plugin-react'; import { visualizer } from 'rollup-plugin-visualizer'; -import { defineConfig } from 'vite'; import dts from 'vite-plugin-dts'; +import { defineConfig } from 'vitest/config'; // eslint-disable-line import/no-unresolved export default defineConfig({ plugins: [ @@ -46,4 +46,8 @@ export default defineConfig({ esbuild: { jsxInject: `import * as React from 'react'`, }, + test: { + environment: 'jsdom', + globals: false, + }, }); From 29d88d5bc3d3632ba2df931287e28a74afb11d8b Mon Sep 17 00:00:00 2001 From: Phil Date: Wed, 12 Aug 2026 11:58:09 +0200 Subject: [PATCH 07/48] test(preact): add vitest harness and useIntl/Translate tests Same gap as @tiny-intl/react: @tiny-intl/preact had zero automated tests. Its useIntl/Translate implementation is a byte-for-byte copy of the react version (aside from importing from preact/compat), so mirror the react test suite with @testing-library/preact instead. package-lock.json already picked up these devDependencies in the preceding react commit, since both installs ran before that commit. --- packages/preact/package.json | 10 +- packages/preact/tests/Translate.test.tsx | 112 ++++++++++++++++++++++ packages/preact/tests/useIntl.test.tsx | 115 +++++++++++++++++++++++ packages/preact/vite.config.ts | 6 +- 4 files changed, 240 insertions(+), 3 deletions(-) create mode 100644 packages/preact/tests/Translate.test.tsx create mode 100644 packages/preact/tests/useIntl.test.tsx diff --git a/packages/preact/package.json b/packages/preact/package.json index 2f5e1c5..fc86c74 100644 --- a/packages/preact/package.json +++ b/packages/preact/package.json @@ -25,18 +25,24 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", - "preview": "vite preview" + "preview": "vite preview", + "test": "vitest", + "coverage": "vitest run --coverage" }, "peerDependencies": { "preact": "10.x" }, "devDependencies": { "@preact/preset-vite": "^2.6.0", + "@testing-library/preact": "^3.2.4", + "@vitest/coverage-v8": "^0.34.6", + "jsdom": "^22.1.0", "preact": "^10.19.1", "rollup-plugin-visualizer": "^5.10.0", "typescript": "^5.2.2", "vite": "^4.5.2", - "vite-plugin-dts": "^3.6.4" + "vite-plugin-dts": "^3.6.4", + "vitest": "^0.34.6" }, "dependencies": { "@tiny-intl/core": "^1.2.0" diff --git a/packages/preact/tests/Translate.test.tsx b/packages/preact/tests/Translate.test.tsx new file mode 100644 index 0000000..911ed99 --- /dev/null +++ b/packages/preact/tests/Translate.test.tsx @@ -0,0 +1,112 @@ +import type { TinyIntl, TinyIntlDict } from '@tiny-intl/core'; + +import { cleanup, render, screen } from '@testing-library/preact'; +import { createTinyIntl, detectLocale } from '@tiny-intl/core'; +import { afterEach, describe, it } from 'vitest'; + +import { Translate } from '../src/Translate'; +import { TinyIntlContext } from '../src/useIntl'; + +const loadDict = (locale: string): TinyIntlDict => { + if (locale === 'en-US' || locale === 'sv-SE') { + return { + hello: 'Hello, {{name}}!', + inbox: 'Inbox', + document: { + zero: 'Documents', + one: 'Document', + other: 'Documents', + }, + plusXDocumentsSelected: { + zero: '{{title}} and {{count}} more documents selected', + one: '{{title}} and {{count}} more document selected', + other: '{{title}} and {{count}} more documents selected', + }, + }; + } + if (locale === 'de-DE') { + return { + hello: 'Hallo, {{name}}!', + inbox: 'Posteingang', + document: { + zero: 'Dokumente', + one: 'Dokument', + other: 'Dokumente', + }, + plusXDocumentsSelected: { + zero: '{{title}} ausgewählt', + one: '{{title}} und ein weiteres Dokument ausgewählt', + other: '{{title}} und {{count}} weitere Dokumente ausgewählt', + }, + }; + } + return {}; +}; + +async function createIntl() { + const intl = createTinyIntl<'en-US' | 'de-DE' | 'sv-SE'>({ + loadDict: (locale) => loadDict(locale), + fallbackLocale: 'en-US', + supportedLocales: ['en-US', 'de-DE', 'sv-SE'], + detectLocale: (params) => detectLocale('en-US', params), + }); + await intl.mount(); + return intl; +} + +describe('@tiny-intl/preact', () => { + let intl: TinyIntl<'en-US' | 'sv-SE' | 'de-DE'>; + + afterEach(cleanup); + + it('renders a plain string translation', async ({ expect }) => { + intl = await createIntl(); + render( + + + , + ); + expect(screen.getByText('Inbox')).toBeTruthy(); + }); + + it('renders the plural form when count is set', async ({ expect }) => { + intl = await createIntl(); + render( + + + , + ); + expect(screen.getByText('Documents')).toBeTruthy(); + }); + + it('renders a formatted number', async ({ expect }) => { + intl = await createIntl(); + render( + + + , + ); + expect(screen.getByText('1,000')).toBeTruthy(); + }); + + it('renders a formatted date', async ({ expect }) => { + intl = await createIntl(); + render( + + + , + ); + expect(screen.getByText('Friday, January 1, 2021')).toBeTruthy(); + }); + + it('passes the value to a function-as-children', async ({ expect }) => { + intl = await createIntl(); + render( + + {(value) => {value}} + , + ); + const el = screen.getByText('Inbox'); + expect(el.tagName).toBe('STRONG'); + }); +}); diff --git a/packages/preact/tests/useIntl.test.tsx b/packages/preact/tests/useIntl.test.tsx new file mode 100644 index 0000000..ca1716e --- /dev/null +++ b/packages/preact/tests/useIntl.test.tsx @@ -0,0 +1,115 @@ +import type { TinyIntl, TinyIntlDict } from '@tiny-intl/core'; + +import { act, cleanup, render, renderHook, screen } from '@testing-library/preact'; +import { createTinyIntl, detectLocale } from '@tiny-intl/core'; +import { afterEach, describe, it } from 'vitest'; + +import { TinyIntlContext, useIntl } from '../src/useIntl'; + +const loadDict = (locale: string): TinyIntlDict => { + if (locale === 'en-US' || locale === 'sv-SE') { + return { + hello: 'Hello, {{name}}!', + inbox: 'Inbox', + document: { + zero: 'Documents', + one: 'Document', + other: 'Documents', + }, + plusXDocumentsSelected: { + zero: '{{title}} and {{count}} more documents selected', + one: '{{title}} and {{count}} more document selected', + other: '{{title}} and {{count}} more documents selected', + }, + }; + } + if (locale === 'de-DE') { + return { + hello: 'Hallo, {{name}}!', + inbox: 'Posteingang', + document: { + zero: 'Dokumente', + one: 'Dokument', + other: 'Dokumente', + }, + plusXDocumentsSelected: { + zero: '{{title}} ausgewählt', + one: '{{title}} und ein weiteres Dokument ausgewählt', + other: '{{title}} und {{count}} weitere Dokumente ausgewählt', + }, + }; + } + return {}; +}; + +async function createIntl() { + const intl = createTinyIntl<'en-US' | 'de-DE' | 'sv-SE'>({ + loadDict: (locale) => loadDict(locale), + fallbackLocale: 'en-US', + supportedLocales: ['en-US', 'de-DE', 'sv-SE'], + detectLocale: (params) => detectLocale('en-US', params), + }); + await intl.mount(); + return intl; +} + +describe('@tiny-intl/preact', () => { + let intl: TinyIntl<'en-US' | 'sv-SE' | 'de-DE'>; + + afterEach(cleanup); + + it('throws outside a provider', ({ expect }) => { + expect(() => renderHook(() => useIntl())).toThrow( + 'useIntl must be used within a TinyIntlContext.Provider', + ); + }); + + it('t() returns the translated string for the mounted locale', async ({ expect }) => { + intl = await createIntl(); + const { result } = renderHook(() => useIntl(), { + wrapper: ({ children }) => ( + {children} + ), + }); + expect(result.current.t('inbox')).toBe('Inbox'); + }); + + it('re-renders with the German string after intl.change', async ({ expect }) => { + intl = await createIntl(); + + function Inbox() { + const { t } = useIntl(); + return {t('inbox')}; + } + + render( + + + , + ); + + expect(screen.getByText('Inbox')).toBeTruthy(); + + await act(async () => { + await intl.change('de-DE'); + }); + + expect(screen.getByText('Posteingang')).toBeTruthy(); + }); + + it('n() and dt() format per locale', async ({ expect }) => { + intl = await createIntl(); + await act(async () => { + await intl.change('de-DE'); + }); + + const { result } = renderHook(() => useIntl(), { + wrapper: ({ children }) => ( + {children} + ), + }); + + expect(result.current.n(1000)).toBe('1.000'); + expect(result.current.dt('2021-01-01', { dateStyle: 'full' })).toBe('Freitag, 1. Januar 2021'); + }); +}); diff --git a/packages/preact/vite.config.ts b/packages/preact/vite.config.ts index ccf9064..dc0babf 100644 --- a/packages/preact/vite.config.ts +++ b/packages/preact/vite.config.ts @@ -2,8 +2,8 @@ import { resolve } from 'path'; import preact from '@preact/preset-vite'; import { visualizer } from 'rollup-plugin-visualizer'; -import { defineConfig } from 'vite'; import dts from 'vite-plugin-dts'; +import { defineConfig } from 'vitest/config'; // eslint-disable-line import/no-unresolved // https://vitejs.dev/config/ export default defineConfig({ @@ -39,4 +39,8 @@ export default defineConfig({ }, }, }, + test: { + environment: 'jsdom', + globals: false, + }, }); From ac9014288c32c793dfcfee759872f4075301ef41 Mon Sep 17 00:00:00 2001 From: Phil Date: Wed, 12 Aug 2026 11:58:59 +0200 Subject: [PATCH 08/48] chore: add root test and lint:all scripts npm run lint had no target and did nothing outside of lint-staged. Add lint:all (lints the whole repo) and test (runs each workspace's vitest suite via lerna) so there is a single command to verify the whole monorepo, matching what CI will call next. --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index d6c5fe1..af0f1de 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,9 @@ "main": "index.js", "scripts": { "lint": "eslint --config ./.eslintrc.cjs --ignore-path ./.eslintignore --cache", + "lint:all": "npm run lint -- .", + "test": "lerna run test -- --run", + "coverage": "lerna run coverage", "release": "lerna publish --no-private", "git-hooks:commit-msg": "commitlint --edit", "git-hooks:pre-commit": "lint-staged", From a6f686b638a8b05ca9cf95aa027af954895e66d0 Mon Sep 17 00:00:00 2001 From: Phil Date: Wed, 12 Aug 2026 12:00:45 +0200 Subject: [PATCH 09/48] ci: run lint and tests for all packages on supported Node versions The old workflow only ran packages/core's coverage script, so react and preact were never linted, typechecked, built, or tested in CI. It also ran on actions/cache@v2 and actions/setup-node@v2 (retired Node 16 runtime), ordered the cache restore before the Node install it depended on, used npm install instead of npm ci despite a committed lockfile, and tested on Node 16/21, both past end-of-life. Replace it with setup-node@v4's built-in npm cache, npm ci, a Node 20/22/24 matrix, and lint:all + test steps that cover core, react and preact. --- .github/workflows/unit-tests.yml | 48 +++++++++++++++----------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 5174198..67f0020 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -1,38 +1,36 @@ -name: Run unit tests +name: CI on: push: branches: ['*'] pull_request: branches: ['*'] + jobs: - build: - name: 'Run tests' + test: + name: Lint & test (node ${{ matrix.node-version }}) runs-on: ubuntu-latest strategy: + fail-fast: false matrix: - node-version: [16, 18, 20, 21] - # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + node-version: [20, 22, 24] steps: - - name: checkout - uses: actions/checkout@v4 - - name: restore caches - uses: actions/cache@v2 - with: - path: | - ./node_modules - ./.coverage - ./.eslintcache - key: ${{ runner.os }}-${{ matrix.node-version }}-${{ hashFiles('package-lock.json') }} - - name: Run tests - uses: actions/setup-node@v2 - with: - node-version: ${{ matrix.node-version }} - cache: 'npm' - - run: npm install && cd packages/core && npm run coverage - # - name: Publish code coverage - # uses: paambaati/codeclimate-action@v3.0.0 - # env: - # CC_TEST_REPORTER_ID: ${{ secrets.CODE_CLIMATE_REPORTER_ID }} + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + + - name: Install + run: npm ci + + - name: Lint + run: npm run lint:all + + - name: Test + run: npm test From 970254c2d6c10e56ccc09d14e805ad71e4077cc1 Mon Sep 17 00:00:00 2001 From: Phil Date: Wed, 12 Aug 2026 12:11:16 +0200 Subject: [PATCH 10/48] fix(react): render Translate correctly for zero count and number count={0} fell through to the string-translation branch (Intl's `.one` fallback), and number={0}/date={0} rendered nothing, because the branch guards used truthiness checks. Switch to explicit presence checks (typeof === 'number', !== undefined) so zero values take their correct branch, and add `relative` to the missing useCallback dependency array. --- packages/react/src/Translate.tsx | 10 ++--- packages/react/tests/Translate.test.tsx | 53 +++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/packages/react/src/Translate.tsx b/packages/react/src/Translate.tsx index 2312d08..288d7bb 100644 --- a/packages/react/src/Translate.tsx +++ b/packages/react/src/Translate.tsx @@ -63,19 +63,19 @@ export function Translate(props: TranslateProps) { const [changed, setChanged] = useState(0); const translateFn = useCallback(() => { - if (count) { + if (typeof count === 'number') { return tc(name, count, options); } - if (date && !relative) { + if (date !== undefined && !relative) { return dt(date, options); } - if (date && relative) { + if (date !== undefined && relative) { return rt(date, options); } - if (number) { + if (typeof number === 'number') { return n(number, options); } @@ -84,7 +84,7 @@ export function Translate(props: TranslateProps) { } return null; - }, [changed, count, date, name, number, options]); + }, [changed, count, date, name, number, options, relative]); useEffect(() => { const dispose = subscribe(() => { diff --git a/packages/react/tests/Translate.test.tsx b/packages/react/tests/Translate.test.tsx index 842d25f..866e14b 100644 --- a/packages/react/tests/Translate.test.tsx +++ b/packages/react/tests/Translate.test.tsx @@ -109,4 +109,57 @@ describe('@tiny-intl/react', () => { const el = screen.getByText('Inbox'); expect(el.tagName).toBe('STRONG'); }); + + it('renders the zero plural form for count={0}', async ({ expect }) => { + intl = await createIntl(); + await intl.change('de-DE'); + render( + + + , + ); + expect(screen.getByText('Dokumente')).toBeTruthy(); + }); + + it('renders the singular plural form for count={1}', async ({ expect }) => { + intl = await createIntl(); + await intl.change('de-DE'); + render( + + + , + ); + expect(screen.getByText('Dokument')).toBeTruthy(); + }); + + it('renders the plural form for count={5}', async ({ expect }) => { + intl = await createIntl(); + await intl.change('de-DE'); + render( + + + , + ); + expect(screen.getByText('Dokumente')).toBeTruthy(); + }); + + it('renders a formatted zero for number={0}', async ({ expect }) => { + intl = await createIntl(); + render( + + + , + ); + expect(screen.getByText('0')).toBeTruthy(); + }); + + it('renders a formatted date for the epoch date={0}', async ({ expect }) => { + intl = await createIntl(); + render( + + + , + ); + expect(screen.getByText('Thursday, January 1, 1970')).toBeTruthy(); + }); }); From 0f2a90168d36682bed47bde25368307008cbc406 Mon Sep 17 00:00:00 2001 From: Phil Date: Wed, 12 Aug 2026 12:11:28 +0200 Subject: [PATCH 11/48] fix(preact): render Translate correctly for zero count and number count={0} fell through to the string-translation branch (Intl's `.one` fallback), and number={0}/date={0} rendered nothing, because the branch guards used truthiness checks. Switch to explicit presence checks (typeof === 'number', !== undefined) so zero values take their correct branch, and add `relative` to the missing useCallback dependency array. --- packages/preact/src/Translate.tsx | 10 ++--- packages/preact/tests/Translate.test.tsx | 53 ++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/packages/preact/src/Translate.tsx b/packages/preact/src/Translate.tsx index edc58b9..a54120b 100644 --- a/packages/preact/src/Translate.tsx +++ b/packages/preact/src/Translate.tsx @@ -63,19 +63,19 @@ export function Translate(props: TranslateProps) { const [changed, setChanged] = useState(0); const translateFn = useCallback(() => { - if (count) { + if (typeof count === 'number') { return tc(name, count, options); } - if (date && !relative) { + if (date !== undefined && !relative) { return dt(date, options); } - if (date && relative) { + if (date !== undefined && relative) { return rt(date, options); } - if (number) { + if (typeof number === 'number') { return n(number, options); } @@ -84,7 +84,7 @@ export function Translate(props: TranslateProps) { } return null; - }, [changed, count, date, name, number, options]); + }, [changed, count, date, name, number, options, relative]); useEffect(() => { const dispose = subscribe(() => { diff --git a/packages/preact/tests/Translate.test.tsx b/packages/preact/tests/Translate.test.tsx index 911ed99..eca5657 100644 --- a/packages/preact/tests/Translate.test.tsx +++ b/packages/preact/tests/Translate.test.tsx @@ -109,4 +109,57 @@ describe('@tiny-intl/preact', () => { const el = screen.getByText('Inbox'); expect(el.tagName).toBe('STRONG'); }); + + it('renders the zero plural form for count={0}', async ({ expect }) => { + intl = await createIntl(); + await intl.change('de-DE'); + render( + + + , + ); + expect(screen.getByText('Dokumente')).toBeTruthy(); + }); + + it('renders the singular plural form for count={1}', async ({ expect }) => { + intl = await createIntl(); + await intl.change('de-DE'); + render( + + + , + ); + expect(screen.getByText('Dokument')).toBeTruthy(); + }); + + it('renders the plural form for count={5}', async ({ expect }) => { + intl = await createIntl(); + await intl.change('de-DE'); + render( + + + , + ); + expect(screen.getByText('Dokumente')).toBeTruthy(); + }); + + it('renders a formatted zero for number={0}', async ({ expect }) => { + intl = await createIntl(); + render( + + + , + ); + expect(screen.getByText('0')).toBeTruthy(); + }); + + it('renders a formatted date for the epoch date={0}', async ({ expect }) => { + intl = await createIntl(); + render( + + + , + ); + expect(screen.getByText('Thursday, January 1, 1970')).toBeTruthy(); + }); }); From ad1e1abe6eebd634e0ea8b33c38eb413fab4c293 Mon Sep 17 00:00:00 2001 From: Phil Date: Wed, 12 Aug 2026 12:22:56 +0200 Subject: [PATCH 12/48] fix(core): point exports types at the emitted declaration files The "." and "./utils" exports conditions declared a types path (lib/esm/types/index.d.ts) that the build never produced, and listed "types" after "import"/"require" where resolvers never reach it since they take the first matching condition. Nest types first under each of import/require, pointing ESM at the emitted .d.ts and CJS at the emitted .d.cts, add an explicit ./package.json export, and mark the package side-effect free. --- packages/core/package.json | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index 5074875..672ed40 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -16,17 +16,29 @@ "main": "./lib/cjs/index.js", "module": "./lib/esm/index.js", "types": "./lib/esm/index.d.ts", + "sideEffects": false, "exports": { ".": { - "import": "./lib/esm/index.js", - "require": "./lib/cjs/index.js", - "types": "./lib/esm/types/index.d.ts" + "import": { + "types": "./lib/esm/index.d.ts", + "default": "./lib/esm/index.js" + }, + "require": { + "types": "./lib/cjs/index.d.cts", + "default": "./lib/cjs/index.js" + } }, "./utils": { - "import": "./lib/esm/utils/index.js", - "require": "./lib/cjs/utils/index.js", - "types": "./lib/esm/utils/index.d.ts" - } + "import": { + "types": "./lib/esm/utils/index.d.ts", + "default": "./lib/esm/utils/index.js" + }, + "require": { + "types": "./lib/cjs/utils/index.d.cts", + "default": "./lib/cjs/utils/index.js" + } + }, + "./package.json": "./package.json" }, "scripts": { "dev": "vite", From 48c2441994c1d22b541e00926731f99af84589c9 Mon Sep 17 00:00:00 2001 From: Phil Date: Wed, 12 Aug 2026 12:23:06 +0200 Subject: [PATCH 13/48] fix(core): exclude vite-env.d.ts from the tsup entry glob entry: ['src'] swept up src/vite-env.d.ts as a build entry, publishing junk lib/esm/vite-env.d.js and lib/cjs/vite-env.d.d.cts files to npm. Narrow the glob to *.ts files excluding *.d.ts so only real source modules are built. --- packages/core/tsup.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/tsup.config.ts b/packages/core/tsup.config.ts index ba1f068..ec37612 100644 --- a/packages/core/tsup.config.ts +++ b/packages/core/tsup.config.ts @@ -20,7 +20,7 @@ const addPackageJson = () => { }; export default defineConfig({ - entry: ['src'], + entry: ['src/**/*.ts', '!src/**/*.d.ts'], target: 'es2022', format, minifyIdentifiers: false, From 25e3210b1cf260c22dd0a1f210d2feaf03145cab Mon Sep 17 00:00:00 2001 From: Phil Date: Wed, 12 Aug 2026 12:23:28 +0200 Subject: [PATCH 14/48] chore(core): drop the unused vite library build npm run build ran vite build (emitting to dist/) before tsup (emitting to lib/), but files only ships lib and src, so everything vite produced was thrown away on every build. Collapse the build script to just the two tsup passes, trim vite.config.ts down to the vitest config it also carries, and drop the two devDependencies (rollup-plugin-visualizer, vite-plugin-dts) that existed solely to feed the dead build. vite itself stays, since vitest depends on it and dev/preview still use it. --- package-lock.json | 2 -- packages/core/package.json | 7 +++---- packages/core/vite.config.ts | 33 --------------------------------- 3 files changed, 3 insertions(+), 39 deletions(-) diff --git a/package-lock.json b/package-lock.json index 97a3ccf..b148005 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16973,11 +16973,9 @@ "license": "MIT", "devDependencies": { "@vitest/coverage-v8": "^0.34.6", - "rollup-plugin-visualizer": "^5.9.2", "tsup": "^8.0.1", "typescript": "^5.2.2", "vite": "^4.5.2", - "vite-plugin-dts": "3.6.0", "vitest": "^0.34.6" } }, diff --git a/packages/core/package.json b/packages/core/package.json index 672ed40..fd04216 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -42,19 +42,18 @@ }, "scripts": { "dev": "vite", - "build": "vite build && npm run build2", - "build2": "FORMAT=esm tsup && FORMAT=cjs tsup", + "build": "npm run build:esm && npm run build:cjs", + "build:esm": "FORMAT=esm tsup", + "build:cjs": "FORMAT=cjs tsup", "preview": "vite preview", "test": "vitest", "coverage": "vitest run --coverage" }, "devDependencies": { "@vitest/coverage-v8": "^0.34.6", - "rollup-plugin-visualizer": "^5.9.2", "tsup": "^8.0.1", "typescript": "^5.2.2", "vite": "^4.5.2", - "vite-plugin-dts": "3.6.0", "vitest": "^0.34.6" } } diff --git a/packages/core/vite.config.ts b/packages/core/vite.config.ts index 6b264b8..ef23c32 100644 --- a/packages/core/vite.config.ts +++ b/packages/core/vite.config.ts @@ -1,38 +1,5 @@ -import { resolve } from 'path'; - -import { visualizer } from 'rollup-plugin-visualizer'; -import dts from 'vite-plugin-dts'; import { defineConfig } from 'vitest/config'; // eslint-disable-line import/no-unresolved export default defineConfig({ - plugins: [ - dts({ - outDir: resolve(__dirname, 'dist', 'types'), - insertTypesEntry: true, - }), - visualizer({ - filename: 'dist/stats.html', - gzipSize: true, - brotliSize: true, - }), - ], - build: { - outDir: resolve(__dirname, 'dist'), - minify: false, - lib: { - // Could also be a dictionary or array of multiple entry points - entry: resolve(__dirname, 'src', 'index.ts'), - name: 'tinyIntl', - // the proper extensions will be added - fileName: 'index', - formats: ['es', 'cjs', 'umd'], - }, - rollupOptions: { - external: [], - output: { - globals: {}, - }, - }, - }, test: {}, }); From 5deaf5958b5d200d34f9206f6ca063c3ad5227fa Mon Sep 17 00:00:00 2001 From: Phil Date: Wed, 12 Aug 2026 12:23:36 +0200 Subject: [PATCH 15/48] fix(react): order exports conditions with types first "types" was listed last in the "." export condition, where resolvers that take the first matching condition would never reach it. Move it first, add an explicit ./package.json export, and mark the package side-effect free. --- packages/react/package.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/react/package.json b/packages/react/package.json index 1b54479..c3a45c5 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -15,12 +15,14 @@ "main": "./lib/index.umd.cjs", "module": "./lib/index.js", "types": "./lib/types/index.d.ts", + "sideEffects": false, "exports": { ".": { + "types": "./lib/types/index.d.ts", "import": "./lib/index.js", - "require": "./lib/index.umd.cjs", - "types": "./lib/types/index.d.ts" - } + "require": "./lib/index.umd.cjs" + }, + "./package.json": "./package.json" }, "scripts": { "dev": "vite", From 3a26fc1b3b8a559d9d9be7fc232526aa4565faca Mon Sep 17 00:00:00 2001 From: Phil Date: Wed, 12 Aug 2026 12:23:41 +0200 Subject: [PATCH 16/48] fix(preact): order exports conditions with types first Same fix as the react adapter: "types" was listed last in the "." export condition, where resolvers that take the first matching condition would never reach it. Move it first, add an explicit ./package.json export, and mark the package side-effect free. --- packages/preact/package.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/preact/package.json b/packages/preact/package.json index fc86c74..48ac682 100644 --- a/packages/preact/package.json +++ b/packages/preact/package.json @@ -15,12 +15,14 @@ "main": "./lib/index.umd.cjs", "module": "./lib/index.js", "types": "./lib/types/index.d.ts", + "sideEffects": false, "exports": { ".": { + "types": "./lib/types/index.d.ts", "import": "./lib/index.js", - "require": "./lib/index.umd.cjs", - "types": "./lib/types/index.d.ts" - } + "require": "./lib/index.umd.cjs" + }, + "./package.json": "./package.json" }, "scripts": { "dev": "vite", From 49bd556424e2b21ec2a75099a6906275e541d23a Mon Sep 17 00:00:00 2001 From: Phil Date: Wed, 12 Aug 2026 12:32:36 +0200 Subject: [PATCH 17/48] fix(preact): emit type declarations for useIntl Annotate useIntl's return type explicitly as TinyIntl. The inferred return type triggered TS2742 (portability diagnostic) because it referenced TinyIntl through a deep node_modules path, causing vite-plugin-dts to skip emitting useIntl.d.ts entirely. The package's type entry point (index.d.ts) re-exported that missing module, leaving every TypeScript consumer of @tiny-intl/preact without types for useIntl, the package's primary API. --- packages/preact/src/useIntl.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/preact/src/useIntl.tsx b/packages/preact/src/useIntl.tsx index 43e9afa..df0f982 100644 --- a/packages/preact/src/useIntl.tsx +++ b/packages/preact/src/useIntl.tsx @@ -5,7 +5,7 @@ import { createContext, useCallback, useContext, useEffect, useState } from 'pre export const TinyIntlContext = createContext | undefined>(undefined); -export function useIntl() { +export function useIntl(): TinyIntl { const intl = useContext(TinyIntlContext); if (!intl) { From c03760dcf0d46df2cb13f4a8bfd2b41bc4da1106 Mon Sep 17 00:00:00 2001 From: Phil Date: Wed, 12 Aug 2026 14:29:27 +0200 Subject: [PATCH 18/48] perf(react): shrink Translate guards typeof count === 'number' and its siblings cost more bytes than the equivalent != null check after minification, while still treating 0 as a value (the fix from the earlier count={0}/number={0} bug). --- packages/react/src/Translate.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/react/src/Translate.tsx b/packages/react/src/Translate.tsx index 288d7bb..65c60e5 100644 --- a/packages/react/src/Translate.tsx +++ b/packages/react/src/Translate.tsx @@ -63,19 +63,19 @@ export function Translate(props: TranslateProps) { const [changed, setChanged] = useState(0); const translateFn = useCallback(() => { - if (typeof count === 'number') { + if (count != null) { return tc(name, count, options); } - if (date !== undefined && !relative) { + if (date != null && !relative) { return dt(date, options); } - if (date !== undefined && relative) { + if (date != null && relative) { return rt(date, options); } - if (typeof number === 'number') { + if (number != null) { return n(number, options); } From 308c2702ec0844ff7ce0e04d35239fe4c9a14177 Mon Sep 17 00:00:00 2001 From: Phil Date: Wed, 12 Aug 2026 14:29:38 +0200 Subject: [PATCH 19/48] perf(preact): shrink Translate guards Same guard shrink as the react adapter: typeof count === 'number' and its siblings become != null, which minifies smaller while still treating 0 as a value. --- packages/preact/src/Translate.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/preact/src/Translate.tsx b/packages/preact/src/Translate.tsx index a54120b..bf3766b 100644 --- a/packages/preact/src/Translate.tsx +++ b/packages/preact/src/Translate.tsx @@ -63,19 +63,19 @@ export function Translate(props: TranslateProps) { const [changed, setChanged] = useState(0); const translateFn = useCallback(() => { - if (typeof count === 'number') { + if (count != null) { return tc(name, count, options); } - if (date !== undefined && !relative) { + if (date != null && !relative) { return dt(date, options); } - if (date !== undefined && relative) { + if (date != null && relative) { return rt(date, options); } - if (typeof number === 'number') { + if (number != null) { return n(number, options); } From a616fb5497570174e8b4bb56b8915a7bdcf64f25 Mon Sep 17 00:00:00 2001 From: Phil Date: Wed, 12 Aug 2026 14:29:47 +0200 Subject: [PATCH 20/48] fix(react): point require at the CJS build Drop the unused UMD format: package.json referenced only ./lib/index.js (import) and ./lib/index.umd.cjs (require), so the plain CJS output was built but never shipped to consumers, and require pointed at the larger of the two CommonJS-capable bundles. Build only es/cjs and point main and the require export condition at lib/index.cjs. --- packages/react/package.json | 4 ++-- packages/react/vite.config.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/react/package.json b/packages/react/package.json index c3a45c5..9e6473c 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -12,7 +12,7 @@ "files": [ "lib" ], - "main": "./lib/index.umd.cjs", + "main": "./lib/index.cjs", "module": "./lib/index.js", "types": "./lib/types/index.d.ts", "sideEffects": false, @@ -20,7 +20,7 @@ ".": { "types": "./lib/types/index.d.ts", "import": "./lib/index.js", - "require": "./lib/index.umd.cjs" + "require": "./lib/index.cjs" }, "./package.json": "./package.json" }, diff --git a/packages/react/vite.config.ts b/packages/react/vite.config.ts index 11aef7a..04b7c7b 100644 --- a/packages/react/vite.config.ts +++ b/packages/react/vite.config.ts @@ -30,7 +30,7 @@ export default defineConfig({ name: 'tinyIntl', // the proper extensions will be added fileName: 'index', - formats: ['es', 'cjs', 'umd'], + formats: ['es', 'cjs'], }, rollupOptions: { external: ['react', 'react-dom', 'react-jsx-runtime'], From 98ffdf0e36a2c509c842cafd13d8044227e5ba4f Mon Sep 17 00:00:00 2001 From: Phil Date: Wed, 12 Aug 2026 14:29:55 +0200 Subject: [PATCH 21/48] fix(preact): point require at the CJS build Same fix as the react adapter: drop the unused UMD format and point main and the require export condition at lib/index.cjs instead of the larger lib/index.umd.cjs. --- packages/preact/package.json | 4 ++-- packages/preact/vite.config.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/preact/package.json b/packages/preact/package.json index 48ac682..9a12c93 100644 --- a/packages/preact/package.json +++ b/packages/preact/package.json @@ -12,7 +12,7 @@ "files": [ "lib" ], - "main": "./lib/index.umd.cjs", + "main": "./lib/index.cjs", "module": "./lib/index.js", "types": "./lib/types/index.d.ts", "sideEffects": false, @@ -20,7 +20,7 @@ ".": { "types": "./lib/types/index.d.ts", "import": "./lib/index.js", - "require": "./lib/index.umd.cjs" + "require": "./lib/index.cjs" }, "./package.json": "./package.json" }, diff --git a/packages/preact/vite.config.ts b/packages/preact/vite.config.ts index dc0babf..0d2f4b4 100644 --- a/packages/preact/vite.config.ts +++ b/packages/preact/vite.config.ts @@ -27,7 +27,7 @@ export default defineConfig({ name: 'tinyIntl', // the proper extensions will be added fileName: 'index', - formats: ['es', 'cjs', 'umd'], + formats: ['es', 'cjs'], }, rollupOptions: { external: ['preact', 'preact/compat'], From f5abd18acc602b20c4d58fba322b395fec005ecd Mon Sep 17 00:00:00 2001 From: Phil Date: Wed, 12 Aug 2026 16:25:32 +0200 Subject: [PATCH 22/48] docs: add advisor audit record and implementation plans MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds plans/ — the audit findings, seven implementation plans, and the review record for each. All seven plans are executed and merged as of 98ffdf0; the deferred findings and open questions in plans/README.md are the remaining backlog. Co-Authored-By: Claude Opus 5 --- plans/001-adapter-test-and-ci-baseline.md | 563 +++++++++++++++ plans/002-remove-solid-js-support.md | 499 +++++++++++++ plans/003-fix-translate-falsy-props.md | 452 ++++++++++++ plans/004-fix-dead-intl-formatter-caches.md | 448 ++++++++++++ plans/005-fix-core-package-exports.md | 552 +++++++++++++++ plans/006-fix-preact-missing-useintl-types.md | 342 +++++++++ plans/007-recover-bundle-size-regression.md | 390 ++++++++++ plans/README.md | 670 ++++++++++++++++++ 8 files changed, 3916 insertions(+) create mode 100644 plans/001-adapter-test-and-ci-baseline.md create mode 100644 plans/002-remove-solid-js-support.md create mode 100644 plans/003-fix-translate-falsy-props.md create mode 100644 plans/004-fix-dead-intl-formatter-caches.md create mode 100644 plans/005-fix-core-package-exports.md create mode 100644 plans/006-fix-preact-missing-useintl-types.md create mode 100644 plans/007-recover-bundle-size-regression.md create mode 100644 plans/README.md diff --git a/plans/001-adapter-test-and-ci-baseline.md b/plans/001-adapter-test-and-ci-baseline.md new file mode 100644 index 0000000..c18ddae --- /dev/null +++ b/plans/001-adapter-test-and-ci-baseline.md @@ -0,0 +1,563 @@ +# Plan 001: Establish a working verification baseline — tests for the react and preact adapters, and a CI pipeline that runs them + +> **Executor instructions**: Follow this plan step by step. Run every +> verification command and confirm the expected result before moving to the +> next step. If anything in the "STOP conditions" section occurs, stop and +> report — do not improvise. When done, update the status row for this plan +> in `plans/README.md`. +> +> **Drift check (run first)**: +> `git diff --stat 794eb48..HEAD -- packages/react packages/preact .github package.json` +> If any in-scope file changed since this plan was written, compare the +> "Current state" excerpts against the live code before proceeding; on a +> mismatch, treat it as a STOP condition. + +## Status + +- **Priority**: P1 +- **Effort**: M +- **Risk**: LOW (adds test/CI infrastructure only; no runtime source changes) +- **Depends on**: none. Assumes `plans/002-remove-solid-js-support.md` lands — + see "A note on `packages/solid-js`" below. +- **Category**: tests +- **Planned at**: commit `794eb48`, 2026-08-12 + +## Why this matters + +This repo publishes framework adapters to npm, and not one of them has a single +automated test. Only `@tiny-intl/core` has a suite, and CI runs only that one +package — it never lints, never typechecks, and never builds the adapters. + +That gap has already shipped a broken package. The Solid adapter's `useIntl()` +returned `'[undefined]'` from every primitive it exposed +(`packages/solid-js/src/useIntl.tsx:22` called `fn.call(args)`, which passes the +argument array as `this` and forwards no arguments) — for its entire published +life. One render test would have caught it. That package is being deleted rather +than fixed (plan 002), but react and preact are built the same way, by the same +copy-paste, and have the same amount of coverage: none. + +After this plan lands, `npm test` at the repo root runs a real suite for all +three remaining packages, CI runs lint + tests on every push, and plan 003 has a +verification gate to prove its fix works and stays fixed. + +### A note on `packages/solid-js` + +`plans/002-remove-solid-js-support.md` deletes that package. This plan therefore +covers **react and preact only** and never touches `packages/solid-js`, whether +or not 002 has run yet. There is no ordering constraint between the two plans. + +If plan 002 is later rejected, `packages/solid-js` will be left with no test +harness and this plan must be re-scoped to add one — budget extra time for it, +because getting `vite-plugin-solid` to cooperate with vitest 0.34 is +substantially harder than the react/preact setup below. + +## Current state + +### Files and their roles + +- `package.json` — root workspace manifest (npm workspaces + lerna). Has a `lint` + script with **no target argument**, and no `test` script at all. +- `.github/workflows/unit-tests.yml` — the only CI workflow. Runs core's coverage + script and nothing else, using deprecated action versions. +- `packages/core/vite.config.ts` — the only package with a vitest config + (`test: {}` at the bottom, via `defineConfig` from `vitest/config`). +- `packages/core/tests/index.test.ts` — the only test file in the repo; **use it + as the structural pattern** for all new tests. +- `packages/react/package.json`, `packages/preact/package.json` — no `test` + script, no test runner in `devDependencies`. +- `packages/react/vite.config.ts`, `packages/preact/vite.config.ts` — build-only + configs; `defineConfig` is imported from `vite`, not `vitest/config`, and there + is no `test` key. + +### Excerpt — root `package.json:5-13` (the lint script has no target) + +```json + "scripts": { + "lint": "eslint --config ./.eslintrc.cjs --ignore-path ./.eslintignore --cache", + "release": "lerna publish --no-private", + "git-hooks:commit-msg": "commitlint --edit", + "git-hooks:pre-commit": "lint-staged", + "preversion": "lerna run coverage && lerna run build", + "prepare": "husky install" + }, +``` + +`npm run lint` with no file arguments lints nothing. It only ever does work via +`.lintstagedrc`, which appends staged filenames: + +```json +{ + "packages/**/*.{ts,tsx,js,jsx}": "npm run lint" +} +``` + +### Excerpt — `.github/workflows/unit-tests.yml` (entire file) + +```yaml +name: Run unit tests + +on: + push: + branches: ['*'] + pull_request: + branches: ['*'] +jobs: + build: + name: 'Run tests' + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [16, 18, 20, 21] + + steps: + - name: checkout + uses: actions/checkout@v4 + - name: restore caches + uses: actions/cache@v2 + with: + path: | + ./node_modules + ./.coverage + ./.eslintcache + key: ${{ runner.os }}-${{ matrix.node-version }}-${{ hashFiles('package-lock.json') }} + - name: Run tests + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + - run: npm install && cd packages/core && npm run coverage +``` + +Problems, all of which this plan fixes: `actions/cache@v2` and +`actions/setup-node@v2` run on the retired Node 16 action runtime; the cache step +runs *before* `setup-node` and duplicates `setup-node`'s own `cache: 'npm'`; +`npm install` is used despite a committed `package-lock.json`; the matrix +includes Node 16 and 21, both long past end-of-life; the step labelled +`Run tests` is actually the Node setup step; and only `packages/core` is tested. + +### Excerpt — `packages/core/vite.config.ts` (the vitest config pattern to copy) + +```ts +import { defineConfig } from 'vitest/config'; // eslint-disable-line import/no-unresolved + +export default defineConfig({ + plugins: [ /* dts, visualizer */ ], + build: { /* ... */ }, + test: {}, +}); +``` + +### Excerpt — `packages/core/tests/index.test.ts:1-6, 44-62` (the test style to match) + +```ts +import type { TinyIntl, TinyIntlDict } from '../src'; + +import { vi, afterEach, describe, it } from 'vitest'; + +import { createTinyIntl, detectBrowserLocale, detectLocale } from '../src'; +import { relativeTimeFormatForDiff } from '../src/utils'; + +describe('@tiny-intl/core', () => { + let intl: TinyIntl<'en-US' | 'sv-SE' | 'de-DE'>; + + afterEach(async () => { + await intl.change('en-US'); + }); + + it('mount instance', async ({ expect }) => { + intl = createTinyIntl<'en-US' | 'de-DE' | 'sv-SE'>({ + loadDict: (locale) => loadDict(locale), + fallbackLocale: 'en-US', + supportedLocales: ['en-US', 'de-DE', 'sv-SE'], + detectLocale: (params) => detectLocale('en-US', params), + }); + await intl.mount(); + expect(intl.locale).toBe('en-US'); + }); +``` + +**Conventions you must match**, all visible above: + +- Tests live in a `tests/` directory beside `src/`, **not** co-located with + source files. Import from `'../src'`, not from `'../lib'` or the package name. +- `expect` is taken from the **per-test fixture argument** + (`it('name', async ({ expect }) => {...})`), not imported from `vitest`. + Keep doing that. +- Only `vi`, `afterEach`, `describe`, `it` are imported from `vitest`. +- Single top-level `describe` named after the package (`'@tiny-intl/core'` → + use `'@tiny-intl/react'` etc.). +- Formatting is Prettier with `singleQuote: true`, `printWidth: 100`, + `trailingComma: "all"` (see `.prettierrc`). 2-space indent (see `.editorconfig`). + +### Excerpt — `packages/react/package.json:25-46` (no test script, no runner) + +```json + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + }, + "dependencies": { + "@tiny-intl/core": "^1.2.0" + }, + "devDependencies": { + "@types/react": "^18.0.28", + "@vitejs/plugin-react": "^4.1.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "rollup-plugin-visualizer": "^5.9.0", + "typescript": "^5.2.2", + "vite": "^4.5.2", + "vite-plugin-dts": "3.6.0" + } +``` + +`packages/preact/package.json` has the same shape with `preact` in place of react. + +### Version constraint you must respect + +The repo is on **vitest `^0.34.6`** and **vite `^4.5.2`** (see +`packages/core/package.json:37-46`). Do **not** upgrade either in this plan — +a vitest 0→3 and vite 4→7 migration is a separate, larger piece of work +(recorded in `plans/README.md` under deferred findings). Install testing +libraries whose versions are compatible with vitest 0.34 / vite 4. + +## Commands you will need + +> These were read from `package.json` / CI config during planning. `node_modules` +> was **not** installed at planning time, so none of them were executed — expect +> to discover the real behaviour of `npm run build` in Step 1. + +| Purpose | Command | Expected on success | +|---|---|---| +| Install (root, workspaces) | `npm install` | exit 0 | +| Core tests | `npm run test --workspace @tiny-intl/core -- --run` | all pass | +| Core coverage | `npm run coverage --workspace @tiny-intl/core` | exit 0 | +| Typecheck+build one adapter | `npm run build --workspace @tiny-intl/react` | exit 0 | +| Lint (after Step 4) | `npm run lint:all` | exit 0 | +| All tests (after Step 4) | `npm test` | all pass | + +## Scope + +**In scope** (the only files you should modify or create): + +- `package.json` (root) — add `test` / `lint:all` scripts +- `.github/workflows/unit-tests.yml` — rewrite +- `packages/react/package.json`, `packages/react/vite.config.ts`, + `packages/react/tests/useIntl.test.tsx` (create), + `packages/react/tests/Translate.test.tsx` (create) +- `packages/preact/package.json`, `packages/preact/vite.config.ts`, + `packages/preact/tests/useIntl.test.tsx` (create), + `packages/preact/tests/Translate.test.tsx` (create) +- `package-lock.json` — will change as a result of installing devDependencies; + commit the result. +- `plans/README.md` — status row only + +**Out of scope** (do NOT touch, even though they look related): + +- **`packages/solid-js/**` — do not add tests, a `test` script, or a vitest + config there.** That package is being deleted by + `plans/002-remove-solid-js-support.md`. If the directory still exists when you + run, simply leave it alone. +- **Any file under `packages/react/src/` or `packages/preact/src/`.** This plan + adds tests that document *current* behaviour. Fixing behaviour is plan 003. +- `packages/core/**` — core already has a working suite; do not restructure it. +- Upgrading vitest, vite, eslint, prettier, lerna or typescript major versions. +- `.eslintrc.cjs` — unless a new test file trips a lint rule, in which case see + Step 4. + +## Git workflow + +- Branch: `advisor/001-adapter-test-and-ci-baseline` +- The repo enforces **conventional commits** via commitlint + (`commitlint.config.js` + `.husky/commit-msg`), with scopes restricted to + `core`, `react`, `solid-js`, `preact` (plan 002 removes `solid-js` from that + list). Recent history for reference: + `test(core): use stubs for mocking navigator variable`, + `ci: implement unit testing inside github action`, + `chore: updated outdated dev dependencies`. + Use `test(react): ...`, `test(preact): ...`, `ci: ...`, `chore: ...`. +- Commit per step or per logical unit. Do NOT push or open a PR unless the + operator instructed it. + +## Steps + +### Step 1: Record the current build/typecheck baseline before changing anything + +Install and find out what actually works today. Each adapter's `build` script is +`tsc && vite build`, so it doubles as a typecheck. + +```bash +npm install +npm run build --workspace @tiny-intl/react +npm run build --workspace @tiny-intl/preact +npm run test --workspace @tiny-intl/core -- --run +``` + +Write the exit status and, on failure, the **first 30 lines of error output** for +each of these four commands into your final report. + +**Known baseline, measured 2026-08-12 — do not act on any of it, just confirm +it still holds:** + +- All four commands exited 0. `npx tsc --noEmit` also exits 0 in both + `packages/react` and `packages/preact`. There is no pre-existing type error. +- The **preact** build prints 6 non-fatal `TS2742` diagnostics from + `vite-plugin-dts` ("The inferred type of 'useIntl' cannot be named without a + reference to …/createTinyIntl. This is likely not portable."). The build still + exits 0. As a result `packages/preact/lib/types/useIntl.d.ts` is **never + emitted**, even though `lib/types/index.d.ts` re-exports from it. React emits + all three declaration files correctly. + +That preact declaration gap is a real shipped bug, but it is **out of scope +here** — it has its own plan (006). If you observe it, note it and move on. If +you observe something *different* from the baseline above, record the exact +output; the repo has drifted. + +**Verify**: all four commands have been run and their results recorded. This step +cannot "fail" — it is a measurement. + +### Step 2: Add the test harness to `@tiny-intl/react` + +2a. Add devDependencies and a `test` script to `packages/react/package.json`: + +```json + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "test": "vitest", + "coverage": "vitest run --coverage" + }, +``` + +Install (from the repo root, so npm workspaces hoists correctly): + +```bash +npm install -D --workspace @tiny-intl/react \ + vitest@^0.34.6 @vitest/coverage-v8@^0.34.6 jsdom@^22 \ + @testing-library/react@^14 +``` + +2b. Give the package a vitest config. `packages/react/vite.config.ts` currently +imports `defineConfig` from `'vite'`. Change that import to `'vitest/config'` and +add a `test` block — matching how `packages/core/vite.config.ts` does it. Leave +every existing `plugins` and `build` entry exactly as it is: + +```ts +import { defineConfig } from 'vitest/config'; // eslint-disable-line import/no-unresolved + +export default defineConfig({ + plugins: [ /* unchanged */ ], + build: { /* unchanged */ }, + test: { + environment: 'jsdom', + globals: false, + }, +}); +``` + +2c. Create `packages/react/tests/useIntl.test.tsx`. It must cover, at minimum: + +1. `useIntl()` outside a provider throws + `'useIntl must be used within a TinyIntlContext.Provider'`. +2. `t('inbox')` returns the translated string for the mounted locale. +3. After `await intl.change('de-DE')`, a component using `useIntl()` **re-renders + with the German string** (this is the subscription path, + `packages/react/src/useIntl.tsx:26-31`). +4. `n(1000)` and `dt('2021-01-01', { dateStyle: 'full' })` return locale-correct + output. + +Build the `intl` instance with `createTinyIntl` from `@tiny-intl/core` and an +inline `loadDict`, exactly as `packages/core/tests/index.test.ts:8-42` does — +copy that fixture dictionary rather than inventing a new one, so all three +packages' tests share vocabulary. Wrap components in +``. + +Locale change happens outside React, so wrap it: +`await act(async () => { await intl.change('de-DE'); });` + +**Verify**: `npm run test --workspace @tiny-intl/react -- --run` → all tests pass, +0 failures. + +### Step 3: Repeat Step 2 for `@tiny-intl/preact` + +Same structure. `packages/preact/src/useIntl.tsx` is a byte-for-byte copy of the +React one except it imports from `'preact/compat'` — so the tests are the same +tests with a different testing library. + +```bash +npm install -D --workspace @tiny-intl/preact \ + vitest@^0.34.6 @vitest/coverage-v8@^0.34.6 jsdom@^22 \ + @testing-library/preact@^3 +``` + +`@preact/preset-vite` is already in the package's devDependencies and already in +`packages/preact/vite.config.ts` `plugins`, so JSX transformation is handled. Add +the same `test: { environment: 'jsdom' }` block and switch the `defineConfig` +import to `'vitest/config'`. + +**Verify**: `npm run test --workspace @tiny-intl/preact -- --run` → all pass. + +### Step 4: Wire up root-level scripts + +In root `package.json`, add a `test` script and a lintable `lint:all`, leaving +the existing `lint` entry untouched (it is invoked by `.lintstagedrc`, which +appends filenames to it — adding a target there would break that): + +```json + "scripts": { + "lint": "eslint --config ./.eslintrc.cjs --ignore-path ./.eslintignore --cache", + "lint:all": "npm run lint -- .", + "test": "lerna run test -- --run", + "coverage": "lerna run coverage", + "release": "lerna publish --no-private", + "git-hooks:commit-msg": "commitlint --edit", + "git-hooks:pre-commit": "lint-staged", + "preversion": "lerna run coverage && lerna run build", + "prepare": "husky install" + }, +``` + +Run `npm run lint:all`. If it reports errors **in the test files you just +wrote**, fix the test files. If it reports errors in pre-existing `src/` files, +do NOT fix them — record them in your report and move on (`src/` is out of +scope). If a lint rule is fundamentally incompatible with test files (e.g. +`import/no-extraneous-dependencies` firing on devDependency imports), add a +narrowly-scoped override to `.eslintrc.cjs` for `packages/*/tests/**` only, in +the same style as the existing `'**/*.config.*'` override at the bottom of that +file. + +**Verify**: `npm test` → core, react and preact each run their suites, exit 0. +`npm run lint:all` → exit 0, or exits non-zero **only** on pre-existing `src/` +findings which you have listed in your report. + +### Step 5: Replace the CI workflow + +Rewrite `.github/workflows/unit-tests.yml` entirely: + +```yaml +name: CI + +on: + push: + branches: ['*'] + pull_request: + branches: ['*'] + +jobs: + test: + name: Lint & test (node ${{ matrix.node-version }}) + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + node-version: [20, 22, 24] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + + - name: Install + run: npm ci + + - name: Lint + run: npm run lint:all + + - name: Test + run: npm test +``` + +Rationale for each change, so you can defend it in review: `setup-node@v4` and +`checkout@v4` run on a supported action runtime (v2 does not); `setup-node`'s +built-in `cache: 'npm'` replaces the hand-rolled `actions/cache` step, which was +both redundant and ordered before the Node install it depended on; `npm ci` is +the correct command when `package-lock.json` is committed; Node 16, 18 and 21 are +all past end-of-life and are dropped. + +**Verify**: `npx --yes yaml-lint .github/workflows/unit-tests.yml` → valid, or if +that tool is unavailable, `node -e "require('fs').readFileSync('.github/workflows/unit-tests.yml','utf8')"` +plus a careful re-read. If `act` or a push to a branch is available, confirm the +workflow goes green. + +## Test plan + +New files (all modelled structurally on `packages/core/tests/index.test.ts` — +`tests/` directory, `describe('@tiny-intl/')`, per-test `{ expect }` +fixture, shared fixture dictionary): + +| File | Cases | +|---|---| +| `packages/react/tests/useIntl.test.tsx` | throws outside provider; `t` returns translation; re-renders on `intl.change`; `n` and `dt` format per locale | +| `packages/react/tests/Translate.test.tsx` | ``; `count={2}` renders the plural form; `number={1000}` renders `1,000`; `date=` renders a formatted date; function-as-children receives the value | +| `packages/preact/tests/useIntl.test.tsx` | same as react | +| `packages/preact/tests/Translate.test.tsx` | same as react | + +Do **not** add `count={0}` or `number={0}` cases here. Those are the regression +tests for plan 003 and belong in that plan, so that plan 003 has a test that goes +red-then-green. + +Verification: `npm test` → all suites pass; 4 new test files exist. + +## Done criteria + +ALL must hold: + +- [ ] `npm ci` exits 0 +- [ ] `npm test` exits 0 and runs suites for core, react and preact +- [ ] `npm run lint:all` exits 0, or fails only on pre-existing `src/` findings listed in the report +- [ ] `ls packages/react/tests packages/preact/tests` — each contains at least 2 test files +- [ ] `git diff --name-only 794eb48..HEAD -- 'packages/*/src'` returns **no output** (no source file was modified) +- [ ] `git status --porcelain packages/solid-js` returns no output (that package was not touched) +- [ ] `.github/workflows/unit-tests.yml` contains `actions/setup-node@v4` and `npm ci`, and contains no occurrence of `@v2` +- [ ] The report records the Step 1 baseline results for all four commands +- [ ] `plans/README.md` status row for 001 updated + +## STOP conditions + +Stop and report back (do not improvise) if: + +- Installing the testing libraries produces peer-dependency errors that only + resolve by upgrading vitest or vite to a new major — that is explicitly out of + scope. Report the conflict. +- You find yourself needing to edit a file under `packages/react/src/` or + `packages/preact/src/` to make a test pass. That is the signal that you have + crossed into plan 003. +- `npm test` (via `lerna run test`) fails because `packages/solid-js` still + exists and has no `test` script. `lerna run` skips packages without the script, + so this should not happen — if it does, report rather than adding a script + there. +- A test you write to document *current* behaviour fails in a way that looks like + a real bug in react or preact that is not the `count={0}` / `number={0}` issue + plan 003 covers. Record the finding; do not fix it. + +## Maintenance notes + +- **Not covered by this plan, deliberately**: CI does not run `lerna run build`, + so a broken adapter build still won't be caught. Nothing blocks adding it — + all three builds are green as of 2026-08-12 — it was simply left out to keep + this plan's diff small. Worth adding as a follow-up, ideally together with a + check that fails on `TS2742` (see plan 006: those diagnostics currently pass + CI silently while dropping a declaration file from the published package). +- Also worth adding to CI once plan 005 lands: `npx publint` on each package, so + a broken `exports` map cannot ship again. +- The `preversion` script (`lerna run coverage && lerna run build`) will now do + more work, because two more packages gained a `coverage` script. That is + intended. +- Reviewers should check that no test asserts against built output (`lib/`) — + every import must come from `../src` or from the `@tiny-intl/core` workspace + package, matching the core suite. +- If a fourth adapter is ever added (Vue, Svelte), the react harness in Step 2 is + the template to copy — it is the simplest of the two. diff --git a/plans/002-remove-solid-js-support.md b/plans/002-remove-solid-js-support.md new file mode 100644 index 0000000..dd75e8e --- /dev/null +++ b/plans/002-remove-solid-js-support.md @@ -0,0 +1,499 @@ +# Plan 002: Remove `@tiny-intl/solid-js` from the monorepo and deprecate it on npm + +> **Executor instructions**: Follow this plan step by step. Run every +> verification command and confirm the expected result before moving to the +> next step. If anything in the "STOP conditions" section occurs, stop and +> report — do not improvise. When done, update the status row for this plan +> in `plans/README.md`. +> +> **Drift check (run first)**: +> `git diff --stat 794eb48..HEAD -- packages/solid-js .eslintrc.cjs package.json commitlint.config.js README.md` +> If any in-scope file changed since this plan was written, compare the +> "Current state" excerpts against the live code before proceeding; on a +> mismatch, treat it as a STOP condition. +> +> **This plan deletes a published package.** Step 7 is human-gated and must NOT +> be executed by an agent. + +## Why this matters + +The maintainer has decided to drop Solid support in the next release. This plan +carries that decision out. + +The decision is well-supported by what the audit found. `@tiny-intl/solid-js` is +the only adapter whose primary API does not work at all: `memoizeCallback` at +`packages/solid-js/src/useIntl.tsx:22` calls `fn.call(args)`, which passes the +argument *array* as the `this` value and forwards **no arguments**, so every +primitive returned by `useIntl()` — `t`, `tc`, `n`, `dt`, `rt`, `sort`, +`collator`, `list` — returns `'[undefined]'` or its numeric equivalent rather +than a translation. That has been the published behaviour since the adapter +shipped, which is also a strong signal about how many users it has. + +Removing it deletes a third copy of the triplicated `Translate.tsx`, drops two +lint dependencies plus the whole Solid toolchain from the dev install, and +removes the single hardest piece of work from plan 001 (Solid + vitest 0.34 +configuration). Plans 001 and 003 have already been scoped to exclude +`packages/solid-js` on the assumption this plan lands. + +What removal does **not** do: it does not un-publish anything. `@tiny-intl/solid-js` +versions up to `1.2.0` stay installable from npm forever. Step 7 covers the +deprecation notice that tells existing installers what happened — that is the +part that actually reaches users, and it needs a human with publish rights. + +## Status + +- **Priority**: P1 +- **Effort**: S +- **Risk**: LOW-MED — the code deletion is mechanical and fully verifiable; the + risk lives in Step 7 (npm deprecation), which is human-gated and reversible. +- **Depends on**: none. Can run first, or in parallel with 001, 004 and 005. +- **Category**: tech-debt +- **Planned at**: commit `794eb48`, 2026-08-12 + +## Current state + +Every reference to Solid in the repository, found with +`grep -rni solid . --exclude-dir={node_modules,.git,lib,dist,coverage,plans}`. +This list is exhaustive as of `794eb48` — if you find a reference not on it, +that is drift and a STOP condition. + +### 1. The package itself — `packages/solid-js/` (13 tracked files) + +``` +packages/solid-js/.gitignore +packages/solid-js/README.md +packages/solid-js/package.json +packages/solid-js/src/Translate.tsx +packages/solid-js/src/index.ts +packages/solid-js/src/useIntl.tsx +packages/solid-js/src/vite-env.d.ts +packages/solid-js/tsconfig.json +packages/solid-js/tsconfig.node.json +packages/solid-js/vite.config.ts +``` + +(plus git-ignored `lib/` and `dist/` build output in the working tree) + +`packages/solid-js/package.json` declares `"name": "@tiny-intl/solid-js"`, +`"version": "1.2.0"`, a `solid-js: >=1.5.0` peer dependency and a +`@tiny-intl/core: ^1.2.0` dependency. **No other package in this repo depends on +it** — `packages/react/package.json` and `packages/preact/package.json` depend +only on `@tiny-intl/core`, and nothing imports from `@tiny-intl/solid-js`. + +### 2. `.eslintrc.cjs:72-79` — the Solid lint override + +```js + { + files: ['packages/solid-js/**/*.ts', 'packages/solid-js/**/*.tsx'], + + extends: [ + '@gridventures/eslint-config-solid-js/typescript', + '@gridventures/eslint-config-base/prettier', + ], + }, +``` + +This is the third entry in the `overrides` array, between the React/Preact +override (ends line 70) and the `'**/*.config.*'` override (starts line 81). + +### 3. `package.json:22,26` — two Solid-only devDependencies + +```json + "@gridventures/eslint-config-base": "^1.3.3", + "@gridventures/eslint-config-react": "^1.4.0", + "@gridventures/eslint-config-solid-js": "^1.4.1", + "@gridventures/eslint-config-typescript": "^1.3.3", + "@types/node": "^20.10.3", + "eslint": "^8.35.0", + "eslint-plugin-solid": "^0.13.0", + "husky": "^8.0.3", +``` + +Both exist only to lint `packages/solid-js`. Note that `eslint-plugin-solid` is +declared at the root but never referenced by name in `.eslintrc.cjs` — it is +pulled in transitively by `@gridventures/eslint-config-solid-js`. + +### 4. `commitlint.config.js:8` — the commit-scope allow-list + +```js + 'scope-enum': async () => { + return [2, 'always', ['core', 'react', 'solid-js', 'preact']]; + }, +``` + +This is enforced on every commit via `.husky/commit-msg`. Leaving `'solid-js'` +in place after the package is gone means the tooling still advertises a scope +that has no code behind it. + +### 5. `README.md:10` — the package list entry + +```markdown +- [@tiny-intl/solid](./packages/solid) ![Solid Adater Size](https://deno.bundlejs.com/badge?q=@tiny-intl/solid-js&treeshake=%5B*%5D&config=%7B%22esbuild%22:%7B%22external%22:%5B%22solid-js%22%5D%7D%7D) +``` + +(The link target `./packages/solid` was already wrong — the directory is +`packages/solid-js`. The whole line goes away here, which resolves that.) + +### 6. `package-lock.json` — 13 Solid-related entries + +`node_modules/@gridventures/eslint-config-solid-js`, +`node_modules/@tiny-intl/solid-js`, `node_modules/babel-preset-solid`, +`node_modules/eslint-plugin-solid` (+ 5 nested `@typescript-eslint/*` entries), +`node_modules/solid-js`, `node_modules/solid-refresh`, +`node_modules/vite-plugin-solid`, and the `packages/solid-js` workspace entry. +These are regenerated, not hand-edited (Step 5). + +### Not a reference, but nearby + +`.vscode.tmpl/settings.json` sets `eslint.workingDirectories` to +`packages/store`, `packages/persist`, `packages/feathers` — none of which exist +in this repo. That file is stale boilerplate from another project. **Out of +scope**; recorded in `plans/README.md` as a deferred finding. + +### Conventions + +- Prettier: `singleQuote: true`, `printWidth: 100`, `trailingComma: "all"`; + 2-space indent (`.editorconfig`). +- `workspaces: ["packages/*"]` in the root `package.json` is a glob — removing + the directory is sufficient, no manifest edit is needed there. +- lerna runs in fixed mode (`lerna.json` has a single `version: "1.2.1"`) and + publishes with `--no-private`. A package that no longer exists is simply never + published again; no lerna configuration change is required. + +## Commands you will need + +> Read from `package.json` during planning; `node_modules` was not installed at +> planning time, so none of these were executed. + +| Purpose | Command | Expected on success | +|---|---|---| +| Install / regenerate lock | `npm install` | exit 0 | +| Core tests | `npm run test --workspace @tiny-intl/core -- --run` | all pass | +| Build an adapter | `npm run build --workspace @tiny-intl/react` | exit 0 | +| Lint | `npm run lint -- packages` (or `npm run lint:all` once plan 001 has landed) | exit 0 | +| List workspaces | `npm query ".workspace"` | three packages, no solid-js | + +## Scope + +**In scope** (the only files you should modify or delete): + +- `packages/solid-js/**` — delete the entire directory +- `.eslintrc.cjs` — remove the Solid override block +- `package.json` (root) — remove two devDependencies +- `commitlint.config.js` — remove the `solid-js` scope +- `README.md` (root) — remove the package-list line +- `package-lock.json` — regenerated by `npm install` +- `plans/README.md` — status row only + +**Out of scope** (do NOT touch, even though they look related): + +- **`packages/react/**` and `packages/preact/**`.** They share no code with the + Solid adapter — `Translate.tsx` is copy-pasted, not imported. Nothing about + removing Solid requires touching them. +- **`packages/core/**`.** Core has no knowledge of any adapter. +- `lerna.json`, package `version` fields, and anything about the release + process. Cutting the release is a human action (Step 7). +- `.vscode.tmpl/settings.json` — stale, but unrelated (see above). +- **Do not run `npm deprecate`, `npm unpublish`, `npm publish`, or + `lerna publish`.** Step 7 is documentation for a human. +- Do not add a replacement, a shim, or a migration guide for Solid users beyond + the deprecation message text in Step 7. If a migration path is wanted, that is + a separate decision. + +## Git workflow + +- Branch: `advisor/002-remove-solid-js-support` +- Conventional commits, enforced by commitlint via `.husky/commit-msg`. + **Sequencing matters here**: commit the `commitlint.config.js` change + (Step 4) *last*, or use a non-`solid-js` scope throughout — once `'solid-js'` + is removed from the `scope-enum`, a commit message scoped `solid-js` will be + rejected by the hook. + Suggested messages, all using allowed scopes: + - `feat: remove solid-js adapter package` (no scope — allowed) + - `chore: drop solid-js lint dependencies` + - `docs: remove solid-js from the package list` + + Prior art in `git log`: `feat: removed defaultProps for compatibility reasons`, + `chore: updated outdated dev dependencies`. +- This is a **breaking change for consumers of `@tiny-intl/solid-js`**. Say so in + the commit body so whoever cuts the release picks the right version bump. +- Do NOT push, publish, or open a PR unless the operator instructed it. + +## Steps + +### Step 1: Confirm nothing depends on the package + +Before deleting anything, prove the removal is safe: + +```bash +grep -rn "@tiny-intl/solid-js" --exclude-dir=node_modules --exclude-dir=.git \ + --exclude-dir=lib --exclude-dir=dist --exclude-dir=plans \ + --include=package.json --include=*.ts --include=*.tsx . +``` + +**Verify**: the only matches are inside `packages/solid-js/` itself and in +`package-lock.json`. If any file under `packages/core`, `packages/react` or +`packages/preact` references `@tiny-intl/solid-js`, STOP and report — the +dependency graph is not what this plan assumes. + +Also record the current published state for the Step 7 handoff: + +```bash +npm view @tiny-intl/solid-js versions --json +npm view @tiny-intl/solid-js dist-tags --json +``` + +(If the registry is unreachable, note it and continue — this is information for +the human doing Step 7, not a gate.) + +### Step 2: Delete the package directory + +```bash +git rm -r packages/solid-js +rm -rf packages/solid-js +``` + +(The second command clears the git-ignored `lib/`, `dist/` and `node_modules` +that `git rm` leaves behind.) + +**Verify**: + +```bash +test -d packages/solid-js && echo "STILL PRESENT" || echo "removed" +``` +→ `removed`. + +### Step 3: Remove the ESLint override + +In `.eslintrc.cjs`, delete the entire override object at lines 72-79 — the one +whose `files` array is `['packages/solid-js/**/*.ts', 'packages/solid-js/**/*.tsx']`. +Leave the React/Preact override above it and the `'**/*.config.*'` override +below it exactly as they are, and make sure the surrounding array commas stay +valid. + +Then remove the two Solid-only devDependencies from the root `package.json`: + +```bash +npm uninstall @gridventures/eslint-config-solid-js eslint-plugin-solid +``` + +**Verify**: + +```bash +grep -n "solid" .eslintrc.cjs package.json +``` +→ no output. + +```bash +node -e "require('./.eslintrc.cjs'); console.log('eslintrc parses')" +``` +→ prints `eslintrc parses`. + +### Step 4: Remove the `solid-js` commit scope + +In `commitlint.config.js`, change line 8 from: + +```js + return [2, 'always', ['core', 'react', 'solid-js', 'preact']]; +``` + +to: + +```js + return [2, 'always', ['core', 'react', 'preact']]; +``` + +**Verify**: + +```bash +echo "fix(solid-js): should be rejected" | npx commitlint +``` +→ exits **non-zero** with a `scope must be one of [core, react, preact]` error. + +```bash +echo "fix(react): should be accepted" | npx commitlint +``` +→ exits 0. + +Remember the Git-workflow note: from this point on, no commit message may use +the `solid-js` scope. + +### Step 5: Update the README and regenerate the lockfile + +Delete line 10 of the root `README.md` — the whole +`- [@tiny-intl/solid](./packages/solid) ...` bullet. The list should end with +the preact entry. + +Then add a short note below the list so anyone landing on the repo (or arriving +from the npm page) understands what happened: + +```markdown +> [!NOTE] +> `@tiny-intl/solid-js` was removed in the next release. Published versions up +> to 1.2.0 remain installable from npm but are no longer maintained. +``` + +Regenerate the lockfile: + +```bash +npm install +``` + +**Verify**: + +```bash +grep -c "solid" package-lock.json +``` +→ `0`. + +```bash +npm query ".workspace" | grep -c '"name"' +``` +→ `3`. (If `npm query` is unavailable on the installed npm version, use +`ls packages/` → `core preact react`.) + +### Step 6: Full verification + +```bash +npm run lint -- packages +npm run test --workspace @tiny-intl/core -- --run +npm run build --workspace @tiny-intl/core +npm run build --workspace @tiny-intl/react +npm run build --workspace @tiny-intl/preact +``` + +**Verify**: lint exits 0; core's 18 tests pass; all three builds exit 0. + +If a react or preact build fails inside `Translate.tsx` with a `TS2345` +union-destructuring error, that is the **pre-existing** failure owned by plan +003 — confirm the error text matches what plan 003 describes, note it, and +continue. It is not caused by this plan. Any *other* failure is a STOP +condition. + +Also confirm you have not touched anything you shouldn't have: + +```bash +git status --porcelain +``` +→ lists only the files in the In-scope list (plus deletions under +`packages/solid-js/`). + +### Step 7: Hand off the npm deprecation — DO NOT RUN THIS + +**This step is for a human with npm publish rights. An executor agent must not +perform it.** Deleting the directory stops future publishes; it does nothing to +the versions already on the registry. Anyone running +`npm install @tiny-intl/solid-js` today still gets `1.2.0` and no indication it +is dead. + +Include the following in your final report, verbatim, as the handoff: + +> **Manual follow-up required before/with the next release.** +> +> Mark the published package deprecated so existing and new installers are +> warned: +> +> ``` +> npm deprecate @tiny-intl/solid-js "No longer maintained. Solid support was removed from tiny-intl; use @tiny-intl/core directly." +> ``` +> +> Notes for whoever runs it: +> - `npm deprecate` is **reversible** — passing an empty string as the message +> clears the notice. +> - It does **not** unpublish. Existing installs keep working; new installs print +> the warning. Do not use `npm unpublish` — it breaks anyone pinned to that +> version. +> - Requires publish rights on the `@tiny-intl` scope. +> - The next release is a **breaking change** for Solid consumers even though +> the remaining packages are unaffected; pick the version bump accordingly and +> mention the removal in the release notes. +> - Published versions at time of removal: (paste the `npm view` output from +> Step 1 here). + +**Verify**: the handoff text above appears in your report. Do not run any npm +publish or deprecate command. + +## Test plan + +There are no new tests. This plan deletes code; the verification is that +everything else still builds, lints and passes: + +- `npm run test --workspace @tiny-intl/core -- --run` → the existing 18 core + tests pass unchanged. Core is untouched, so any failure means something went + wrong outside the intended scope. +- `npm run build` for core, react and preact → all exit 0, proving no build + depended on the Solid package or its toolchain. +- `npm run lint -- packages` → exits 0, proving the ESLint config is still valid + after the override block was removed. +- `npx commitlint` on a `solid-js`-scoped message → rejected (Step 4). +- Every `grep` in the Done criteria returning empty is the machine-checkable + proof that no reference survived. + +Do **not** add tests, and do not port any of the Solid package's would-be tests +elsewhere. + +## Done criteria + +ALL must hold: + +- [ ] `test -d packages/solid-js` → false +- [ ] `grep -rni solid . --exclude-dir={node_modules,.git,lib,dist,coverage,plans} --exclude=package-lock.json` + → **exactly one line**: the `@tiny-intl/solid-js` mention inside the + README removal note that Step 5 adds. No code, config or dependency + reference may remain. + *(Corrected 2026-08-12: this criterion originally said "no output", which + contradicted Step 5's own instruction to add that note. The executor + caught the contradiction and followed Step 5 — the right call.)* +- [ ] `grep -c solid package-lock.json` → `0` +- [ ] `ls packages/` → exactly `core`, `preact`, `react` +- [ ] `node -e "require('./.eslintrc.cjs')"` exits 0 +- [ ] `echo "fix(solid-js): x" | npx commitlint` exits non-zero +- [ ] `npm run lint -- packages` exits 0 +- [ ] `npm run test --workspace @tiny-intl/core -- --run` exits 0 (18 tests) +- [ ] `npm run build` exits 0 for core, react and preact +- [ ] `README.md` contains no `packages/solid` link and does contain the removal note +- [ ] `git status --porcelain` lists only in-scope paths +- [ ] The Step 7 npm-deprecation handoff text is in the final report, and no + npm publish/deprecate command was executed +- [ ] `plans/README.md` status row for 002 updated + +## STOP conditions + +Stop and report back (do not improvise) if: + +- Step 1 finds any file under `packages/core`, `packages/react` or + `packages/preact` importing from or depending on `@tiny-intl/solid-js`. +- `grep -rni solid` turns up a reference not listed in "Current state" — the + repo has drifted since this plan was written, and the removal may be + incomplete. +- Removing `@gridventures/eslint-config-solid-js` or `eslint-plugin-solid` + breaks linting of the react, preact or core packages. That would mean another + config depends on them transitively; report before re-adding anything. +- A react or preact build fails with an error **other than** the pre-existing + `TS2345` in `Translate.tsx` described in plan 003. +- You find yourself needing to edit `packages/react`, `packages/preact` or + `packages/core` to make anything pass. +- Anyone or anything suggests running `npm unpublish`. Do not. + +## Maintenance notes + +- **The finding this supersedes**: the `fn.call(args)` bug at + `packages/solid-js/src/useIntl.tsx:22` is now closed by deletion rather than + by a fix. It is recorded with its evidence in `plans/README.md` under + "Findings closed by plan 002" in case the decision is ever revisited — the + fix was a one-line change to `fn(...args)` plus tests. +- **Plans 001 and 003 assume this lands.** Both have been scoped to react and + preact only. If this plan is rejected or reverted, `packages/solid-js` will be + left with no test coverage and two unfixed bugs (the `useIntl` breakage above, + and the falsy `count`/`number` props in its `Translate.tsx`), and those two + plans need re-scoping to cover it again. +- **`Translate.tsx` is still duplicated** between react and preact — byte-identical + below the imports. Removing the third copy reduces the drift surface but does + not eliminate it; plan 003 keeps a `diff` check between the two. +- **What a reviewer should scrutinise**: that the diff contains no changes under + `packages/core`, `packages/react` or `packages/preact`; that the `overrides` + array in `.eslintrc.cjs` is still valid JavaScript with correct commas; and + that the commitlint scope change was committed with an allowed scope. +- If Solid support is ever reinstated, note that the hardest part was never the + adapter code — it was getting `vite-plugin-solid` to cooperate with vitest + 0.34 in a test environment. Budget for that before budgeting for the adapter. diff --git a/plans/003-fix-translate-falsy-props.md b/plans/003-fix-translate-falsy-props.md new file mode 100644 index 0000000..c5395c0 --- /dev/null +++ b/plans/003-fix-translate-falsy-props.md @@ -0,0 +1,452 @@ +# Plan 003: Fix `` — `count={0}` and `number={0}` render the wrong thing (or nothing) in the react and preact adapters + +> **Executor instructions**: Follow this plan step by step. Run every +> verification command and confirm the expected result before moving to the +> next step. If anything in the "STOP conditions" section occurs, stop and +> report — do not improvise. When done, update the status row for this plan +> in `plans/README.md`. +> +> **Drift check (run first)**: +> `git diff --stat 794eb48..HEAD -- packages/react/src/Translate.tsx packages/preact/src/Translate.tsx` +> If any in-scope file changed since this plan was written, compare the +> "Current state" excerpts against the live code before proceeding; on a +> mismatch, treat it as a STOP condition. + +## Status + +- **Priority**: P1 +- **Effort**: S +- **Risk**: LOW — the branch conditions change, so every `` prop + combination needs a test, but the typecheck baseline is verified green and the + change is three guards. +- **Depends on**: `plans/001-adapter-test-and-ci-baseline.md` +- **Category**: bug +- **Planned at**: commit `794eb48`, 2026-08-12 + +## Why this matters + +The `` component dispatches on which prop was passed, using +truthiness checks: `if (count)`, `if (date && ...)`, `if (number)`. Zero is +falsy, so the two most common zero-valued cases fall through to the wrong +branch: + +- `` skips the plural branch and lands on + `if (name)`, calling `t('document')`. Core's `t` falls back to the `.one` key + (`packages/core/src/createTinyIntl.ts:116`), so the user sees the **singular** + form — "Document" — where their dictionary explicitly defines a `zero` entry + saying "Documents". Empty-state counts are exactly where correct plural + handling matters most, and this is a library whose headline feature is native + `Intl.PluralRules` support. +- `` matches no branch at all and returns `null`. The + component **renders nothing** for the number zero. Any "0 items", "€0.00" or + "0%" in a consuming app silently disappears. +- `` (Unix epoch) has the same shape of problem, though + it is a far rarer input. + +Both published adapters carry the identical bug — `Translate.tsx` is byte-identical +between react and preact below the import block. + +### A note on `packages/solid-js` + +The Solid adapter had the same bug. It is not fixed here: that package is being +deleted by `plans/002-remove-solid-js-support.md`. This plan covers **react and +preact only** and never touches `packages/solid-js`, whether or not 002 has run +yet — there is no ordering constraint between the two plans. If plan 002 is +later rejected, this plan must be re-scoped to include the Solid copy (which +needs the same three condition changes, expressed as `props.count` / +`props.number` / `props.date` inside its `createMemo`). + +## Current state + +### Files and their roles + +- `packages/react/src/Translate.tsx` — React adapter; bug at lines 66 and 78. +- `packages/preact/src/Translate.tsx` — byte-identical to the React file below + the imports; same lines. + +### Excerpt — `packages/react/src/Translate.tsx:53-101` (the bug, and its surroundings) + +```tsx +export function Translate(props: TranslateProps) { + const intl = useContext(TinyIntlContext); + + if (!intl) { + throw new Error('useIntl must be used within a TinyIntlContext.Provider'); + } + + const { subscribe, t, tc, dt, n, rt } = intl; + const { name, count, date, number, options, children, relative } = props; // eslint-disable-line no-shadow + + const [changed, setChanged] = useState(0); + + const translateFn = useCallback(() => { + if (count) { // <-- BUG: 0 is falsy + return tc(name, count, options); + } + + if (date && !relative) { // <-- BUG: epoch 0 is falsy + return dt(date, options); + } + + if (date && relative) { + return rt(date, options); + } + + if (number) { // <-- BUG: 0 is falsy + return n(number, options); + } + + if (name) { + return t(name, options); + } + + return null; + }, [changed, count, date, name, number, options]); +``` + +`packages/preact/src/Translate.tsx:53-101` is the same code with +`from 'preact/compat'` on line 4. + +### Excerpt — `packages/react/src/Translate.tsx:8-51` (the props union — read this before Step 2) + +```tsx +export type TranslateProps = { + children?: (value: string | null) => React.ReactNode; +} & ( + | { + // Case: string translation + name: string; + count?: number; + date?: undefined; + number?: undefined; + options?: TinyIntlTranslateTemplate; + relative?: undefined; + unit?: undefined; + } + | { + // Case: dateFormat + name?: undefined; + count?: undefined; + data?: undefined; + date?: Date | string | number; + number?: undefined; + options?: Intl.DateTimeFormatOptions; + relative?: undefined; + } + | { + // Case: relativeTimeFormat + name?: undefined; + count?: undefined; + data?: undefined; + date?: Date | string | number; + number?: undefined; + options?: Intl.RelativeTimeFormatOptions; + relative?: true; + } + | { + // Case: numberFormat + name?: undefined; + count?: undefined; + data?: undefined; + date?: undefined; + number?: number; + options?: Intl.NumberFormatOptions; + relative?: undefined; + } +); +``` + +**TypeScript narrows this union correctly today — verified, 2026-08-12.** An +earlier draft of this plan claimed `tc(name, count, options)` was a latent +`TS2345` error. That was wrong, and it was measured: `npx tsc --noEmit` exits +**0** in both `packages/react` and `packages/preact` (with `src/Translate.tsx` +confirmed present in the compilation via `--listFiles`). + +The reason matters for Step 2. Each union member gives `count`, `date` and +`number` a *unit* type — `count?: number` in the first member and +`count?: undefined` in the other three — which is exactly the shape TypeScript's +control-flow analysis for destructured discriminated unions (TS 4.6+) can +discriminate on. So inside `if (count) { … }`, narrowing `count` to a truthy +`number` eliminates the three members where it is `undefined`, which in turn +narrows `name` to `string` and `options` to `TinyIntlTranslateTemplate | undefined`. +The call typechecks. + +**Consequence for the fix**: replacing `if (count)` with +`typeof count === 'number'` should narrow *just as well* — it also excludes the +`undefined` members. So the casts an earlier draft prescribed are most likely +unnecessary, and adding them would weaken type safety for no reason. Step 2 +therefore tells you to write the fix **without** casts and let the compiler +decide. + +Note the typo carried through three of the four union members in both packages: +`data?: undefined` where `date?: undefined` was clearly meant. Fixing that would +change which prop combinations typecheck for consumers — **out of scope here**, +recorded in `plans/README.md` as a deferred finding. + +### Conventions + +- Prettier: `singleQuote: true`, `printWidth: 100`, `trailingComma: "all"`; + 2-space indent. +- React/Preact files are linted with `@gridventures/eslint-config-react` + + `/hooks` (see the `overrides` block in `.eslintrc.cjs`). +- The existing `/* eslint-disable react-hooks/exhaustive-deps */` at the top of + both files stays — do not remove it. + +## Commands you will need + +> Read from `package.json` during planning; `node_modules` was not installed at +> planning time, so these were not executed. + +| Purpose | Command | Expected on success | +|---|---|---| +| Install | `npm install` | exit 0 | +| Typecheck + build (per package) | `npm run build --workspace @tiny-intl/react` | exit 0 | +| Tests (per package) | `npm run test --workspace @tiny-intl/react -- --run` | all pass | +| Full suite | `npm test` | all pass | +| Lint | `npm run lint:all` | exit 0 | + +## Scope + +**In scope** (the only files you should modify): + +- `packages/react/src/Translate.tsx` +- `packages/preact/src/Translate.tsx` +- `packages/react/tests/Translate.test.tsx` +- `packages/preact/tests/Translate.test.tsx` +- `plans/README.md` — status row only + +**Out of scope** (do NOT touch, even though they look related): + +- **`packages/solid-js/**`** — being deleted by plan 002. If the directory still + exists when you run, leave it alone. +- **The `TranslateProps` union itself.** Do not add a discriminant, do not fix + the `data?: undefined` typo, do not split it into separate components. Any of + those changes what compiles for existing consumers — a breaking change that + needs its own release decision. This plan changes runtime behaviour only, plus + the minimum type assertions needed to keep `tsc` quiet. +- `packages/*/src/useIntl.tsx` — the react/preact ones have a separate deferred + finding (stale `useCallback` identity). +- `packages/core/**` — core's `t()` fallback to `.one` is intended behaviour for + a missing key. +- The falsy-`options` question. `options={0}` is not representable in the type; + ignore it. + +## Git workflow + +- Branch: `advisor/003-fix-translate-falsy-props` +- Conventional commits, enforced by commitlint, with scopes limited to + `core`, `react`, `preact` (and `solid-js` until plan 002 removes it). One + commit per package reads well here: + `fix(react): render Translate correctly for zero count and number`, then + `fix(preact): ...`. Prior art in `git log`: + `fix(core): return fallback if navigator is undefined`. +- Do NOT push or open a PR unless the operator instructed it. + +## Steps + +### Step 1: Reproduce the bug, and confirm the typecheck baseline is green + +```bash +npm install +npm run build --workspace @tiny-intl/react +cd packages/react && npx tsc --noEmit; echo "tsc exit=$?"; cd ../.. +``` + +**Expected**: build exits 0 and `tsc exit=0`. This was verified on 2026-08-12 — +there is **no** pre-existing type error in `Translate.tsx`. If you see type +errors here, STOP: the repo has drifted and the analysis in "Current state" no +longer holds. + +(Capture `tsc`'s exit code without a pipe — `npx tsc --noEmit | head` reports +`head`'s status, not the compiler's.) + +Then add failing tests **before** touching the source. In +`packages/react/tests/Translate.test.tsx` (created by plan 001), add: + +```tsx +it('renders the zero plural form for count={0}', async ({ expect }) => { + // de-DE dictionary defines document.zero = 'Dokumente', document.one = 'Dokument' + // expected: 'Dokumente' +}); + +it('renders a formatted zero for number={0}', async ({ expect }) => { + // expected: '0', not '' and not null +}); +``` + +**Verify**: `npm run test --workspace @tiny-intl/react -- --run` → these two +tests **fail**, and the failure messages show `'Dokument'` (singular) and an +empty render respectively. If they pass, STOP — the bug does not reproduce and +this plan's premise is wrong. + +### Step 2: Fix `packages/react/src/Translate.tsx` + +Replace the body of `translateFn` (lines 65-87) with explicit presence checks. +**Write it exactly as below — with no type assertions.** See "Current state": +TypeScript discriminates this union on the unit types of `count`/`date`/`number`, +and `typeof` guards preserve that narrowing. + +```tsx + const translateFn = useCallback(() => { + if (typeof count === 'number') { + return tc(name, count, options); + } + + if (date !== undefined && !relative) { + return dt(date, options); + } + + if (date !== undefined && relative) { + return rt(date, options); + } + + if (typeof number === 'number') { + return n(number, options); + } + + if (name) { + return t(name, options); + } + + return null; + }, [changed, count, date, name, number, options, relative]); +``` + +Three things changed and each is load-bearing: + +1. `if (count)` → `typeof count === 'number'`, so `0` takes the plural branch. +2. `if (number)` → `typeof number === 'number'`, so `0` is formatted. +3. `date &&` → `date !== undefined`, so the epoch `0` formats as a date. + +Plus `relative` is added to the `useCallback` dependency array, where it was +missing. Everything else — including the final `if (name)` — is unchanged. + +**If and only if `tsc` then reports an error**, add the narrowest fix that +silences it, in this order of preference: (a) an extra `typeof` guard that helps +the compiler discriminate, e.g. `typeof count === 'number' && typeof name === 'string'`; +(b) failing that, an `as` assertion on the specific argument that errors, with a +comment naming the TS error code. Do **not** pre-emptively add assertions. +**Report every assertion you had to add, with the exact compiler error that +forced it** — that outcome contradicts the verified baseline and your reviewer +needs to know. + +`TinyIntlTranslateTemplate` is already imported at the top of the file +(line 2) — no new import is needed unless you add an assertion that uses it. + +**Verify**: + +```bash +npm run build --workspace @tiny-intl/react +cd packages/react && npx tsc --noEmit; echo "tsc exit=$?"; cd ../.. +npm run test --workspace @tiny-intl/react -- --run +``` +→ build exits 0, `tsc exit=0`, and the two tests from Step 1 now pass. + +### Step 3: Apply the identical fix to `packages/preact/src/Translate.tsx` + +The Preact file is character-identical to the React one from line 8 onward. +Apply exactly the same replacement, then confirm the two files have not drifted +apart in any way other than their imports: + +```bash +diff <(tail -n +8 packages/react/src/Translate.tsx) <(tail -n +8 packages/preact/src/Translate.tsx) +``` +→ no output (both files are identical after their import blocks). + +Add the same two tests to `packages/preact/tests/Translate.test.tsx`. + +**Verify**: `npm run build --workspace @tiny-intl/preact` exits 0; +`npm run test --workspace @tiny-intl/preact -- --run` → all pass. + +### Step 4: Prove no truthiness checks on numeric props remain + +```bash +grep -rn "if (count)\|if (number)\|date &&" packages/react/src/Translate.tsx packages/preact/src/Translate.tsx +``` +→ no output. + +```bash +npm test +npm run lint:all +``` + +**Verify**: full suite passes; lint exits 0. + +## Test plan + +Add to each of `packages/react/tests/Translate.test.tsx` and +`packages/preact/tests/Translate.test.tsx` — modelled structurally on +`packages/core/tests/index.test.ts` (per-test `{ expect }` fixture, shared +fixture dictionary, `describe` named after the package): + +| Case | Props | Expected | +|---|---|---| +| zero plural (regression) | `name="document" count={0}`, locale de-DE | `Dokumente` (the `zero` entry), **not** `Dokument` | +| one plural | `name="document" count={1}`, de-DE | `Dokument` | +| many plural | `name="document" count={5}`, de-DE | `Dokumente` | +| zero number (regression) | `number={0}`, en-US | `0` — non-empty render | +| non-zero number | `number={1000}`, en-US | `1,000` | +| epoch date (regression) | `date={0}` | a formatted date string, non-empty | +| plain name | `name="inbox"`, de-DE | `Posteingang` | +| template params | `name="hello" options={{ name: 'John' }}` | `Hello, John!` | +| function children | `name="inbox"` with `children={(v) => ...}` | the callback receives the translated value | +| no matching prop | `{}` (cast as needed) | renders nothing | + +The first, fourth and sixth rows are the regression tests for this plan — they +must be observed failing in Step 1 before the fix. + +Verification: `npm test` → all pass, with ≥ 3 new tests per adapter package. + +## Done criteria + +ALL must hold: + +- [ ] `grep -rn "if (count)\|if (number)" packages/react/src/Translate.tsx packages/preact/src/Translate.tsx` → no output +- [ ] `grep -rn "date &&" packages/react/src/Translate.tsx packages/preact/src/Translate.tsx` → no output +- [ ] `npm run build --workspace @tiny-intl/react` exits 0 +- [ ] `npm run build --workspace @tiny-intl/preact` exits 0 +- [ ] `npm test` exits 0; each adapter has a passing `count={0}` and `number={0}` test +- [ ] `npm run lint:all` exits 0 +- [ ] `diff <(tail -n +8 packages/react/src/Translate.tsx) <(tail -n +8 packages/preact/src/Translate.tsx)` → no output +- [ ] `git status --porcelain packages/solid-js` returns no output (that package was not touched) +- [ ] `git status --porcelain` lists only files from the In-scope list +- [ ] `plans/README.md` status row for 003 updated + +## STOP conditions + +Stop and report back (do not improvise) if: + +- The Step 1 regression tests **pass** before the fix — the bug does not + reproduce as described and the diagnosis is wrong. +- `packages/react/src/Translate.tsx:66` does not read `if (count) {` — the code + has drifted since this plan was written. +- **`tsc --noEmit` reports errors in `Translate.tsx` in Step 1, before you have + changed anything.** The verified baseline is clean; errors there mean the repo + drifted and this plan's type analysis is stale. +- Making `tsc` pass after Step 2 requires changing `TranslateProps` itself, or + more than one `as` assertion per file. Report what you found; changing the + exported prop types is a breaking change and needs a human decision. +- Fixing the epoch-date case (`date !== undefined`) breaks an existing test — + it may reveal that some consumer path relies on `date={null}` or + `date={''}` falling through. Report before adapting. + +## Maintenance notes + +- **An explicit discriminant would still be an improvement.** TypeScript + narrows the current union only because every member happens to give + `count`/`date`/`number` a unit type; that is a property the union could lose + accidentally in any future edit, silently taking type safety with it. An + explicit `kind` prop, or splitting `` into ``, ``, + ``, would make the discrimination intentional and let TypeScript + reject invalid prop *combinations* at the call site (which it does not do + today). Breaking API change — next major. +- **Deferred, recorded in `plans/README.md`**: the `data?: undefined` typo in + three of the four union members (both packages) — clearly meant to be `date`, + and fixing it changes which prop combinations typecheck. +- `Translate.tsx` is duplicated between react and preact, byte-identical below + the imports (it used to be triplicated; plan 002 removes the third copy). + Every future change to one must be mirrored; the `diff` check in Step 3's + verification is worth promoting to a CI guard if this pattern persists. +- **What a reviewer should scrutinise**: that the branch *order* is unchanged + (count → date → relative date → number → name), since it defines precedence + when a consumer passes prop combinations the types don't forbid. diff --git a/plans/004-fix-dead-intl-formatter-caches.md b/plans/004-fix-dead-intl-formatter-caches.md new file mode 100644 index 0000000..5c0bd59 --- /dev/null +++ b/plans/004-fix-dead-intl-formatter-caches.md @@ -0,0 +1,448 @@ +# Plan 004: Fix the five `Intl` formatter caches in core — every one is written with one key and read with another, so none of them ever hits + +> **Executor instructions**: Follow this plan step by step. Run every +> verification command and confirm the expected result before moving to the +> next step. If anything in the "STOP conditions" section occurs, stop and +> report — do not improvise. When done, update the status row for this plan +> in `plans/README.md`. +> +> **Drift check (run first)**: +> `git diff --stat 794eb48..HEAD -- packages/core/src/createTinyIntl.ts packages/core/tests` +> If any in-scope file changed since this plan was written, compare the +> "Current state" excerpts against the live code before proceeding; on a +> mismatch, treat it as a STOP condition. + +## Status + +- **Priority**: P1 +- **Effort**: S +- **Risk**: LOW — output values are unchanged; only the number of `Intl` + constructions changes. The caches are already cleared on every locale change, + so keying purely by options is correct. +- **Depends on**: none (core already has a working vitest suite) +- **Category**: perf +- **Planned at**: commit `794eb48`, 2026-08-12 + +## Why this matters + +`createTinyIntl` builds five `Map` caches so that repeated calls to `n()`, +`dt()`, `rt()`, `list()` and `collator()` reuse their `Intl.*Format` instances. +Constructing an `Intl` formatter is the expensive part of the operation — it +resolves locale data and builds the format pattern — while `.format()` on an +existing instance is comparatively cheap. That is why the cache exists. + +The cache does not work. Each function computes a `cacheKey` from its options, +**writes** the formatter under `cacheKey`, and then **reads** with +`cache.get(locale)`. `locale` is a value like `'de-DE'`; the stored keys are +`'_'` or a JSON string like `'{"dateStyle":"full"}'`. They can never be equal, +so every lookup misses and a brand-new `Intl` formatter is constructed on +**every single call** to any of those five functions. The `Map`s fill up and are +never read. + +For the library's core use case — a list view formatting a few hundred numbers +or dates per render — this is the difference between five formatter +constructions and several hundred. It is a one-character-per-site fix in a +package that advertises itself on being small and fast. + +There is no correctness bug today: because the lookup always misses, the +formatter is always built with the caller's actual options. That also means the +naive "fix" of keying by `locale` would be actively wrong — it would return a +formatter built with someone else's options. The fix below keys by `cacheKey` +alone, which is safe precisely because `change()` clears all five caches. + +## Current state + +### Files and their roles + +- `packages/core/src/createTinyIntl.ts` — the whole of `createTinyIntl`, + including the five caches, the `newCacheKey` helper, and the `change()` + function that clears them. +- `packages/core/tests/index.test.ts` — the existing suite; already covers the + *output* of all five functions, so it protects this refactor. New tests here + will assert the cache actually hits. + +### Excerpt — `packages/core/src/createTinyIntl.ts:65-67` (the key helper) + +```ts +function newCacheKey(v?: object | undefined) { + return v ? JSON.stringify(v) : '_'; +} +``` + +### Excerpt — `packages/core/src/createTinyIntl.ts:78-97` (the caches, and where they are cleared) + +```ts + const numberFormatCache = new Map(); + const dateTimeFormatCache = new Map(); + const relativeTimeFormatCache = new Map(); + const listFormatCache = new Map(); + const collatorCache = new Map(); +``` + +```ts + async function change(nextLocale: Locales, staticDict?: TinyIntlDict, forceLoad = false) { + if (locale === nextLocale && !forceLoad) { + return dict; + } + locale = nextLocale; + pluralRules = new Intl.PluralRules(locale); + numberFormatCache.clear(); + dateTimeFormatCache.clear(); + relativeTimeFormatCache.clear(); + numberFormatCache.clear(); // <-- line 95: duplicate of line 92 + listFormatCache.clear(); + collatorCache.clear(); +``` + +Every cache is cleared whenever the locale changes — which is what makes +"key by options only" correct. Line 95 clears `numberFormatCache` a second time; +line 92 already did. Harmless, but it is the reason no one noticed that no cache +was clearing wrongly: all five *are* covered. + +### Excerpt — `packages/core/src/createTinyIntl.ts:129-206` (the five broken sites) + +```ts + function n(number: number, options?: Intl.NumberFormatOptions): string { + const cacheKey = newCacheKey(options); + let formatter = numberFormatCache.get(locale); // <-- reads `locale` + if (!formatter) { + formatter = new Intl.NumberFormat(locale, options); + numberFormatCache.set(cacheKey, formatter); // <-- writes `cacheKey` + } + return formatter.format(number); + } + + function dt(date: Date | string | number, options?: Intl.DateTimeFormatOptions): string { + const dateValue = new Date(date); + const cacheKey = newCacheKey(options); + let formatter = dateTimeFormatCache.get(locale); // <-- same + if (!formatter) { + formatter = new Intl.DateTimeFormat(locale, options); + dateTimeFormatCache.set(cacheKey, formatter); + } + return formatter.format(dateValue); + } + + // Intl.RelativeTimeFormat is not supported in Safari < 14 or on MacOS < 11 + function rt(date: Date | string | number, options?: TinyIntlRelativeTimeFormatOptions): string { + const { fallback, ...rtOptions } = options || {}; + const [value, unit] = automaticRelativeTimeFormat(date); + if (!Intl.RelativeTimeFormat) { + console.warn('Intl.RelativeTimeFormat is not supported in this browser'); + if (fallback) { + return fallback(value, unit); + } + return ''; + } + const cacheKey = newCacheKey(rtOptions); + let formatter = relativeTimeFormatCache.get(locale); // <-- same + if (!formatter) { + formatter = new Intl.RelativeTimeFormat(locale, rtOptions); + relativeTimeFormatCache.set(cacheKey, formatter); + } + return formatter.format(value, unit); + } + + function collator(options?: Intl.CollatorOptions) { + if (!Intl.Collator) { + console.warn('Intl.Collator is not supported in this browser'); + return (x: string, y: string) => x.localeCompare(y); + } + const cacheKey = newCacheKey(options || {}); // <-- note: `|| {}` + let formatter = collatorCache.get(locale); // <-- same + if (!formatter) { + formatter = new Intl.Collator(locale, options); + collatorCache.set(cacheKey, formatter); + } + return formatter.compare; + } + + function list(items: string[], options?: Intl.ListFormatOptions | 'AND' | 'OR') { + if (!Intl.ListFormat) { + console.warn('Intl.ListFormat is not supported in this browser'); + return items.join(', '); + } + let type: 'conjunction' | 'disjunction' = 'conjunction'; + if (options === 'OR') { + type = 'disjunction'; + } + const intlOptions = typeof options === 'string' ? ({ type, style: 'long' } as const) : options; + const cacheKey = newCacheKey(intlOptions); + let formatter = listFormatCache.get(locale); // <-- same + if (!formatter) { + formatter = new Intl.ListFormat(locale, intlOptions); + listFormatCache.set(cacheKey, formatter); + } + return formatter.format(items); + } +``` + +Note `collator` uses `newCacheKey(options || {})` while the other four use +`newCacheKey(options)`. That produces the key `'{}'` for a no-options call where +the others produce `'_'` — a cosmetic inconsistency worth normalising while you +are here. + +### Conventions + +- The file starts with `/* eslint-disable @typescript-eslint/naming-convention */` + — keep it. +- Prettier: `singleQuote: true`, `printWidth: 100`, `trailingComma: "all"`; + 2-space indent. +- Test style: see `packages/core/tests/index.test.ts` — `describe('@tiny-intl/core')`, + per-test `{ expect }` fixture argument (not an import), `vi` imported from + `vitest` for stubbing, `afterEach` resetting the locale to `'en-US'`. + +## Commands you will need + +> Read from `package.json` during planning; `node_modules` was not installed at +> planning time, so these were not executed. + +| Purpose | Command | Expected on success | +|---|---|---| +| Install | `npm install` | exit 0 | +| Core tests | `npm run test --workspace @tiny-intl/core -- --run` | all pass | +| Core coverage | `npm run coverage --workspace @tiny-intl/core` | exit 0 | +| Core build (also typechecks) | `npm run build --workspace @tiny-intl/core` | exit 0 | +| Lint | `npm run lint:all` (or `npm run lint -- packages/core`) | exit 0 | + +## Scope + +**In scope** (the only files you should modify): + +- `packages/core/src/createTinyIntl.ts` +- `packages/core/tests/index.test.ts` +- `plans/README.md` — status row only + +**Out of scope** (do NOT touch, even though they look related): + +- **`change()`'s early return and its `forceLoad` parameter.** There is a + separate known issue there (a lost-update race, and `staticDict` being + silently dropped when the locale is unchanged) recorded in `plans/README.md`. + Touching `change()` beyond deleting the one duplicate `.clear()` line will + collide with that work. +- Cache eviction / size bounds. The maps are unbounded by design; see + Maintenance notes. +- The `console.warn` calls in `rt`, `collator` and `list` — noisy, but changing + them changes observable behaviour that the existing test at + `packages/core/tests/index.test.ts:176-208` depends on. +- Anything under `packages/react` or `packages/preact` (or `packages/solid-js`, + if it is still present — plan 002 deletes it). + +## Git workflow + +- Branch: `advisor/004-fix-dead-intl-formatter-caches` +- Conventional commits (commitlint enforced). Prior art in `git log`: + `fix(core): improved performance by using param for initial dict loading`. + Suggested: `fix(core): read Intl formatter caches with the key they are written with`. +- Do NOT push or open a PR unless the operator instructed it. + +## Steps + +### Step 1: Write the failing cache-hit tests first + +Add a new block to `packages/core/tests/index.test.ts`, inside the existing +`describe('@tiny-intl/core')`. It must prove the cache hits by counting +constructor calls: + +```ts + it('reuses cached Intl formatters', async ({ expect }) => { + await intl.change('en-US'); + + const numberFormatSpy = vi.spyOn(Intl, 'NumberFormat'); + intl.n(1); + intl.n(2); + intl.n(3); + expect(numberFormatSpy).toHaveBeenCalledTimes(1); + + intl.n(4, { style: 'percent' }); + intl.n(5, { style: 'percent' }); + expect(numberFormatSpy).toHaveBeenCalledTimes(2); + numberFormatSpy.mockRestore(); + }); +``` + +Repeat the same shape for `Intl.DateTimeFormat` (via `intl.dt`), +`Intl.RelativeTimeFormat` (via `intl.rt` — stub `Date.now` as at +`packages/core/tests/index.test.ts:119-129`), `Intl.ListFormat` (via +`intl.list`) and `Intl.Collator` (via `intl.collator`). Five tests, or one test +with five sections. + +Add one more test proving the cache is invalidated on locale change: + +```ts + it('rebuilds Intl formatters after a locale change', async ({ expect }) => { + await intl.change('en-US'); + const spy = vi.spyOn(Intl, 'NumberFormat'); + intl.n(1000); + await intl.change('de-DE'); + intl.n(1000); + expect(spy).toHaveBeenCalledTimes(2); + spy.mockRestore(); + }); +``` + +If `vi.spyOn(Intl, 'NumberFormat')` does not work with `new` in vitest 0.34, +fall back to `vi.stubGlobal` with a hand-rolled counting wrapper that delegates +to the original constructor — the same `vi.stubGlobal` technique the suite +already uses for `navigator` at `packages/core/tests/index.test.ts:258`. Note +which approach you used in your report. + +**Verify**: `npm run test --workspace @tiny-intl/core -- --run` → the new +cache-hit tests **fail**, each reporting 3 (or 2) constructor calls where 1 was +expected. The locale-change test should already pass. If the cache-hit tests +pass before the fix, STOP — the bug does not reproduce. + +### Step 2: Read each cache with the key it is written with + +In `packages/core/src/createTinyIntl.ts`, change five lines. In each function, +the `.get(locale)` becomes `.get(cacheKey)`: + +| Line | Function | From | To | +|---|---|---|---| +| 131 | `n` | `numberFormatCache.get(locale)` | `numberFormatCache.get(cacheKey)` | +| 142 | `dt` | `dateTimeFormatCache.get(locale)` | `dateTimeFormatCache.get(cacheKey)` | +| 162 | `rt` | `relativeTimeFormatCache.get(locale)` | `relativeTimeFormatCache.get(cacheKey)` | +| 176 | `collator` | `collatorCache.get(locale)` | `collatorCache.get(cacheKey)` | +| 200 | `list` | `listFormatCache.get(locale)` | `listFormatCache.get(cacheKey)` | + +Do not change anything else in those functions — in particular, the `new Intl.*` +constructions must keep passing `locale` as their first argument. `locale` is +still the right thing to *construct* with; it is only wrong as a *lookup key*. + +While in `collator` (line 175), normalise the key computation for consistency +with the other four: + +```ts + const cacheKey = newCacheKey(options); +``` + +(dropping the `|| {}`, so a no-options call keys as `'_'` like everywhere else). + +**Verify**: + +```bash +grep -n "Cache.get(locale)" packages/core/src/createTinyIntl.ts +``` +→ no output. + +```bash +grep -c "Cache.get(cacheKey)" packages/core/src/createTinyIntl.ts +``` +→ `5`. + +```bash +npm run test --workspace @tiny-intl/core -- --run +``` +→ all tests pass, including the new cache-hit tests and all 18 pre-existing +tests. **The pre-existing tests are the real safety net here** — they assert +concrete formatted output for de-DE, en-US and sv-SE across numbers, dates, +relative times, sorting and lists. If any of them now fails, the caching change +has altered output and something is wrong. + +### Step 3: Remove the duplicate cache clear + +At `packages/core/src/createTinyIntl.ts:95`, delete the second +`numberFormatCache.clear();` (line 92 already does it). The block should end up +as exactly five `.clear()` calls, one per cache: + +```ts + numberFormatCache.clear(); + dateTimeFormatCache.clear(); + relativeTimeFormatCache.clear(); + listFormatCache.clear(); + collatorCache.clear(); +``` + +Do not otherwise modify `change()`. + +**Verify**: + +```bash +grep -c "numberFormatCache.clear()" packages/core/src/createTinyIntl.ts +``` +→ `1`. + +### Step 4: Full verification + +```bash +npm run build --workspace @tiny-intl/core +npm run coverage --workspace @tiny-intl/core +npm run lint:all +``` + +**Verify**: build exits 0; coverage run exits 0 with all tests passing; lint +exits 0. + +## Test plan + +- **File**: `packages/core/tests/index.test.ts` (existing). +- **Structural pattern**: the file itself — `describe('@tiny-intl/core')`, + per-test `{ expect }` fixture, `vi` for stubbing, `afterEach` resetting to + `'en-US'`. Place the new tests after the existing `'list formatting'` test so + related concerns stay together. +- **New cases**: + 1. `n` constructs one `Intl.NumberFormat` across repeated identical calls + 2. …and a second for a different options object + 3. `dt` — same two properties + 4. `rt` — same two properties (with `Date.now` stubbed) + 5. `list` — same two properties + 6. `collator` — same two properties + 7. a locale change forces reconstruction +- **Regression protection**: the 18 pre-existing tests already pin the formatted + output of every affected function in three locales. They must all still pass + unmodified — do not adjust an existing assertion to accommodate this change. +- **Verification**: `npm run test --workspace @tiny-intl/core -- --run` → all + pass, with ≥ 6 new tests. + +## Done criteria + +ALL must hold: + +- [ ] `grep -n "Cache.get(locale)" packages/core/src/createTinyIntl.ts` → no output +- [ ] `grep -c "Cache.get(cacheKey)" packages/core/src/createTinyIntl.ts` → `5` +- [ ] `grep -c "numberFormatCache.clear()" packages/core/src/createTinyIntl.ts` → `1` +- [ ] `npm run test --workspace @tiny-intl/core -- --run` exits 0, with ≥ 6 new tests +- [ ] All 18 pre-existing core tests pass **without any assertion being modified** + (`git diff packages/core/tests/index.test.ts` shows additions only) +- [ ] `npm run build --workspace @tiny-intl/core` exits 0 +- [ ] `npm run lint:all` exits 0 +- [ ] `git status --porcelain` lists only files from the In-scope list +- [ ] `plans/README.md` status row for 004 updated + +## STOP conditions + +Stop and report back (do not improvise) if: + +- The Step 1 cache-hit tests pass **before** the fix — the bug does not + reproduce and the diagnosis is wrong. +- `packages/core/src/createTinyIntl.ts:131` does not read + `let formatter = numberFormatCache.get(locale);` — the code has drifted. +- Any pre-existing test in `packages/core/tests/index.test.ts` fails after Step + 2. Do **not** edit that test to make it pass; the change has altered output, + which it must not, and that needs investigating. +- You conclude the cache should be keyed by `` `${locale}:${cacheKey}` ``. + It should not — `change()` clears every cache on locale change, so the locale + is already implied. If you believe `change()` can be reached without clearing, + report that finding instead of working around it. +- `vi.spyOn` on an `Intl` constructor cannot be made to work after two attempts + with both the `spyOn` and `stubGlobal` approaches. + +## Maintenance notes + +- **Unbounded growth, accepted**: each cache now grows one entry per distinct + options object per locale, cleared on every locale change. For normal usage + (a handful of format configurations) that is a few entries. A caller + generating options dynamically — e.g. `{ currency: order.currency }` across + many currencies — would grow the map. If that ever becomes a real + problem, an LRU bound is the fix; do not pre-emptively add one. +- **Key stability**: `newCacheKey` uses `JSON.stringify`, whose output depends + on property insertion order. `{ style: 'currency', currency: 'EUR' }` and + `{ currency: 'EUR', style: 'currency' }` produce different keys and therefore + two formatters. Correct, just slightly wasteful. Sorting keys before + stringifying would fix it, at the cost of doing more work on the hot path. +- **What a reviewer should scrutinise**: that every `new Intl.*(...)` call still + receives `locale` as its first argument, and that the diff to the existing + test file is additions only. +- If `change()` is ever refactored to *not* clear the caches (for example, to + cache across locales), these keys must immediately become locale-qualified. + That is the one change that would silently break this fix. diff --git a/plans/005-fix-core-package-exports.md b/plans/005-fix-core-package-exports.md new file mode 100644 index 0000000..7640364 --- /dev/null +++ b/plans/005-fix-core-package-exports.md @@ -0,0 +1,552 @@ +# Plan 005: Fix `@tiny-intl/core`'s `exports` map — the declared `types` path does not exist — and delete the dead vite build + +> **Executor instructions**: Follow this plan step by step. Run every +> verification command and confirm the expected result before moving to the +> next step. If anything in the "STOP conditions" section occurs, stop and +> report — do not improvise. When done, update the status row for this plan +> in `plans/README.md`. +> +> **Drift check (run first)**: +> `git diff --stat 794eb48..HEAD -- packages/core/package.json packages/core/vite.config.ts packages/core/tsup.config.ts packages/react/package.json packages/preact/package.json` +> If any in-scope file changed since this plan was written, compare the +> "Current state" excerpts against the live code before proceeding; on a +> mismatch, treat it as a STOP condition. + +## Status + +- **Priority**: P2 +- **Effort**: S +- **Risk**: MED — this changes how the published packages resolve for *every* + consumer. It cannot be verified by the unit tests; it needs the packaging + checks in Step 2. Get those green before merging. +- **Depends on**: none +- **Category**: tech-debt / dx +- **Planned at**: commit `794eb48`, 2026-08-12 + +## Why this matters + +`@tiny-intl/core` declares a `types` entry in its `exports` map that points at a +file that has never existed: + +```json +"types": "./lib/esm/types/index.d.ts" +``` + +The build (tsup) emits declarations to `lib/esm/index.d.ts` — there is no +`lib/esm/types/` directory at all. The `types` condition is also listed **last** +in the condition object, where the Node/TypeScript resolution algorithm — which +picks the first matching condition — will have already matched `import` or +`require` before reaching it. And the `require` branch points at +`./lib/cjs/index.js`, whose declaration file is `index.d.cts`, which is not the +name TypeScript looks for next to a `.js` file. + +For a TypeScript library, type resolution *is* the product. Consumers on +`moduleResolution: "node16" | "nodenext" | "bundler"` — the default for anything +modern — are relying on a fallback path rather than the declared one, and CJS +consumers are the most likely to get "Could not find a declaration file for +module '@tiny-intl/core'". The exact symptom varies by resolver and TypeScript +version, which is precisely why Step 2 measures it with a tool instead of +guessing. + +The same file also carries a dead build: `npm run build` runs `vite build` +(emitting to `dist/`) and then tsup (emitting to `lib/`), but `files` only ships +`lib` and `src`. Everything vite produces is thrown away on every build, along +with two devDependencies that exist solely to feed it. And tsup's +`entry: ['src']` glob sweeps in `src/vite-env.d.ts`, publishing junk +`lib/esm/vite-env.d.js` and `lib/cjs/vite-env.d.d.cts` files to npm. + +## Current state + +### Files and their roles + +- `packages/core/package.json` — the broken `exports` map; the `build` script + that runs the dead vite step. +- `packages/core/vite.config.ts` — a library build config emitting to `dist/`, + **and** the vitest `test: {}` config. The test config must survive; the build + config should not. +- `packages/core/tsup.config.ts` — the build that actually produces what ships. +- `packages/react/package.json`, `packages/preact/package.json` — same + `types`-last condition ordering; their paths do at least exist. + +### Excerpt — `packages/core/package.json:8-30` + +```json + "type": "module", + "files": [ + "lib", + "src" + ], + "main": "./lib/cjs/index.js", + "module": "./lib/esm/index.js", + "types": "./lib/esm/index.d.ts", + "exports": { + ".": { + "import": "./lib/esm/index.js", + "require": "./lib/cjs/index.js", + "types": "./lib/esm/types/index.d.ts" + }, + "./utils": { + "import": "./lib/esm/utils/index.js", + "require": "./lib/cjs/utils/index.js", + "types": "./lib/esm/utils/index.d.ts" + } + }, + "scripts": { + "dev": "vite", + "build": "vite build && npm run build2", + "build2": "FORMAT=esm tsup && FORMAT=cjs tsup", +``` + +Note the top-level `"types": "./lib/esm/index.d.ts"` is **correct** — it is only +the one inside `exports` that points at the non-existent +`lib/esm/types/index.d.ts`. + +### What the build actually emits + +`packages/core/lib/esm/` contains: + +``` +createTinyIntl.d.ts createTinyIntl.js createTinyIntl.js.map +detectBrowserLocale.d.ts detectBrowserLocale.js detectBrowserLocale.js.map +detectLocale.d.ts detectLocale.js detectLocale.js.map +index.d.ts index.js index.js.map +package.json utils/ +vite-env.d.d.ts vite-env.d.js vite-env.d.js.map +``` + +`packages/core/lib/cjs/` is the same with `.d.cts` in place of `.d.ts` +(`index.d.cts`, `createTinyIntl.d.cts`, …) and a `package.json` containing +`{"type":"commonjs"}`. + +There is **no `lib/esm/types/` directory**. The three `vite-env.d.*` files are +the junk from the entry glob. + +### Excerpt — `packages/core/tsup.config.ts` (what really builds the package) + +```ts +const baseOutDir = 'lib'; +const format: Format = (process.env.FORMAT as 'cjs' | 'esm') || 'cjs'; + +const addPackageJson = () => { + const dir = path.resolve(baseOutDir); + writeFileSync( + path.resolve(dir, format, 'package.json'), + JSON.stringify({ type: format === 'cjs' ? 'commonjs' : 'module' }), + ); +}; + +export default defineConfig({ + entry: ['src'], + target: 'es2022', + format, + bundle: false, + sourcemap: true, + clean: true, + dts: true, + outDir: format === 'cjs' ? `${baseOutDir}/cjs` : `${baseOutDir}/esm`, + config: format === 'cjs' ? 'tsconfig.cjs.json' : 'tsconfig.json', + onSuccess: () => { addPackageJson(); /* ... */ }, + outExtension: () => ({ js: '.js' }), +}); +``` + +`entry: ['src']` is the glob that pulls in `src/vite-env.d.ts`. + +### Excerpt — `packages/core/vite.config.ts` (the dead build, plus the live test config) + +```ts +import { defineConfig } from 'vitest/config'; // eslint-disable-line import/no-unresolved + +export default defineConfig({ + plugins: [ + dts({ outDir: resolve(__dirname, 'dist', 'types'), insertTypesEntry: true }), + visualizer({ filename: 'dist/stats.html', gzipSize: true, brotliSize: true }), + ], + build: { + outDir: resolve(__dirname, 'dist'), + minify: false, + lib: { entry: resolve(__dirname, 'src', 'index.ts'), name: 'tinyIntl', fileName: 'index', + formats: ['es', 'cjs', 'umd'] }, + rollupOptions: { external: [], output: { globals: {} } }, + }, + test: {}, +}); +``` + +Everything above `test: {}` produces `dist/`, which `files: ["lib", "src"]` does +not publish. `test: {}` is what vitest reads and **must survive**. + +### One thing that looks wrong but is not + +`files` includes `"src"`. That is **deliberate and must stay**: tsup runs with +`sourcemap: true`, and the emitted `.js.map` files reference `../../src/*.ts`. +Shipping `src` is what makes those source maps resolve for consumers. Do not +"optimise" it away. + +### Conventions + +- Prettier: `singleQuote: true`, `printWidth: 100`, `trailingComma: "all"`. +- `package.json` files in this repo are 2-space indented with keys in the order + shown above; preserve that ordering rather than reformatting. + +## Commands you will need + +> Read from `package.json` during planning; `node_modules` was not installed at +> planning time, so none of these were executed — including the packaging +> checks in Step 2, which need network access to fetch the tools via `npx`. + +| Purpose | Command | Expected on success | +|---|---|---| +| Install | `npm install` | exit 0 | +| Build core | `npm run build --workspace @tiny-intl/core` | exit 0 | +| Publint | `npx --yes publint packages/core` | no errors | +| Types check | `npx --yes @arethetypeswrong/cli --pack packages/core` | no ✗ rows | +| Core tests | `npm run test --workspace @tiny-intl/core -- --run` | all pass | +| Lint | `npm run lint:all` | exit 0 | + +## Scope + +**In scope** (the only files you should modify): + +- `packages/core/package.json` +- `packages/core/vite.config.ts` +- `packages/core/tsup.config.ts` +- `packages/react/package.json`, `packages/preact/package.json` — condition + ordering + `sideEffects` only +- `package-lock.json` — changes when devDependencies are removed +- `plans/README.md` — status row only + +**Out of scope** (do NOT touch, even though they look related): + +- **The adapter packages' `vite.config.ts` files.** Their vite build is the + *real* build for those packages (`build: "tsc && vite build"` → `lib/`), + unlike core's. Only core's vite build is dead. +- `files: ["lib", "src"]` in core — `src` is required for source maps, see above. +- Package versions and anything under `lerna.json`. Releasing is a separate, + human-gated action (`npm run release` → `lerna publish`). +- Publishing. This plan changes packaging metadata; it does not publish. +- **`packages/solid-js/**` and the root `README.md`** — both are owned by + `plans/002-remove-solid-js-support.md`, which deletes that package and its + README entry. Do not add `sideEffects` or fix `exports` there; if the + directory still exists when you run, leave it alone. +- Adding `exports` subpaths that do not exist today. + +## Git workflow + +- Branch: `advisor/005-fix-core-package-exports` +- Conventional commits (commitlint enforced). Prior art in `git log`: + `feat: better esm exports`, `fix: built config`. + Suggested: `fix(core): point exports types at the emitted declaration files` + and `chore(core): drop the unused vite library build`. +- Do NOT push, publish, or open a PR unless the operator instructed it. + +## Steps + +### Step 1: Build, and confirm the declared types path really is missing + +```bash +npm install +npm run build --workspace @tiny-intl/core +ls packages/core/lib/esm/ +ls packages/core/lib/cjs/ +ls packages/core/lib/esm/types 2>&1 +``` + +**Verify**: `lib/esm/index.d.ts` and `lib/cjs/index.d.cts` exist; +`ls packages/core/lib/esm/types` reports "No such file or directory". If a +`types/` directory *does* exist, STOP — the build differs from what this plan +assumes. + +### Step 2: Measure the current packaging state with real tools + +```bash +npx --yes publint packages/core +npx --yes @arethetypeswrong/cli --pack packages/core +``` + +Record the full output of both. `@arethetypeswrong/cli` reports per-resolution +verdicts (node10, node16-cjs, node16-esm, bundler); expect failures on at least +the CJS rows. This output is your before/after evidence — you will re-run both +at the end. + +If both tools report a clean bill of health, STOP and report: the resolution +problem may be masked by fallbacks in a way this plan mis-diagnoses, and the +`exports` fix should then be judged on correctness-of-metadata alone rather than +on a user-visible failure. + +**Verify**: both commands ran and their output is recorded. (If `npx` cannot +reach the network, note that and continue — the `exports` map is still +demonstrably wrong per Step 1 — but say clearly in your report that the +packaging verification could not be performed.) + +### Step 3: Fix core's `exports` map + +Replace the `exports` block in `packages/core/package.json` with nested +conditions, `types` first in each: + +```json + "main": "./lib/cjs/index.js", + "module": "./lib/esm/index.js", + "types": "./lib/esm/index.d.ts", + "sideEffects": false, + "exports": { + ".": { + "import": { + "types": "./lib/esm/index.d.ts", + "default": "./lib/esm/index.js" + }, + "require": { + "types": "./lib/cjs/index.d.cts", + "default": "./lib/cjs/index.js" + } + }, + "./utils": { + "import": { + "types": "./lib/esm/utils/index.d.ts", + "default": "./lib/esm/utils/index.js" + }, + "require": { + "types": "./lib/cjs/utils/index.d.cts", + "default": "./lib/cjs/utils/index.js" + } + }, + "./package.json": "./package.json" + }, +``` + +Why each part: + +- `types` must be the **first** key inside each condition object — resolvers take + the first match, so a `types` entry listed after `import`/`require` is + unreachable. +- The ESM branch gets `.d.ts`, the CJS branch gets `.d.cts`, matching what tsup + actually emits (confirmed in Step 1). +- `"./package.json"` is exported because tooling routinely reads it, and an + `exports` map without it blocks that. +- `sideEffects: false` tells bundlers every module is safe to drop when unused. + This package is pure functions; the claim is true. It is what makes the bundle + size badge in the README honest for partial imports. + +Confirm each of the six declared paths exists: + +```bash +for f in lib/esm/index.d.ts lib/esm/index.js lib/cjs/index.d.cts lib/cjs/index.js \ + lib/esm/utils/index.d.ts lib/esm/utils/index.js \ + lib/cjs/utils/index.d.cts lib/cjs/utils/index.js; do + test -f "packages/core/$f" && echo "OK $f" || echo "MISSING $f" +done +``` + +**Verify**: every line prints `OK`. Any `MISSING` line is a STOP condition — the +map must describe files that exist. + +### Step 4: Stop emitting `vite-env.d.*` into the published package + +In `packages/core/tsup.config.ts`, narrow the entry glob so declaration files +are not treated as entry points: + +```ts + entry: ['src/**/*.ts', '!src/**/*.d.ts'], +``` + +Rebuild and confirm the junk is gone: + +```bash +npm run build --workspace @tiny-intl/core +ls packages/core/lib/esm/ packages/core/lib/cjs/ | grep vite-env +``` + +**Verify**: the `grep` produces no output, and `lib/esm/index.js`, +`lib/esm/utils/index.js` and their `.d.ts` counterparts all still exist (re-run +the Step 3 file-existence loop). + +If the negation glob is not supported by the installed tsup version, fall back +to listing entries explicitly: +`entry: ['src/index.ts', 'src/createTinyIntl.ts', 'src/detectLocale.ts', 'src/detectBrowserLocale.ts', 'src/utils/index.ts', 'src/utils/flattie.ts', 'src/utils/relativeTimeFormat.ts']` +— and say in your report which form you used. + +### Step 5: Delete the dead vite build from core + +5a. In `packages/core/package.json`, collapse the two build scripts: + +```json + "build": "npm run build:esm && npm run build:cjs", + "build:esm": "FORMAT=esm tsup", + "build:cjs": "FORMAT=cjs tsup", +``` + +(removing `"build2"` and the leading `vite build`). Leave `dev`, `preview`, +`test` and `coverage` alone. + +5b. In `packages/core/vite.config.ts`, remove the `plugins` and `build` keys and +their now-unused imports, keeping **only** the vitest configuration: + +```ts +import { defineConfig } from 'vitest/config'; // eslint-disable-line import/no-unresolved + +export default defineConfig({ + test: {}, +}); +``` + +5c. Remove the two devDependencies that existed only for that build: + +```bash +npm uninstall --workspace @tiny-intl/core rollup-plugin-visualizer vite-plugin-dts +``` + +Keep `vite` itself — vitest 0.34 depends on it and `dev`/`preview` still use it. + +5d. Delete the stale output directory (it is git-ignored, so this only affects +your working tree): `rm -rf packages/core/dist`. + +**Verify**: + +```bash +npm run build --workspace @tiny-intl/core +test -d packages/core/dist && echo "dist recreated — unexpected" || echo "no dist — correct" +npm run test --workspace @tiny-intl/core -- --run +``` +→ build exits 0, `no dist — correct`, all core tests pass (this proves the +trimmed `vite.config.ts` still configures vitest). + +### Step 6: Fix the condition ordering in the adapter packages + +Both `packages/react/package.json` and `packages/preact/package.json` have the +same `types`-last problem. Their paths do exist +(`./lib/types/index.d.ts` is produced by `vite-plugin-dts`), so this is metadata +correctness rather than a live breakage. For each, replace the `exports` block +and add `sideEffects`: + +```json + "sideEffects": false, + "exports": { + ".": { + "types": "./lib/types/index.d.ts", + "import": "./lib/index.js", + "require": "./lib/index.umd.cjs" + }, + "./package.json": "./package.json" + }, +``` + +Leave `main`, `module`, `types`, `files` and everything else untouched. These +packages emit a single `.d.ts` set for both formats, so the flat form with +`types` first is correct and no nesting is needed. + +**Verify**: for each adapter, + +```bash +npm run build --workspace @tiny-intl/react +npx --yes publint packages/react +``` +→ build exits 0; publint reports no errors. Repeat for preact. +(If an adapter build fails inside `Translate.tsx`, that is the pre-existing +failure owned by plan 003 — note it and move on; it is not caused by this plan.) + +Do **not** apply this to `packages/solid-js` even if the directory is still +present — plan 002 deletes it. + +### Step 7: Re-run the packaging checks + +```bash +npm run build --workspace @tiny-intl/core +npx --yes publint packages/core +npx --yes @arethetypeswrong/cli --pack packages/core +npm test +npm run lint:all +``` + +**Verify**: both packaging tools report strictly better results than the Step 2 +baseline — specifically, no error about a missing or unresolvable types entry. +All tests pass. Lint exits 0. Include the before/after tool output in your +report. + +(If `npm test` fails inside a react or preact `Translate` test, check whether +plan 003 has landed — that is its territory, not this plan's.) + +## Test plan + +This plan is not covered by unit tests — it changes packaging metadata, which +the vitest suite (importing from `../src`) cannot observe. Verification is +therefore tool-based, and that is the point of Steps 2 and 8: + +- `publint` — validates `exports`, `main`, `module`, `types`, file existence and + format/extension agreement. +- `@arethetypeswrong/cli --pack` — resolves the packed tarball as node10, + node16-cjs, node16-esm and bundler consumers would, and reports which + resolutions find types. +- The existing core suite must still pass, which proves the trimmed + `vite.config.ts` still supplies vitest's configuration (Step 5). +- The per-path `test -f` loop in Step 3 is the machine-checkable guard that the + `exports` map only names files the build actually produces. + +Do not add unit tests for this plan. If you want a lasting guard, the right one +is adding `npx publint` to the CI workflow — note that as a follow-up rather +than doing it here (CI is plan 001's file). + +## Done criteria + +ALL must hold: + +- [ ] `grep -n "lib/esm/types" packages/core/package.json` → no output +- [ ] `node -e "const e=require('./packages/core/package.json').exports; for (const k of ['.','./utils']) for (const c of ['import','require']) { const p=e[k][c].types; require('fs').accessSync('packages/core/'+p); } console.log('all exports types paths exist')"` → prints the message, exits 0 +- [ ] `npm run build --workspace @tiny-intl/core` exits 0 +- [ ] `ls packages/core/lib/esm packages/core/lib/cjs | grep vite-env` → no output +- [ ] `test -d packages/core/dist` → false after a build +- [ ] `grep -n "vite build" packages/core/package.json` → no output +- [ ] `grep -l '"sideEffects": false' packages/core/package.json packages/react/package.json packages/preact/package.json` → three files +- [ ] `npx --yes publint packages/core` → no errors +- [ ] `npx --yes @arethetypeswrong/cli --pack packages/core` → strictly better than the Step 2 baseline, with no missing-types errors +- [ ] `npm test` exits 0 +- [ ] `npm run lint:all` exits 0 +- [ ] `git status --porcelain packages/solid-js README.md` returns no output (both are plan 002's) +- [ ] `git status --porcelain` lists only files from the In-scope list +- [ ] `plans/README.md` status row for 005 updated + +## STOP conditions + +Stop and report back (do not improvise) if: + +- `packages/core/lib/esm/types/` **does** exist after a clean build — the build + differs from what this plan measured, and the `exports` map may be correct. +- Any path in the new `exports` map does not exist after a build (the Step 3 + file-existence loop prints `MISSING`). +- `npm uninstall` of `vite-plugin-dts` or `rollup-plugin-visualizer` breaks a + build in *any* package — the adapters use both, and if the monorepo relied on + hoisting from core's devDependencies, they must be added to the adapters' + own devDependencies instead. Report before doing that. +- Trimming `packages/core/vite.config.ts` stops vitest from running. Restore the + file and report; the fix is to move the config to `vitest.config.ts` instead, + which is a different change than this plan describes. +- You conclude the fix requires changing package `version` fields, or running + `npm publish` / `lerna publish`. It does not — publishing is a separate, + human-gated action. +- `@arethetypeswrong/cli` reports *worse* results after the change than before. + +## Maintenance notes + +- **Follow-up worth doing**: add `npx publint` (and optionally + `attw --pack`) as a CI step so a future edit to `exports` cannot silently + point at a non-existent file again. That belongs in + `.github/workflows/unit-tests.yml`, which plan 001 rewrites — sequence this + after 001. +- **This change ships on the next release, not immediately.** `exports` changes + affect consumers only once published. Whoever runs `npm run release` should + treat this as a patch that fixes type resolution and mention it in the + release notes. +- **`files: ["lib", "src"]` must keep `src`** — source maps reference it. If + someone later removes `sourcemap: true` from `tsup.config.ts`, `src` can go + too, and the npm tarball roughly halves. +- **What a reviewer should scrutinise**: that `types` is the first key in every + condition object; that the CJS branch points at `.d.cts` and the ESM branch at + `.d.ts`; and that `sideEffects: false` is actually true for these packages + (it is — every module is pure function/type declarations, with no top-level + side effects). +- The adapter packages still build with vite and emit a `dist/stats.html` from + `rollup-plugin-visualizer` into a directory that is not published. That is + harmless there (unlike core, whose *entire* vite output was dead) and is left + alone deliberately. +- If plan 002 is rejected and `packages/solid-js` survives, it needs the same + Step 6 treatment (`types` first, `sideEffects: false`) — it has the identical + `exports` shape as the other two adapters. diff --git a/plans/006-fix-preact-missing-useintl-types.md b/plans/006-fix-preact-missing-useintl-types.md new file mode 100644 index 0000000..bf80701 --- /dev/null +++ b/plans/006-fix-preact-missing-useintl-types.md @@ -0,0 +1,342 @@ +# Plan 006: `@tiny-intl/preact` ships a type entry point that re-exports a file it never emits + +> **Executor instructions**: Follow this plan step by step. Run every +> verification command and confirm the expected result before moving on. Touch +> only the files listed as in scope. If any STOP condition occurs, stop and +> report — do not improvise. +> +> **This plan may turn out to be a no-op.** Step 1 decides. Read it first. +> +> **Drift check (run first)**: +> `git diff --stat 794eb48..HEAD -- packages/preact packages/core/package.json` + +## Status + +- **Priority**: P1 (published, user-facing, silent) +- **Effort**: S +- **Risk**: LOW +- **Depends on**: `plans/005-fix-core-package-exports.md` — see Step 1 +- **Category**: bug / packaging +- **Planned at**: commit `794eb48`, observed 2026-08-12 while reviewing plan 002 + +## Why this matters + +`packages/preact/lib/types/index.d.ts` — the file `@tiny-intl/preact` names in +both its `types` field and its `exports` map — contains: + +```ts +export * from './Translate'; +export * from './useIntl'; +``` + +But `useIntl.d.ts` **is not emitted**. The directory after a clean build holds +only `Translate.d.ts` and `index.d.ts`. So the package's public type entry point +re-exports a module that does not exist in the tarball. + +Every TypeScript consumer of `@tiny-intl/preact` is affected: `useIntl` — the +package's primary API — has no type declaration, and depending on the consumer's +`moduleResolution` and `skipLibCheck` settings they get either an implicit +`any`, an unresolved-module error on the re-export, or a broken entry point. +`@tiny-intl/react` emits all three files correctly, so this is preact-only and +easy to miss. + +The build does not fail. `vite-plugin-dts` prints six `TS2742` diagnostics, +declines to emit the file, and exits 0. Nothing in CI looks at that output. + +### Measured evidence (2026-08-12) + +``` +$ npm run build --workspace @tiny-intl/preact # exit 0 +src/useIntl.tsx:8:17 - error TS2742: The inferred type of 'useIntl' cannot be +named without a reference to '../../../node_modules/@tiny-intl/core/lib/esm/createTinyIntl'. +This is likely not portable. A type annotation is necessary. + (×6) + +$ ls packages/react/lib/types/ → Translate.d.ts index.d.ts useIntl.d.ts +$ ls packages/preact/lib/types/ → Translate.d.ts index.d.ts +``` + +### Root cause, and why it is probably plan 005's to fix + +`packages/react/tsconfig.json` uses `"moduleResolution": "Node"`; +`packages/preact/tsconfig.json` uses `"moduleResolution": "bundler"`. That is the +only material difference between two otherwise byte-identical `useIntl.tsx` +files. + +`"Node"` (node10) ignores a package's `exports` map entirely and reads the +top-level `"types": "./lib/esm/index.d.ts"` from `@tiny-intl/core` — which is +correct, so react resolves `TinyIntl` through the package entry and can name it. + +`"bundler"` **honours** `exports`. And `@tiny-intl/core`'s exports map declares: + +```json +"types": "./lib/esm/types/index.d.ts" +``` + +That path does not exist — it is exactly the defect plan 005 fixes. With the +declared types path unresolvable, TypeScript falls back to reaching the symbol +through the deep internal path `…/lib/esm/createTinyIntl`, which it cannot write +into a portable `.d.ts` — hence `TS2742`, hence no emit. + +**That hypothesis was tested on 2026-08-12 and DISPROVEN.** Plan 005 landed — +core's `exports` map now declares `types` first in every condition, pointing at +files that exist, and `@arethetypeswrong/cli` reports 🟢 for node16-CJS, +node16-ESM and bundler. The preact build was then re-run from scratch: + +``` +$ npm run build --workspace @tiny-intl/preact # exit 0 +TS2742 count: 6 # unchanged +$ ls packages/preact/lib/types/ +Translate.d.ts index.d.ts # useIntl.d.ts still missing +$ ls packages/react/lib/types/ +Translate.d.ts index.d.ts useIntl.d.ts # react still fine +``` + +So the broken `exports` map was **not** the cause. Do not spend time re-testing +it — go straight to the fix in Step 2, which addresses `TS2742` directly and +independently of root cause (the diagnostic itself says "A type annotation is +necessary"). + +Two candidate causes remain, neither yet confirmed, both only worth chasing if +Step 2 fails: + +1. **`moduleResolution` split** — preact `"bundler"`, react `"Node"`. Still the + most plausible: the two files are otherwise byte-identical below their import + lines. Step 3 covers this as a fallback. +2. **`vite-plugin-dts` version skew** — preact resolves its own `^3.6.4` (3.6.4 + installed) while react uses the hoisted pinned `3.6.0`. Worth testing by + temporarily aligning them if Steps 2 and 3 both fail. + +Why the annotation fix works regardless: `TS2742` fires when TypeScript must +*write down* an inferred type but cannot construct a portable path to the +symbol's declaring module. `useIntl`'s return type is inferred from a spread of +`TinyIntl` plus eight wrapped callbacks. Annotate the return explicitly +with a type that is already imported by name, and there is nothing left to +infer. + +## Current state + +- `packages/preact/tsconfig.json` — `"moduleResolution": "bundler"`, + `"target": "ES2020"`, `paths` aliasing `react`/`react-dom` to `preact/compat`. +- `packages/preact/src/useIntl.tsx:8` — `export function useIntl() {` with an + **inferred** return type. The inferred type spreads `intl` + (`TinyIntl`, imported as a type on line 2) plus eight locally wrapped + callbacks. +- `packages/preact/vite.config.ts` — `dts({ outDir: resolve(__dirname, 'lib', 'types'), insertTypesEntry: true })`. +- `packages/react/src/useIntl.tsx` — identical source, different import + specifier (`'react'` vs `'preact/compat'`), emits fine. + +### Conventions + +- Prettier: `singleQuote: true`, `printWidth: 100`, `trailingComma: "all"`; + 2-space indent. +- Commit scopes allowed by commitlint: `core`, `react`, `preact`. + +## Commands you will need + +| Purpose | Command | Expected on success | +|---|---|---| +| Install | `npm install` | exit 0 | +| Build preact | `npm run build --workspace @tiny-intl/preact` | exit 0 | +| Inspect emitted types | `ls packages/preact/lib/types/` | 3 files incl. `useIntl.d.ts` | +| Count TS2742 | `npm run build --workspace @tiny-intl/preact 2>&1 \| grep -c TS2742` | `0` | +| Tests | `npm test` | all pass | +| Lint | `npm run lint:all` | exit 0 | + +## Scope + +**In scope**: + +- `packages/preact/src/useIntl.tsx` — an explicit return type annotation (Step 2) +- `packages/preact/tsconfig.json` — only as the documented fallback in Step 3 +- `packages/preact/package.json` + `package-lock.json` — only as the second + fallback in Step 3b + +**Out of scope**: + +- `packages/core/**` — the `exports` map is plan 005's; do not edit it here. +- `packages/react/**` — emits correctly; leave it alone. +- Changing what `useIntl` returns at runtime. This is a types-only change; the + emitted JavaScript must be byte-identical. +- `packages/preact/src/Translate.tsx` and all test files. + +## Git workflow + +- Conventional commits. Suggested: `fix(preact): emit type declarations for useIntl`. +- Do NOT push or open a PR. + +## Steps + +### Step 1: Confirm the bug still reproduces on a clean build + +Plan 005 must have landed in your branch. Confirm, then reproduce: + +```bash +grep -n "lib/esm/types" packages/core/package.json # expect: no output +rm -rf packages/preact/lib packages/core/lib +npm run build --workspace @tiny-intl/core +npm run build --workspace @tiny-intl/preact 2>&1 | tee /tmp/preact-build.txt +grep -c TS2742 /tmp/preact-build.txt +ls packages/preact/lib/types/ +``` + +If the first grep still finds `lib/esm/types`, plan 005 has NOT landed — +**STOP and report**. + +**Expected** (measured 2026-08-12): `TS2742` count `6`, and +`packages/preact/lib/types/` contains only `Translate.d.ts` and `index.d.ts`. +Record both. This is the "before" evidence. + +If instead the count is 0 and `useIntl.d.ts` is present, the bug has been fixed +by something else since — **STOP and report**; do not make changes the repo no +longer needs. + +**Verify**: you have the `TS2742` count and the directory listing recorded. + +### Step 2: Annotate `useIntl`'s return type explicitly + +`TS2742` means TypeScript cannot *write down* the inferred type without +referencing a module it has no portable path to. The direct fix is to stop +relying on inference: declare the return type using only types that are already +imported by name. + +In `packages/preact/src/useIntl.tsx`, add an exported interface describing the +return value and annotate the function with it. The shape must match what the +function actually returns today — spread of `TinyIntl`, with `t`, `tc`, +`n`, `d`, `dt`, `rt`, `sort`, `collator`, `list` and `change` overridden. Derive +it from the already-imported `TinyIntl` type rather than restating each +signature, e.g.: + +```tsx +export type UseIntlResult = TinyIntl; + +export function useIntl(): UseIntlResult { +``` + +Then check that against reality: the function returns `{ ...intl, t, tc, n, +d: dt, dt, rt, sort, collator, list, change: intl.change }`. If every one of +those wrapped values has the same type as the corresponding member of +`TinyIntl` (they are `useCallback` wrappers around exactly those +members, so they should), `TinyIntl` is the honest annotation and no +new interface is needed. + +**If the annotation does not typecheck** — i.e. the returned object is not +assignable to `TinyIntl` — do not force it with a cast. That would mean +the wrappers genuinely change the type, which is information your reviewer +needs. STOP and report the compiler error. + +**Verify**: + +```bash +cd packages/preact && npx tsc --noEmit; echo "tsc exit=$?"; cd ../.. +npm run build --workspace @tiny-intl/preact 2>&1 | grep -c TS2742 # expect 0 +ls packages/preact/lib/types/ # expect 3 files +``` + +### Step 3: Fallback only — align `moduleResolution` with react + +**Only if Step 2 fails to eliminate `TS2742`.** Change +`packages/preact/tsconfig.json`'s `"moduleResolution": "bundler"` to `"Node"`, +matching `packages/react/tsconfig.json`, and remove +`"allowImportingTsExtensions": true` if TypeScript then rejects it (that option +requires `bundler`/`node16`+ resolution and `noEmit`). + +This is a fallback, not the preferred fix: `bundler` is the more modern and more +correct setting, and moving away from it trades a newer resolution mode for a +declaration file. Take it only if Step 2 has failed, revert it if it does not +help, and say so prominently in your report either way. + +**Verify**: same three commands as Step 2. + +### Step 3b: Second fallback — test the plugin version skew + +**Only if Steps 2 and 3 both fail.** `packages/preact` resolves its own +`vite-plugin-dts@3.6.4`; `packages/react` uses the hoisted pinned `3.6.0`. +Temporarily align preact to `3.6.0`: + +```bash +npm install -D --workspace @tiny-intl/preact vite-plugin-dts@3.6.0 +npm run build --workspace @tiny-intl/preact 2>&1 | grep -c TS2742 +ls packages/preact/lib/types/ +``` + +If that emits `useIntl.d.ts`, the plugin version was the cause — keep the change +(pinning both adapters to the same version is desirable anyway, see Maintenance +notes) and revert whatever Step 3 changed. If it does not help, revert this too +and **STOP**: three hypotheses exhausted means the diagnosis needs a human. + +**Verify**: report which of Steps 2 / 3 / 3b actually fixed it, and confirm you +reverted the ones that did not. + +### Step 4: Confirm the runtime output did not change + +This is a types-only plan. Prove the emitted JavaScript is unaffected: + +```bash +git diff --stat -- packages/preact/src/useIntl.tsx +npm test +npm run lint:all +``` + +**Verify**: the source diff touches only type annotations (no change to any +expression or statement); all tests pass; lint exits 0. + +## Test plan + +The regression guard here is a build artifact, not a unit test — the failure +mode is "a file is missing from the package", which no runtime test can see. + +- **Primary check**: `ls packages/preact/lib/types/` contains `useIntl.d.ts`. +- **Primary check**: the preact build emits zero `TS2742` diagnostics. +- **Consumer-level check** (do this if plan 005's tooling is available): + `npx --yes @arethetypeswrong/cli --pack packages/preact` — it resolves the + packed tarball the way a consumer would and will flag an unresolvable + re-export. +- The existing preact test suite (added by plan 001) must still pass unchanged; + it proves the runtime behaviour is untouched. + +Do not add a unit test for this. If you want a lasting guard, the right one is a +CI assertion that the build emits no `TS2742` — noted as a follow-up in plan 001. + +## Done criteria + +ALL must hold (or Step 1 concluded no-op, in which case only the first two): + +- [ ] `ls packages/preact/lib/types/` lists `useIntl.d.ts`, `Translate.d.ts`, `index.d.ts` +- [ ] `npm run build --workspace @tiny-intl/preact 2>&1 | grep -c TS2742` → `0` +- [ ] `cd packages/preact && npx tsc --noEmit` exits 0 +- [ ] `npm test` exits 0 +- [ ] `npm run lint:all` exits 0 +- [ ] `git status --porcelain` lists only in-scope files +- [ ] The report states which Step 1 outcome occurred + +## STOP conditions + +Stop and report back (do not improvise) if: + +- `grep -n "lib/esm/types" packages/core/package.json` still matches — plan 005 + has not landed and this plan's Step 1 cannot run. +- The returned object is not assignable to `TinyIntl` (Step 2). Report + the compiler error; do not cast around it. +- Eliminating `TS2742` appears to require editing `packages/core/**`. That is + plan 005's territory — report what you found. +- The preact build starts *failing* (non-zero exit) at any point. It exits 0 + today; a regression to failure is worse than the bug. + +## Maintenance notes + +- **How this stayed hidden**: `vite-plugin-dts` prints `TS2742` as an "error" + but does not fail the build, and CI never built the adapters at all. The + durable fix is a CI step that fails on `TS2742` — cheap, and it would have + caught this the day it appeared. +- **Version skew worth resolving separately**: `packages/react` pins + `vite-plugin-dts` to `3.6.0` while `packages/preact` floats on `^3.6.4`, so + the two adapters generate declarations with different plugin versions. Whether + or not that contributed here, having two adapters on two versions of the same + build plugin is a latent source of exactly this kind of asymmetry. Align them. +- **What a reviewer should scrutinise**: that the diff is annotations only — the + compiled JS must be identical — and that `UseIntlResult` (if introduced) is + actually exported, or consumers hit `TS4023` on the re-export instead. +- If Step 1 reported no-op, keep this plan file: it documents why the preact + declaration output is worth watching, and the CI guard in the first bullet is + still worth adding. diff --git a/plans/007-recover-bundle-size-regression.md b/plans/007-recover-bundle-size-regression.md new file mode 100644 index 0000000..fc89602 --- /dev/null +++ b/plans/007-recover-bundle-size-regression.md @@ -0,0 +1,390 @@ +# Plan 007: Recover the adapter bundle-size regression, and stop shipping two dead bundles + +> **Executor instructions**: Follow this plan step by step. Run every +> verification command and confirm the expected result before moving on. Touch +> only the files listed as in scope. If any STOP condition occurs, stop and +> report — do not improvise. +> +> **Drift check (run first)**: +> `git diff --stat 49bd556..HEAD -- packages/react packages/preact` + +## Status + +- **Priority**: P1 (this project's entire value proposition is size) +- **Effort**: S +- **Risk**: LOW-MED — Step 2 changes runtime guards that a regression test suite + already pins; Step 3 changes what the `require` entry resolves to. +- **Depends on**: plans 001–006 (all merged into `main` at `49bd556`) +- **Category**: perf / packaging +- **Planned at**: commit `49bd556`, 2026-08-12 + +## Why this matters + +`tiny-intl` is sold on bundle size — every README leads with a bundlejs badge. +Plan 003 fixed a real bug (`` rendered the singular, +`number={0}` rendered nothing) but paid for it in bytes, because +`typeof count === 'number'` is far longer than `count`. + +Measured with esbuild (`bundle: true, minify: true, format: 'esm'`, framework +externalised) then gzip level 9 — a methodology that **reproduces the published +badge numbers exactly** for both adapters: + +| package | before (`794eb48`) | after (`49bd556`) | delta | +|---|---|---|---| +| `@tiny-intl/core` | 1418 B | 1422 B | +4 B | +| `@tiny-intl/react` | **541 B** | 558 B | **+17 B** | +| `@tiny-intl/preact` | **727 B** | 741 B | **+14 B** | + +Core's +4 B is gzip noise (its minified size actually *fell*, 3116 → 3102 B). +The adapter regressions are real and are the target here. + +There is also dead weight in the published tarballs. Both adapters build +`formats: ['es', 'cjs', 'umd']`, but `package.json` references only +`./lib/index.js` (import) and `./lib/index.umd.cjs` (require). So: + +- `lib/index.cjs` — **3039 B in react, 1571 B in preact — is referenced by + nothing** and ships in every install. +- The `require` entry points at the **larger** of the two CommonJS-capable + files (react: UMD 3588 B vs CJS 3039 B). +- Pointing `require` at a UMD bundle is also why + `attw --pack packages/preact` reports 👺 "Masquerading as ESM" on the node16 + rows — a known outstanding defect. **Fixing the size issue fixes that too.** + +## Current state + +### The guards to shrink — `packages/react/src/Translate.tsx:65-87` + +```tsx + const translateFn = useCallback(() => { + if (typeof count === 'number') { + return tc(name, count, options); + } + + if (date !== undefined && !relative) { + return dt(date, options); + } + + if (date !== undefined && relative) { + return rt(date, options); + } + + if (typeof number === 'number') { + return n(number, options); + } + + if (name) { + return t(name, options); + } + + return null; + }, [changed, count, date, name, number, options, relative]); +``` + +`packages/preact/src/Translate.tsx` is byte-identical below line 8. + +The prop types (`TranslateProps`, same file) declare `count?: number`, +`number?: number`, `date?: Date | string | number` — and, in the union members +where each is absent, the unit type `undefined`. So the only non-value these can +hold is `undefined`, which is what makes the shorter form below equivalent. + +### The build config — `packages/react/vite.config.ts` / `packages/preact/vite.config.ts` + +```ts + build: { + outDir: resolve(__dirname, 'lib'), + lib: { + entry: resolve(__dirname, 'src', 'index.ts'), + name: 'tinyIntl', + fileName: 'index', + formats: ['es', 'cjs', 'umd'], + }, +``` + +Both files also carry a vitest `test` block added by plan 001 — **leave it +alone**. + +### The manifests — `packages/react/package.json` / `packages/preact/package.json` + +```json + "main": "./lib/index.umd.cjs", + "module": "./lib/index.js", + "types": "./lib/types/index.d.ts", + "sideEffects": false, + "exports": { + ".": { + "types": "./lib/types/index.d.ts", + "import": "./lib/index.js", + "require": "./lib/index.umd.cjs" + }, + "./package.json": "./package.json" + }, +``` + +### Conventions + +- Prettier: `singleQuote: true`, `printWidth: 100`, `trailingComma: "all"`. +- Commit scopes allowed by commitlint: `core`, `react`, `preact`. +- The suite is 52 tests (core 24, react 14, preact 14). `npm test` must stay green. + +## Commands you will need + +| Purpose | Command | Expected | +|---|---|---| +| Install | `npm ci` | exit 0 | +| Build one package | `npm run build --workspace @tiny-intl/react` | exit 0 | +| Typecheck | `cd packages/react && npx tsc --noEmit` | exit 0 | +| Tests | `npm test` | 52 pass | +| Lint | `npm run lint:all` | exit 0 | +| Measure | `node measure.mjs` (Step 1) | prints 3 rows | + +## Scope + +**In scope**: + +- `packages/react/src/Translate.tsx`, `packages/preact/src/Translate.tsx` +- `packages/react/vite.config.ts`, `packages/preact/vite.config.ts` — the + `formats` array only +- `packages/react/package.json`, `packages/preact/package.json` — `main` and the + `require` condition only + +**Out of scope**: + +- `packages/core/**` — its +4 B is gzip noise, not a regression worth chasing. +- The `test` blocks in the adapter `vite.config.ts` files. +- Any test file. The existing regression tests are the safety net; **do not edit + them to accommodate a change.** +- `TranslateProps` itself. +- Core's `files: ["lib", "src"]` and its sourcemaps. That is a separate, + larger tarball-size decision the maintainer has not yet made. +- Publishing, versions, `lerna.json`. + +## Git workflow + +- Conventional commits. Suggested: `perf(react): shrink Translate guards`, + `perf(preact): shrink Translate guards`, `fix(react): point require at the CJS build`. +- Do NOT push or open a PR. + +## Steps + +### Step 1: Set up the measurement harness and record the baseline + +Create `measure.mjs` in the repo root (delete it before you commit — it must not +end up in the diff): + +```js +import { createRequire } from 'node:module'; +import { gzipSync } from 'node:zlib'; +const require = createRequire(import.meta.url); +const { build } = require('esbuild'); +const targets = [ + { name: '@tiny-intl/core', entry: 'packages/core/lib/esm/index.js', external: [] }, + { name: '@tiny-intl/react', entry: 'packages/react/lib/index.js', external: ['react','react-dom','react/jsx-runtime'] }, + { name: '@tiny-intl/preact', entry: 'packages/preact/lib/index.js', external: ['preact','preact/compat','preact/jsx-runtime'] }, +]; +for (const t of targets) { + const r = await build({ entryPoints: [t.entry], bundle: true, minify: true, format: 'esm', + treeShaking: true, external: t.external, write: false, legalComments: 'none' }); + const out = r.outputFiles[0].contents; + console.log(t.name.padEnd(20) + ' min=' + String(out.length).padStart(5) + + 'B min+gzip=' + String(gzipSync(out, { level: 9 }).length).padStart(4) + 'B'); +} +``` + +```bash +npm ci +for p in core react preact; do npm run build --workspace @tiny-intl/$p; done +node measure.mjs +``` + +**Expected baseline** (measured 2026-08-12 at `49bd556`): core `1422B`, +react `558B`, preact `741B`. Record what you actually get. + +If your numbers differ by more than ~5 B from those, **STOP and report** — your +esbuild version differs and the before/after comparison below will not be +meaningful. + +### Step 2: Shrink the guards + +In **both** `packages/react/src/Translate.tsx` and +`packages/preact/src/Translate.tsx`, replace the four guards: + +| from | to | +|---|---| +| `typeof count === 'number'` | `count != null` | +| `date !== undefined && !relative` | `date != null && !relative` | +| `date !== undefined && relative` | `date != null && relative` | +| `typeof number === 'number'` | `number != null` | + +Why this is equivalent and not a regression of plan 003's fix: each of these +props is typed as `T | undefined`, so `!= null` excludes exactly the same values +`typeof`/`!== undefined` did — while still being **true for `0`**, which is the +entire point of plan 003. It is also marginally more robust: a plain-JS consumer +passing `null` now falls through instead of being formatted as a number. + +`!= null` (rather than `!== undefined`) is deliberate — it is the shortest form +that survives minification, and it is why this recovers bytes. + +**If ESLint's `eqeqeq` rule rejects `!= null`**, do not add a disable comment. +Fall back to `!== undefined` for all four (still shorter than `typeof`), record +that you did, and continue. + +Change nothing else — not the branch order, not the `if (name)` guard, not the +dependency array. + +**Verify**: + +```bash +cd packages/react && npx tsc --noEmit; echo "react tsc=$?"; cd ../.. +cd packages/preact && npx tsc --noEmit; echo "preact tsc=$?"; cd ../.. +diff <(tail -n +8 packages/react/src/Translate.tsx) <(tail -n +8 packages/preact/src/Translate.tsx) +npm test +``` + +→ both `tsc=0`; the `diff` produces no output (adapters stay identical); +**52 tests pass**. The `count={0}` / `number={0}` / `date={0}` regression tests +from plan 003 passing is the proof this did not undo that fix. + +If `tsc` now errors, `!= null` failed to narrow the props union — **STOP and +report the exact error**; do not paper over it with an assertion. + +### Step 3: Stop building and shipping the UMD bundle + +In both `packages/react/vite.config.ts` and `packages/preact/vite.config.ts`, +change the formats array: + +```ts + formats: ['es', 'cjs'], +``` + +Then in both `package.json` files, point the CommonJS entry at the real CJS +build instead of the UMD one: + +```json + "main": "./lib/index.cjs", +``` + +```json + "require": "./lib/index.cjs" +``` + +Leave `module`, `types`, `sideEffects`, `files` and the `./package.json` export +untouched. + +Rationale to defend in review: `index.cjs` was already being built and shipped +while nothing referenced it; `index.umd.cjs` was the larger file *and* the cause +of `attw`'s "Masquerading as ESM" finding. A UMD build exists to be loaded from a +`