@dloizides/frontend-devtools
v1.2.0
Published
Shared RN-web dev tooling for the dloizides.com SaaS frontends: the 17-rule custom ESLint plugin pack (also at the /eslint subpath) and the parameterized initI18n() i18next + react-i18next init factory. Owned shape erevna-web / katalogos-web / kefi-web sh
Maintainers
Readme
@dloizides/frontend-devtools
Shared RN-web dev tooling for the dloizides.com SaaS frontends. Extracts two
product-agnostic concerns that were duplicated byte-for-byte across
erevna-web, katalogos-web, and kefi-web:
- The custom ESLint rule pack (17 rules) — previously 17 identical
eslint-plugins/*.mjsfiles in each app. initI18n()— the parameterized i18next + react-i18next init factory — previously an identicalsrc/localization/i18n.tsin each app.
Decoupling: the package owns the logic (rule implementations + the i18next
.use().init()chain). Each app owns its config — which rules it enables and with what options, plus its ownlocales/*.jsonstrings. This package never imports a product.
Install
npm install -D @dloizides/frontend-devtoolseslint, i18next, and react-i18next are optional peer dependencies — apps
that only use one half don't need the other's peers.
ESLint rule pack
Import from the root or the /eslint subpath. Three shapes are exported:
| Export | Use when |
| --- | --- |
| plugin | You want one namespaced plugin, e.g. '@dloizides/no-null-check'. |
| rules | You want the raw Record<name, rule> map. |
| legacyPlugins | Migration: keep the exact <name>/<name> keys the apps already use. |
Migrating the apps (zero config-key churn)
The apps registered each rule under a namespace equal to its name
('no-null-check/no-null-check', 'smart-max-lines/smart-max-lines', …), and
inline eslint-disable-next-line no-null-check/no-null-check comments depend on
that. Spread legacyPlugins to preserve every key:
// eslint.config.mjs
import { legacyPlugins } from '@dloizides/frontend-devtools/eslint';
export default [
{
files: ['**/*.ts', '**/*.tsx'],
plugins: { ...legacyPlugins },
rules: {
'no-null-check/no-null-check': 'error',
'smart-max-lines/smart-max-lines': ['error', { functionMax: 50 }],
// ...existing keys unchanged
},
},
];Two rules worth knowing the exact semantics of
smart-max-lines — functionWarn is NOT a warning tier
ESLint has no per-report severity: every context.report() a rule makes is
emitted at the severity the rule is configured with. A rule cannot warn on one
line and error on another.
So ['error', { functionWarn: 30 }] does not give you "warn at 30, error at
50" — it gives you a hard error at 30, silently overriding the documented
50-line limit. That mismatch is why functionWarn is now opt-in and off by
default (since 1.1.0). Out of the box the only threshold is functionMax:
| Function kind | Enforced limit |
| --- | --- |
| React component (returns JSX) | componentMax, default 200 |
| Regular function | functionMax, default 50 |
Set functionWarn only if you deliberately want a second, stricter hard
threshold — it reports at the rule's own severity, not at 'warn'.
no-null-check — the autofix brings its own import
The fix rewrites x === undefined → !isValueDefined(x). That is only valid
source if isValueDefined is in scope, so the fixer adds the import when the
file lacks it:
'no-null-check/no-null-check': ['error', { utilImportPath: '@dloizides/utils' }],utilImportPath (default @dloizides/utils) is the module the import is added
from. The fixer never touches the module that defines or re-exports the guard
— it would otherwise self-import.
Before 1.1.0 the fixer emitted the call with no import, so
eslint --fixproduced files referencing an undeclared global. Runninglint:fixon a fresh app corrupted ~20 files at once. If you are on ≤1.0.3, upgrade before runninglint:fix.
Delete the local eslint-plugins/*.mjs files and their imports; the rule names,
options, and severities stay exactly as they were.
Namespaced plugin (alternative)
import { plugin as devtools } from '@dloizides/frontend-devtools/eslint';
export default [
{
plugins: { '@dloizides': devtools },
rules: { '@dloizides/no-null-check': 'error' },
},
];Rules
enforce-function-style, enforce-module-structure, enforce-route-preload,
enforce-test-colocation, enum-file-isolation, i18n-interpolation,
i18n-param-names, no-barrel-companion-file, no-duplicate-nav-prefix,
no-duplicate-shared-patterns, no-generated-models-barrel-value-import,
no-null-check, no-optional-undefined, no-product-imports-in-shared,
prefer-const-enum, require-stable-hook-args, smart-max-lines.
ruleNames exports the sorted list for migration assertions (prove no rule was
dropped).
i18n init factory
// src/localization/i18n.ts
import { initI18n } from '@dloizides/frontend-devtools';
import * as Localization from 'expo-localization';
import en from './locales/en.json';
const i18n = initI18n({
resources: { en: { translation: en } },
lng: Localization.getLocales()[0]?.languageCode, // optional; defaults to fallback
});
export default i18n;InitI18nOptions
| Field | Type | Default | Notes |
| --- | --- | --- | --- |
| resources | Resource | — (required) | App-owned strings bundle. |
| lng | string | fallbackLng | Active language. |
| fallbackLng | string | 'en' | Fallback for missing keys. |
| escapeValue | boolean | false | React already escapes output. |
| defaultNS | string \| string[] | i18next default | Optional. |
| ns | string \| string[] | — | Optional; for multi-namespace apps. |
The factory wires react-i18next into the i18next singleton, runs .init(),
swallows the (synchronous-for-in-memory) init rejection, and returns the
singleton. The FM() / FD() helpers + locales/*.json stay app-owned
(per the SaaS code standards).
Scripts
| Script | What |
| --- | --- |
| npm run build | Bundle with tsup (CJS + ESM + d.ts). |
| npm test | Jest (rule behaviour via RuleTester + i18n init). |
| npm run typecheck | tsc --noEmit. |
| npm run lint | ESLint over src. |
License
MIT © dloizides
