@timothyrusso/config-presets 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/.claude/settings.template.json +41 -0
  2. package/LICENSE +21 -0
  3. package/README.md +120 -0
  4. package/biome/base.json +75 -0
  5. package/commitlint.config.cjs +26 -0
  6. package/dist/checks/deps.d.ts +22 -0
  7. package/dist/checks/deps.d.ts.map +1 -0
  8. package/dist/checks/deps.js +45 -0
  9. package/dist/checks/deps.js.map +1 -0
  10. package/dist/checks/files.d.ts +30 -0
  11. package/dist/checks/files.d.ts.map +1 -0
  12. package/dist/checks/files.js +70 -0
  13. package/dist/checks/files.js.map +1 -0
  14. package/dist/checks/hooks.d.ts +15 -0
  15. package/dist/checks/hooks.d.ts.map +1 -0
  16. package/dist/checks/hooks.js +62 -0
  17. package/dist/checks/hooks.js.map +1 -0
  18. package/dist/checks/i18n.d.ts +43 -0
  19. package/dist/checks/i18n.d.ts.map +1 -0
  20. package/dist/checks/i18n.js +168 -0
  21. package/dist/checks/i18n.js.map +1 -0
  22. package/dist/checks/text.d.ts +30 -0
  23. package/dist/checks/text.d.ts.map +1 -0
  24. package/dist/checks/text.js +86 -0
  25. package/dist/checks/text.js.map +1 -0
  26. package/dist/cli.d.ts +3 -0
  27. package/dist/cli.d.ts.map +1 -0
  28. package/dist/cli.js +129 -0
  29. package/dist/cli.js.map +1 -0
  30. package/dist/index.d.ts +8 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +7 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/init/claudeSettings.d.ts +19 -0
  35. package/dist/init/claudeSettings.d.ts.map +1 -0
  36. package/dist/init/claudeSettings.js +23 -0
  37. package/dist/init/claudeSettings.js.map +1 -0
  38. package/dist/init/init.d.ts +35 -0
  39. package/dist/init/init.d.ts.map +1 -0
  40. package/dist/init/init.js +286 -0
  41. package/dist/init/init.js.map +1 -0
  42. package/dist/init/packageJson.d.ts +39 -0
  43. package/dist/init/packageJson.d.ts.map +1 -0
  44. package/dist/init/packageJson.js +97 -0
  45. package/dist/init/packageJson.js.map +1 -0
  46. package/dist/kitConfig.d.ts +69 -0
  47. package/dist/kitConfig.d.ts.map +1 -0
  48. package/dist/kitConfig.js +84 -0
  49. package/dist/kitConfig.js.map +1 -0
  50. package/dist/kitConfig.schema.json +163 -0
  51. package/github/ISSUE_TEMPLATE/config.yml +1 -0
  52. package/github/ISSUE_TEMPLATE/feature.yml +63 -0
  53. package/github/pr-checks.yml +153 -0
  54. package/jest/package.json +3 -0
  55. package/jest/preset.js +18 -0
  56. package/lefthook.yml +30 -0
  57. package/package.json +73 -0
  58. package/templates/dependency-cruiser.template.mjs +13 -0
  59. package/templates/eslint.config.template.mjs +11 -0
  60. package/tsconfig/expo.json +14 -0
@@ -0,0 +1,41 @@
1
+ {
2
+ "extraKnownMarketplaces": {
3
+ "agentic-kit": {
4
+ "source": {
5
+ "source": "github",
6
+ "repo": "timothyrusso/agentic-kit"
7
+ }
8
+ }
9
+ },
10
+ "enabledPlugins": {
11
+ "agentic-kit@agentic-kit": true
12
+ },
13
+ "permissions": {
14
+ "allow": ["Bash(agent-device *)"],
15
+ "deny": [
16
+ "Bash(git commit *--no-verify*)",
17
+ "Bash(git commit -n)",
18
+ "Bash(git commit -n *)",
19
+ "Bash(git commit * -n)",
20
+ "Bash(git commit * -n *)",
21
+ "Bash(git commit -n*)",
22
+ "Bash(git commit * -n*)",
23
+ "Bash(git -c core.hooksPath*)",
24
+ "Bash(git * -c core.hooksPath*)",
25
+ "Bash(git config core.hooksPath*)",
26
+ "Bash(git config * core.hooksPath*)",
27
+ "Bash(git push *--no-verify*)",
28
+ "Bash(git push *--force*)",
29
+ "Bash(git push -f *)",
30
+ "Bash(git push * -f)",
31
+ "Bash(git push * -f *)",
32
+ "Bash(git push * +*)",
33
+ "Bash(LEFTHOOK=0 *)",
34
+ "Read(~/.ssh/**)",
35
+ "Read(~/.aws/**)",
36
+ "Read(~/.npmrc)",
37
+ "Read(./.env)",
38
+ "Read(./.env.*)"
39
+ ]
40
+ }
41
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Timothy Russo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # @timothyrusso/config-presets
2
+
3
+ Everything an Expo app copies or extends to join the kit: Biome, commitlint, lefthook, tsconfig,
4
+ Jest and CI presets, the `kit.config.json` loader, the check scripts and an `init` command that
5
+ wires them all into an app.
6
+
7
+ ## Init
8
+
9
+ At the root of an Expo app (for example a fresh `npx create-expo-app --template blank-typescript`):
10
+
11
+ ```sh
12
+ npx @timothyrusso/config-presets init # asks a few questions
13
+ npx @timothyrusso/config-presets init --yes # takes the defaults, for agents and CI
14
+ npm run check
15
+ ```
16
+
17
+ `init` writes `kit.config.json` and the config files, merges `package.json`, `tsconfig.json` and
18
+ `.claude/settings.json`, then installs the tools (`npm install`), `jest-expo` with `jest` and
19
+ `@types/jest` at the app's SDK versions (`npx expo install --dev`), formats what it wrote and
20
+ installs the git hooks. Nothing the app already has is overwritten without `--force`; a rival
21
+ config (say `eslint.config.js`) is kept and reported.
22
+
23
+ | Flag | Default |
24
+ | --- | --- |
25
+ | `--yes`, `-y` | ask (the defaults are also used when stdin is not a terminal) |
26
+ | `--project-name <name>` | `package.json` `name` |
27
+ | `--features-root <features\|src/features>` | `src/features` when `src/app` exists, else `features` |
28
+ | `--app-root <dir>` | `src/app` when it exists, else `app` |
29
+ | `--i18n <catalog folder\|none>` | `none` |
30
+ | `--languages <en,it>` | `en` (the first is the reference) |
31
+ | `--dashes <forbid\|allow>` | `forbid` |
32
+ | `--force` | keep what exists |
33
+ | `--no-install` | install, format and install the hooks |
34
+
35
+ On create-expo-app's untouched blank entry, `init` also rewrites `index.ts` to import `App` through
36
+ the `@/` alias without plain comments, and replaces em and en dashes in the root Markdown files
37
+ (the template's `AGENTS.md` has some), so `npm run check` passes on day one.
38
+
39
+ ### What it writes
40
+
41
+ | File | Content |
42
+ | --- | --- |
43
+ | `kit.config.json` | the answers, validated against the schema |
44
+ | `biome.json` | `extends: ["@timothyrusso/config-presets/biome"]` |
45
+ | `commitlint.config.cjs` | `extends: ['@timothyrusso/config-presets/commitlint']` |
46
+ | `lefthook.yml` | copy of [`lefthook.yml`](lefthook.yml) |
47
+ | `eslint.config.mjs` | `arch.configs.recommended(loadKitConfig())` from `@timothyrusso/eslint-plugin-arch` |
48
+ | `.dependency-cruiser.mjs` | `createDependencyCruiserConfig(loadKitConfig())` from `@timothyrusso/arch-rules` |
49
+ | `jest.config.cjs` | `require('@timothyrusso/config-presets/jest')` |
50
+ | `.github/workflows/pr-checks.yml` | copy of [`github/pr-checks.yml`](github/pr-checks.yml) |
51
+ | `.github/ISSUE_TEMPLATE/feature.yml`, `config.yml` | the Feature issue template the agents parse, synced from the plugin's `templates/ISSUE_TEMPLATE/` |
52
+ | `.nvmrc` | `22` |
53
+ | `tsconfig.json` | `extends` the preset, plus the `@/*` path for Metro and Jest |
54
+ | `.claude/settings.json` | the deny list, the `agent-device` allow and the kit's marketplace, merged |
55
+ | `package.json` | the scripts below, `prepare: lefthook install` and the tools |
56
+
57
+ The ESLint and dependency-cruiser files are templates that import the sibling packages by name,
58
+ so this package depends on neither at runtime (they are optional peers).
59
+
60
+ ### Scripts
61
+
62
+ ```text
63
+ lint biome check . && eslint .
64
+ typecheck tsc --noEmit
65
+ check:text config-presets check-text
66
+ check:arch config-presets check-deps
67
+ check:i18n config-presets check-i18n
68
+ check:unused-keys config-presets check-unused-keys
69
+ check:hooks config-presets check-hooks
70
+ test jest --passWithNoTests
71
+ check all of the above
72
+ ```
73
+
74
+ The catalog checks print `SKIP` until `kit.config.json` has an `i18n` section, and `check:arch`
75
+ passes with a note until `featuresRoot` or `appRoot` exists, so `check` never needs editing.
76
+
77
+ ## Presets
78
+
79
+ | Import | What |
80
+ | --- | --- |
81
+ | `@timothyrusso/config-presets/biome` | Biome base: formatting, the recommended rules, `noConsole`, `noExplicitAny`, native and build folders ignored, `package.json` expanded as npm writes it |
82
+ | `@timothyrusso/config-presets/commitlint` | `type(issue): message`; types `feat fix chore docs refactor test ci perf build`; the scope must be the issue number |
83
+ | `@timothyrusso/config-presets/tsconfig/expo.json` | `expo/tsconfig.base` plus `strict`, `noUncheckedIndexedAccess`, `noImplicitOverride`, `noFallthroughCasesInSwitch`, `noUnusedLocals`, `noUnusedParameters`, `verbatimModuleSyntax` and `@/*` |
84
+ | `@timothyrusso/config-presets/jest` | `preset: 'jest-expo'`, `@/` mapped to the app root, tests in `__tests__/**/*.test.*` |
85
+ | `lefthook.yml` | pre-commit: Biome check and format (restaged), ESLint and the dash check on staged files; commit-msg: commitlint, the issue-number check and the dash check |
86
+ | `github/pr-checks.yml` | ubuntu: `npm ci`, commitlint over the PR's commits, the architecture check with one marker comment on the PR, `npm run check`, `npx expo export --platform ios`; macOS: a path-filtered simulator build template for native and watch targets |
87
+
88
+ TypeScript: the tools support TypeScript 5.9 and 6.x. dependency-cruiser 18 does not support
89
+ TypeScript 7, so `init` adds `typescript ~5.9.3` when the app has none and warns on 7.
90
+
91
+ ## Checks
92
+
93
+ Each reads `kit.config.json` (nearest one above the working directory) and exits non-zero on a
94
+ problem.
95
+
96
+ - `check-text [files]`: no em or en dash in any file the repository tracks or would track (code,
97
+ Markdown, JSON, YAML, Swift, ...), when `lint.dashes` is `forbid`. ESLint's `no-dashes` covers
98
+ JavaScript and TypeScript only; this covers the rest. `--message <file>` checks a commit
99
+ message; `--fix` replaces the dashes with hyphens.
100
+ - `check-i18n`: every key in every language of `i18n.languages`, plural pairs (`_one`, `_other`)
101
+ complete, `{placeholder}` slots matching the first language, and no plain prose typed into JSX.
102
+ Catalogs are `<catalogPath>/<language>.ts` (or `.js`, `.json`) exporting the catalog as the
103
+ language code or as the default export.
104
+ - `check-unused-keys`: every key of the first language is read somewhere, as a quoted string, in
105
+ `appRoot`, `featuresRoot` or `src`.
106
+ - `check-hooks`: no `useMemo` or `useCallback` calling `t()` without depending on it, no memoised
107
+ component calling `tr()` instead of `useT()`.
108
+ - `check-deps [--config <file>]`: runs the app's dependency-cruiser over `featuresRoot` and
109
+ `appRoot` with the generated architecture rules.
110
+
111
+ ## Loader
112
+
113
+ ```ts
114
+ import { loadKitConfig } from '@timothyrusso/config-presets';
115
+
116
+ const config = loadKitConfig(); // nearest kit.config.json, schema defaults applied
117
+ ```
118
+
119
+ It throws `KitConfigError` listing every problem at once. The schema ships as
120
+ `@timothyrusso/config-presets/kit.config.schema.json`.
@@ -0,0 +1,75 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/2.5.14/schema.json",
3
+ "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true },
4
+ "files": {
5
+ "ignoreUnknown": true,
6
+ "includes": [
7
+ "**",
8
+ "!**/android",
9
+ "!**/ios",
10
+ "!**/node_modules",
11
+ "!**/dist",
12
+ "!**/coverage",
13
+ "!**/.expo",
14
+ "!**/web-build",
15
+ "!**/expo-env.d.ts"
16
+ ]
17
+ },
18
+ "formatter": {
19
+ "enabled": true,
20
+ "formatWithErrors": false,
21
+ "indentStyle": "space",
22
+ "indentWidth": 2,
23
+ "lineEnding": "lf",
24
+ "lineWidth": 120,
25
+ "attributePosition": "auto",
26
+ "bracketSpacing": true
27
+ },
28
+ "assist": { "actions": { "source": { "organizeImports": "on" } } },
29
+ "linter": {
30
+ "enabled": true,
31
+ "rules": {
32
+ "preset": "recommended",
33
+ "style": { "useImportType": "error" },
34
+ "correctness": {
35
+ "useExhaustiveDependencies": "off",
36
+ "noUndeclaredDependencies": "off",
37
+ "useImportExtensions": "off",
38
+ "noUndeclaredVariables": "off",
39
+ "noUnusedVariables": { "level": "error", "fix": "safe" }
40
+ },
41
+ "suspicious": {
42
+ "noExplicitAny": "error",
43
+ "noConsole": "error",
44
+ "noEmptyBlockStatements": "off"
45
+ },
46
+ "complexity": { "noForEach": "off" }
47
+ }
48
+ },
49
+ "javascript": {
50
+ "formatter": {
51
+ "quoteStyle": "single",
52
+ "jsxQuoteStyle": "double",
53
+ "quoteProperties": "asNeeded",
54
+ "trailingCommas": "all",
55
+ "semicolons": "always",
56
+ "arrowParentheses": "asNeeded",
57
+ "bracketSameLine": false,
58
+ "bracketSpacing": true,
59
+ "attributePosition": "auto"
60
+ }
61
+ },
62
+ "json": {
63
+ "formatter": { "trailingCommas": "none" }
64
+ },
65
+ "overrides": [
66
+ {
67
+ "includes": ["**/package.json"],
68
+ "json": { "formatter": { "expand": "always" } }
69
+ },
70
+ {
71
+ "includes": ["scripts/**"],
72
+ "linter": { "rules": { "suspicious": { "noConsole": "off" } } }
73
+ }
74
+ ]
75
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Commit messages are `type(issue): message`, e.g. `feat(12): add the settings screen`: the scope
3
+ * is always the GitHub issue number. Extend it from the app's `commitlint.config.cjs`:
4
+ *
5
+ * module.exports = { extends: ['@timothyrusso/config-presets/commitlint'] };
6
+ */
7
+ const config = {
8
+ extends: ['@commitlint/config-conventional'],
9
+ plugins: [
10
+ {
11
+ rules: {
12
+ 'scope-issue-number': ({ scope }) => [
13
+ /^\d+$/.test(scope ?? ''),
14
+ 'scope must be the GitHub issue number, e.g. feat(12): add the settings screen',
15
+ ],
16
+ },
17
+ },
18
+ ],
19
+ rules: {
20
+ 'type-enum': [2, 'always', ['feat', 'fix', 'chore', 'docs', 'refactor', 'test', 'ci', 'perf', 'build']],
21
+ 'scope-empty': [2, 'never'],
22
+ 'scope-issue-number': [2, 'always'],
23
+ },
24
+ };
25
+
26
+ module.exports = config;
@@ -0,0 +1,22 @@
1
+ import type { SourceLayout } from './files.js';
2
+ /** Config files `check-deps` looks for, in order, when no `--config` is given. */
3
+ export declare const DEPCRUISE_CONFIG_FILES: readonly [".dependency-cruiser.mjs", ".dependency-cruiser.js", ".dependency-cruiser.cjs", ".dependency-cruiser.json"];
4
+ export interface DepsCheckOptions {
5
+ readonly rootDir: string;
6
+ readonly config: SourceLayout;
7
+ readonly configFile?: string;
8
+ }
9
+ export interface DepsPlan {
10
+ /** The folders to cruise, relative to `rootDir`: `featuresRoot` and `appRoot`, those that exist. */
11
+ readonly roots: readonly string[];
12
+ readonly configFile: string | undefined;
13
+ }
14
+ /** What `check-deps` would run, without running it. */
15
+ export declare function planDepsCheck({ rootDir, config, configFile }: DepsCheckOptions): DepsPlan;
16
+ /**
17
+ * Runs the app's own dependency-cruiser over `featuresRoot` and `appRoot` with the generated
18
+ * architecture rules. Passes with a note while neither folder exists yet (a fresh app), so the
19
+ * check can sit in `npm run check` from day one. Returns the exit code; output goes to the terminal.
20
+ */
21
+ export declare function runDepsCheck(options: DepsCheckOptions, write: (line: string) => void): number;
22
+ //# sourceMappingURL=deps.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deps.d.ts","sourceRoot":"","sources":["../../src/checks/deps.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,kFAAkF;AAClF,eAAO,MAAM,sBAAsB,uHAKzB,CAAC;AAEX,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,QAAQ;IACvB,oGAAoG;IACpG,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;CACzC;AAED,uDAAuD;AACvD,wBAAgB,aAAa,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE,gBAAgB,GAAG,QAAQ,CAOzF;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,gBAAgB,EAAE,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,CAmB7F"}
@@ -0,0 +1,45 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { existsSync, statSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ /** Config files `check-deps` looks for, in order, when no `--config` is given. */
5
+ export const DEPCRUISE_CONFIG_FILES = [
6
+ '.dependency-cruiser.mjs',
7
+ '.dependency-cruiser.js',
8
+ '.dependency-cruiser.cjs',
9
+ '.dependency-cruiser.json',
10
+ ];
11
+ /** What `check-deps` would run, without running it. */
12
+ export function planDepsCheck({ rootDir, config, configFile }) {
13
+ const roots = [...new Set([config.featuresRoot, config.appRoot])].filter(dir => {
14
+ const full = resolve(rootDir, dir);
15
+ return existsSync(full) && statSync(full).isDirectory();
16
+ });
17
+ const file = configFile ?? DEPCRUISE_CONFIG_FILES.find(name => existsSync(resolve(rootDir, name)));
18
+ return { roots, configFile: file };
19
+ }
20
+ /**
21
+ * Runs the app's own dependency-cruiser over `featuresRoot` and `appRoot` with the generated
22
+ * architecture rules. Passes with a note while neither folder exists yet (a fresh app), so the
23
+ * check can sit in `npm run check` from day one. Returns the exit code; output goes to the terminal.
24
+ */
25
+ export function runDepsCheck(options, write) {
26
+ const { roots, configFile } = planDepsCheck(options);
27
+ if (!configFile) {
28
+ write(`FAIL: no dependency-cruiser config (${DEPCRUISE_CONFIG_FILES.join(', ')}). Run config-presets init.`);
29
+ return 1;
30
+ }
31
+ if (roots.length === 0) {
32
+ write(`PASS: no ${options.config.featuresRoot}/ or ${options.config.appRoot}/ folder yet, nothing to cruise`);
33
+ return 0;
34
+ }
35
+ const result = spawnSync('npx', ['--no-install', 'depcruise', '--config', configFile, ...roots], {
36
+ cwd: options.rootDir,
37
+ stdio: 'inherit',
38
+ });
39
+ if (result.error) {
40
+ write(`FAIL: could not run dependency-cruiser: ${result.error.message}`);
41
+ return 1;
42
+ }
43
+ return result.status ?? 1;
44
+ }
45
+ //# sourceMappingURL=deps.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deps.js","sourceRoot":"","sources":["../../src/checks/deps.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAGpC,kFAAkF;AAClF,MAAM,CAAC,MAAM,sBAAsB,GAAG;IACpC,yBAAyB;IACzB,wBAAwB;IACxB,yBAAyB;IACzB,0BAA0B;CAClB,CAAC;AAcX,uDAAuD;AACvD,MAAM,UAAU,aAAa,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,UAAU,EAAoB;IAC7E,MAAM,KAAK,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE;QAC7E,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;QACnC,OAAO,UAAU,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAC;IAC1D,CAAC,CAAC,CAAC;IACH,MAAM,IAAI,GAAG,UAAU,IAAI,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC;IACnG,OAAO,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC;AACrC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,OAAyB,EAAE,KAA6B;IACnF,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;IACrD,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,KAAK,CAAC,uCAAuC,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC,6BAA6B,CAAC,CAAC;QAC7G,OAAO,CAAC,CAAC;IACX,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACvB,KAAK,CAAC,YAAY,OAAO,CAAC,MAAM,CAAC,YAAY,QAAQ,OAAO,CAAC,MAAM,CAAC,OAAO,iCAAiC,CAAC,CAAC;QAC9G,OAAO,CAAC,CAAC;IACX,CAAC;IACD,MAAM,MAAM,GAAG,SAAS,CAAC,KAAK,EAAE,CAAC,cAAc,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,EAAE,GAAG,KAAK,CAAC,EAAE;QAC/F,GAAG,EAAE,OAAO,CAAC,OAAO;QACpB,KAAK,EAAE,SAAS;KACjB,CAAC,CAAC;IACH,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QACjB,KAAK,CAAC,2CAA2C,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QACzE,OAAO,CAAC,CAAC;IACX,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,IAAI,CAAC,CAAC;AAC5B,CAAC"}
@@ -0,0 +1,30 @@
1
+ /** What every check returns: `ok`, a one-line summary and one line per problem. */
2
+ export interface CheckResult {
3
+ readonly ok: boolean;
4
+ readonly summary: string;
5
+ readonly problems: readonly string[];
6
+ }
7
+ /** Where an app keeps its source, from `kit.config.json`. A `KitConfig` satisfies it. */
8
+ export interface SourceLayout {
9
+ readonly appRoot: string;
10
+ readonly featuresRoot: string;
11
+ }
12
+ /** Folders no check descends into: dependencies, native projects and build output. */
13
+ export declare const SKIP_DIRS: ReadonlySet<string>;
14
+ /** Every file under `dir` whose name passes `accept`, skipping `SKIP_DIRS`. Missing `dir` yields none. */
15
+ export declare function walkFiles(dir: string, accept: (name: string) => boolean): string[];
16
+ /**
17
+ * The folders that hold app source, as absolute paths: `appRoot`, `featuresRoot` and `src`, those
18
+ * that exist, with a folder dropped when another one already contains it.
19
+ */
20
+ export declare function sourceRoots(rootDir: string, config: SourceLayout): string[];
21
+ /** Every `.ts` and `.tsx` file (or only `.tsx` with `tsxOnly`) under the source roots. */
22
+ export declare function sourceFiles(roots: readonly string[], tsxOnly?: boolean): string[];
23
+ /**
24
+ * Tracked and untracked, not ignored, files of the git work tree at `rootDir`, relative to it.
25
+ * Outside a git repository it walks the folder instead, skipping `SKIP_DIRS`.
26
+ */
27
+ export declare function projectFiles(rootDir: string): string[];
28
+ /** `file` relative to `rootDir`, with forward slashes, for messages. */
29
+ export declare function display(rootDir: string, file: string): string;
30
+ //# sourceMappingURL=files.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"files.d.ts","sourceRoot":"","sources":["../../src/checks/files.ts"],"names":[],"mappings":"AAIA,mFAAmF;AACnF,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;CACtC;AAED,yFAAyF;AACzF,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED,sFAAsF;AACtF,eAAO,MAAM,SAAS,EAAE,WAAW,CAAC,MAAM,CAUxC,CAAC;AAEH,0GAA0G;AAC1G,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,GAAG,MAAM,EAAE,CAclF;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,GAAG,MAAM,EAAE,CAI3E;AAED,0FAA0F;AAC1F,wBAAgB,WAAW,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,UAAQ,GAAG,MAAM,EAAE,CAG/E;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAWtD;AAED,wEAAwE;AACxE,wBAAgB,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE7D"}
@@ -0,0 +1,70 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { existsSync, readdirSync, statSync } from 'node:fs';
3
+ import { join, relative, resolve, sep } from 'node:path';
4
+ /** Folders no check descends into: dependencies, native projects and build output. */
5
+ export const SKIP_DIRS = new Set([
6
+ 'node_modules',
7
+ '.git',
8
+ 'ios',
9
+ 'android',
10
+ '.expo',
11
+ 'dist',
12
+ 'coverage',
13
+ 'build',
14
+ 'web-build',
15
+ ]);
16
+ /** Every file under `dir` whose name passes `accept`, skipping `SKIP_DIRS`. Missing `dir` yields none. */
17
+ export function walkFiles(dir, accept) {
18
+ if (!existsSync(dir))
19
+ return [];
20
+ const out = [];
21
+ const visit = (current) => {
22
+ for (const entry of readdirSync(current, { withFileTypes: true })) {
23
+ if (entry.isDirectory()) {
24
+ if (!SKIP_DIRS.has(entry.name))
25
+ visit(join(current, entry.name));
26
+ }
27
+ else if (entry.isFile() && accept(entry.name)) {
28
+ out.push(join(current, entry.name));
29
+ }
30
+ }
31
+ };
32
+ visit(dir);
33
+ return out.sort();
34
+ }
35
+ /**
36
+ * The folders that hold app source, as absolute paths: `appRoot`, `featuresRoot` and `src`, those
37
+ * that exist, with a folder dropped when another one already contains it.
38
+ */
39
+ export function sourceRoots(rootDir, config) {
40
+ const candidates = [...new Set([config.appRoot, config.featuresRoot, 'src'].map(dir => resolve(rootDir, dir)))];
41
+ const existing = candidates.filter(dir => existsSync(dir) && statSync(dir).isDirectory());
42
+ return existing.filter(dir => !existing.some(other => other !== dir && dir.startsWith(other + sep)));
43
+ }
44
+ /** Every `.ts` and `.tsx` file (or only `.tsx` with `tsxOnly`) under the source roots. */
45
+ export function sourceFiles(roots, tsxOnly = false) {
46
+ const accept = tsxOnly ? (name) => name.endsWith('.tsx') : (name) => /\.tsx?$/.test(name);
47
+ return roots.flatMap(root => walkFiles(root, accept));
48
+ }
49
+ /**
50
+ * Tracked and untracked, not ignored, files of the git work tree at `rootDir`, relative to it.
51
+ * Outside a git repository it walks the folder instead, skipping `SKIP_DIRS`.
52
+ */
53
+ export function projectFiles(rootDir) {
54
+ try {
55
+ const out = execFileSync('git', ['ls-files', '--cached', '--others', '--exclude-standard', '-z'], {
56
+ cwd: rootDir,
57
+ encoding: 'utf8',
58
+ stdio: ['ignore', 'pipe', 'ignore'],
59
+ });
60
+ return out.split('\0').filter(Boolean).sort();
61
+ }
62
+ catch {
63
+ return walkFiles(rootDir, () => true).map(file => relative(rootDir, file));
64
+ }
65
+ }
66
+ /** `file` relative to `rootDir`, with forward slashes, for messages. */
67
+ export function display(rootDir, file) {
68
+ return relative(rootDir, file).split(sep).join('/');
69
+ }
70
+ //# sourceMappingURL=files.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"files.js","sourceRoot":"","sources":["../../src/checks/files.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import { type CheckResult } from './files.js';
2
+ import type { CatalogCheckOptions } from './i18n.js';
3
+ /** Problems in one file's source. `file` is only used in the messages. */
4
+ export declare function staleLanguageProblems(file: string, src: string): string[];
5
+ /**
6
+ * Two ways translated copy silently freezes, for apps whose translator is `t()` from a `useT()`
7
+ * hook and `tr()` outside React:
8
+ *
9
+ * 1. A `useMemo` or `useCallback` that calls `t()` without listing it as a dependency keeps the
10
+ * translator it was created with, so it answers in the old language after a switch.
11
+ * 2. A memoised component that calls `tr()` (which reads the language but does not subscribe)
12
+ * skips the re-render and keeps the words it first drew. Class components are exempt.
13
+ */
14
+ export declare function checkHooks({ rootDir, config }: CatalogCheckOptions): CheckResult;
15
+ //# sourceMappingURL=hooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.d.ts","sourceRoot":"","sources":["../../src/checks/hooks.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,WAAW,EAAqC,MAAM,YAAY,CAAC;AACjF,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAmBrD,0EAA0E;AAC1E,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAqBzE;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,mBAAmB,GAAG,WAAW,CAWhF"}
@@ -0,0 +1,62 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { display, sourceFiles, sourceRoots } from './files.js';
3
+ /** A class component cannot call a hook, so it may use `tr()`. */
4
+ const CLASS_COMPONENT = /\bextends\s+(React\.)?(Pure)?Component\b/;
5
+ /** The index of the bracket that closes the one at `open`, or the end of `src`. */
6
+ function closingBracket(src, open) {
7
+ let depth = 0;
8
+ for (let i = open; i < src.length; i += 1) {
9
+ const c = src[i];
10
+ if (c === '(' || c === '[' || c === '{')
11
+ depth += 1;
12
+ else if (c === ')' || c === ']' || c === '}') {
13
+ depth -= 1;
14
+ if (depth === 0)
15
+ return i;
16
+ }
17
+ }
18
+ return src.length - 1;
19
+ }
20
+ /** Problems in one file's source. `file` is only used in the messages. */
21
+ export function staleLanguageProblems(file, src) {
22
+ const problems = [];
23
+ for (const match of src.matchAll(/use(?:Memo|Callback)\(/g)) {
24
+ const start = match.index;
25
+ const call = src.slice(start, closingBracket(src, start + match[0].length - 1) + 1);
26
+ const open = call.lastIndexOf('[');
27
+ const close = call.lastIndexOf(']');
28
+ if (open < 0 || close < open)
29
+ continue;
30
+ const body = call.slice(0, open);
31
+ const deps = call.slice(open, close + 1);
32
+ if (!/\bt\(/.test(body) || /\bt\b/.test(deps))
33
+ continue;
34
+ const line = src.slice(0, start).split('\n').length;
35
+ problems.push(`${file}:${line}: hook calls t() but does not depend on it, so it keeps one language`);
36
+ }
37
+ if (/\btr\(/.test(src) && !CLASS_COMPONENT.test(src) && /memo\(function/.test(src) && !/useT\(\)/.test(src)) {
38
+ const line = src.split('\n').findIndex(l => /\btr\(/.test(l) && !/^\s*(\*|\/\/)/.test(l)) + 1;
39
+ problems.push(`${file}:${line}: memoised component calls tr(), which does not subscribe: use useT()`);
40
+ }
41
+ return problems;
42
+ }
43
+ /**
44
+ * Two ways translated copy silently freezes, for apps whose translator is `t()` from a `useT()`
45
+ * hook and `tr()` outside React:
46
+ *
47
+ * 1. A `useMemo` or `useCallback` that calls `t()` without listing it as a dependency keeps the
48
+ * translator it was created with, so it answers in the old language after a switch.
49
+ * 2. A memoised component that calls `tr()` (which reads the language but does not subscribe)
50
+ * skips the re-render and keeps the words it first drew. Class components are exempt.
51
+ */
52
+ export function checkHooks({ rootDir, config }) {
53
+ if (!config.i18n) {
54
+ return { ok: true, summary: 'SKIP: no i18n section in kit.config.json, so no stale language check', problems: [] };
55
+ }
56
+ const problems = sourceFiles(sourceRoots(rootDir, config)).flatMap(file => staleLanguageProblems(display(rootDir, file), readFileSync(file, 'utf8')));
57
+ if (problems.length > 0) {
58
+ return { ok: false, summary: `FAIL: ${problems.length} stale-language problem(s)`, problems };
59
+ }
60
+ return { ok: true, summary: 'PASS: no hook or memoised component holds a stale language', problems };
61
+ }
62
+ //# sourceMappingURL=hooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.js","sourceRoot":"","sources":["../../src/checks/hooks.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ import { type CheckResult, type SourceLayout } from './files.js';
2
+ /** The fields of `kit.config.json` the catalog checks read. A `KitConfig` satisfies it. */
3
+ export interface I18nKitConfig extends SourceLayout {
4
+ readonly i18n?: {
5
+ readonly catalogPath: string;
6
+ readonly languages: readonly string[];
7
+ };
8
+ }
9
+ export interface CatalogCheckOptions {
10
+ /** App root that every path in `kit.config.json` is relative to. */
11
+ readonly rootDir: string;
12
+ readonly config: I18nKitConfig;
13
+ }
14
+ /** A catalog flattened to `[dotted.key, value]` pairs, in source order. */
15
+ export type FlatCatalog = ReadonlyArray<readonly [string, string]>;
16
+ /** The file that holds `language` inside `catalogPath`: `<catalogPath>/<language>.ts`, or `.js` or `.json`. */
17
+ export declare function catalogFile(rootDir: string, catalogPath: string, language: string): string | undefined;
18
+ /** Every string leaf of a nested catalog object, keyed by its dotted path. */
19
+ export declare function flattenCatalog(value: unknown, prefix?: string): Array<[string, string]>;
20
+ /**
21
+ * Loads one language's catalog through jiti, so TypeScript catalogs are read as the real objects
22
+ * rather than parsed as text. The catalog is the export named after the language (`export const en`),
23
+ * or the default export.
24
+ */
25
+ export declare function loadCatalog(file: string, language: string): Promise<FlatCatalog>;
26
+ /**
27
+ * Catalog parity, driven by `kit.config.json` `i18n`. The first language is the reference.
28
+ *
29
+ * - every key of every language exists in every other one;
30
+ * - a plural key (`_one`) has its partner (`_other`) in the same language, and the reverse;
31
+ * - `{placeholder}` slots match the reference, so no language renders a literal brace;
32
+ * - no line of plain prose sits inside JSX under the source roots.
33
+ */
34
+ export declare function checkI18n({ rootDir, config }: CatalogCheckOptions): Promise<CheckResult>;
35
+ /**
36
+ * Every key of the reference catalog is read somewhere under the source roots, as a quoted
37
+ * string: `t('home.title')`, `tr('home.title')` or a module-level table of keys. A plural key
38
+ * counts as used when its base is, because `t()` picks the form from `count`. A key built at
39
+ * runtime from pieces reads as unused, on purpose: a key nobody can search for is a key nobody
40
+ * can safely rename.
41
+ */
42
+ export declare function checkUnusedKeys({ rootDir, config }: CatalogCheckOptions): Promise<CheckResult>;
43
+ //# sourceMappingURL=i18n.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../../src/checks/i18n.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,WAAW,EAAW,KAAK,YAAY,EAA4B,MAAM,YAAY,CAAC;AAEpG,2FAA2F;AAC3F,MAAM,WAAW,aAAc,SAAQ,YAAY;IACjD,QAAQ,CAAC,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAA;KAAE,CAAC;CACzF;AAED,MAAM,WAAW,mBAAmB;IAClC,oEAAoE;IACpE,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;CAChC;AAED,2EAA2E;AAC3E,MAAM,MAAM,WAAW,GAAG,aAAa,CAAC,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;AAInE,+GAA+G;AAC/G,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAItG;AAED,8EAA8E;AAC9E,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,SAAK,GAAG,KAAK,CAAC,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAMnF;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAQtF;AA4DD;;;;;;;GAOG;AACH,wBAAsB,SAAS,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,mBAAmB,GAAG,OAAO,CAAC,WAAW,CAAC,CAwC9F;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CAAC,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,mBAAmB,GAAG,OAAO,CAAC,WAAW,CAAC,CAsBpG"}