@rexezuge/tooling 0.0.0-stage → 1.0.1

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 (170) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +297 -2
  3. package/dist/eslint-boundaries.d.ts +75 -0
  4. package/dist/eslint-boundaries.d.ts.map +1 -0
  5. package/dist/eslint-boundaries.js +138 -0
  6. package/dist/eslint-boundaries.js.map +1 -0
  7. package/dist/eslint-rules.d.ts +41 -0
  8. package/dist/eslint-rules.d.ts.map +1 -0
  9. package/dist/eslint-rules.js +273 -0
  10. package/dist/eslint-rules.js.map +1 -0
  11. package/dist/eslint.d.ts +96 -0
  12. package/dist/eslint.d.ts.map +1 -0
  13. package/dist/eslint.js +189 -0
  14. package/dist/eslint.js.map +1 -0
  15. package/dist/functions/pages-proxy.d.ts +113 -0
  16. package/dist/functions/pages-proxy.d.ts.map +1 -0
  17. package/dist/functions/pages-proxy.js +131 -0
  18. package/dist/functions/pages-proxy.js.map +1 -0
  19. package/dist/index.d.ts +31 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +28 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/scripts/backup/d1-target.d.ts +35 -0
  24. package/dist/scripts/backup/d1-target.d.ts.map +1 -0
  25. package/dist/scripts/backup/d1-target.js +31 -0
  26. package/dist/scripts/backup/d1-target.js.map +1 -0
  27. package/dist/scripts/backup/destination-config.d.ts +61 -0
  28. package/dist/scripts/backup/destination-config.d.ts.map +1 -0
  29. package/dist/scripts/backup/destination-config.js +57 -0
  30. package/dist/scripts/backup/destination-config.js.map +1 -0
  31. package/dist/scripts/backup/encrypt-backup.d.ts +35 -0
  32. package/dist/scripts/backup/encrypt-backup.d.ts.map +1 -0
  33. package/dist/scripts/backup/encrypt-backup.js +98 -0
  34. package/dist/scripts/backup/encrypt-backup.js.map +1 -0
  35. package/dist/scripts/backup/naming.d.ts +44 -0
  36. package/dist/scripts/backup/naming.d.ts.map +1 -0
  37. package/dist/scripts/backup/naming.js +58 -0
  38. package/dist/scripts/backup/naming.js.map +1 -0
  39. package/dist/scripts/check-god-files.d.ts +164 -0
  40. package/dist/scripts/check-god-files.d.ts.map +1 -0
  41. package/dist/scripts/check-god-files.js +271 -0
  42. package/dist/scripts/check-god-files.js.map +1 -0
  43. package/dist/scripts/ensure-spa-shell-stub.d.ts +3 -0
  44. package/dist/scripts/ensure-spa-shell-stub.d.ts.map +1 -0
  45. package/dist/scripts/ensure-spa-shell-stub.js +30 -0
  46. package/dist/scripts/ensure-spa-shell-stub.js.map +1 -0
  47. package/dist/scripts/init-secrets.d.ts +63 -0
  48. package/dist/scripts/init-secrets.d.ts.map +1 -0
  49. package/dist/scripts/init-secrets.js +240 -0
  50. package/dist/scripts/init-secrets.js.map +1 -0
  51. package/dist/scripts/lib/cli-args.d.ts +78 -0
  52. package/dist/scripts/lib/cli-args.d.ts.map +1 -0
  53. package/dist/scripts/lib/cli-args.js +116 -0
  54. package/dist/scripts/lib/cli-args.js.map +1 -0
  55. package/dist/scripts/lib/github-actions.d.ts +26 -0
  56. package/dist/scripts/lib/github-actions.d.ts.map +1 -0
  57. package/dist/scripts/lib/github-actions.js +38 -0
  58. package/dist/scripts/lib/github-actions.js.map +1 -0
  59. package/dist/scripts/lib/wrangler-table.d.ts +46 -0
  60. package/dist/scripts/lib/wrangler-table.d.ts.map +1 -0
  61. package/dist/scripts/lib/wrangler-table.js +99 -0
  62. package/dist/scripts/lib/wrangler-table.js.map +1 -0
  63. package/dist/scripts/migrations-lock.d.ts +3 -0
  64. package/dist/scripts/migrations-lock.d.ts.map +1 -0
  65. package/dist/scripts/migrations-lock.js +46 -0
  66. package/dist/scripts/migrations-lock.js.map +1 -0
  67. package/dist/scripts/prepare-wrangler-config.d.ts +3 -0
  68. package/dist/scripts/prepare-wrangler-config.d.ts.map +1 -0
  69. package/dist/scripts/prepare-wrangler-config.js +50 -0
  70. package/dist/scripts/prepare-wrangler-config.js.map +1 -0
  71. package/dist/scripts/spa-shell.d.ts +41 -0
  72. package/dist/scripts/spa-shell.d.ts.map +1 -0
  73. package/dist/scripts/spa-shell.js +155 -0
  74. package/dist/scripts/spa-shell.js.map +1 -0
  75. package/dist/scripts/validate-locales.d.ts +26 -0
  76. package/dist/scripts/validate-locales.d.ts.map +1 -0
  77. package/dist/scripts/validate-locales.js +350 -0
  78. package/dist/scripts/validate-locales.js.map +1 -0
  79. package/dist/scripts/verify-migrations.d.ts +62 -0
  80. package/dist/scripts/verify-migrations.d.ts.map +1 -0
  81. package/dist/scripts/verify-migrations.js +302 -0
  82. package/dist/scripts/verify-migrations.js.map +1 -0
  83. package/dist/scripts/verify-spa-shell.d.ts +3 -0
  84. package/dist/scripts/verify-spa-shell.d.ts.map +1 -0
  85. package/dist/scripts/verify-spa-shell.js +53 -0
  86. package/dist/scripts/verify-spa-shell.js.map +1 -0
  87. package/dist/scripts/wrangler-config/cli.d.ts +22 -0
  88. package/dist/scripts/wrangler-config/cli.d.ts.map +1 -0
  89. package/dist/scripts/wrangler-config/cli.js +51 -0
  90. package/dist/scripts/wrangler-config/cli.js.map +1 -0
  91. package/dist/scripts/wrangler-config/patches.d.ts +51 -0
  92. package/dist/scripts/wrangler-config/patches.d.ts.map +1 -0
  93. package/dist/scripts/wrangler-config/patches.js +140 -0
  94. package/dist/scripts/wrangler-config/patches.js.map +1 -0
  95. package/dist/scripts/wrangler-config/resources.d.ts +70 -0
  96. package/dist/scripts/wrangler-config/resources.d.ts.map +1 -0
  97. package/dist/scripts/wrangler-config/resources.js +290 -0
  98. package/dist/scripts/wrangler-config/resources.js.map +1 -0
  99. package/dist/scripts/wrangler-config/types.d.ts +103 -0
  100. package/dist/scripts/wrangler-config/types.d.ts.map +1 -0
  101. package/dist/scripts/wrangler-config/types.js +49 -0
  102. package/dist/scripts/wrangler-config/types.js.map +1 -0
  103. package/dist/test/integration-migrations.d.ts +167 -0
  104. package/dist/test/integration-migrations.d.ts.map +1 -0
  105. package/dist/test/integration-migrations.js +171 -0
  106. package/dist/test/integration-migrations.js.map +1 -0
  107. package/dist/test/mocks/cloudflare-workers.d.ts +106 -0
  108. package/dist/test/mocks/cloudflare-workers.d.ts.map +1 -0
  109. package/dist/test/mocks/cloudflare-workers.js +90 -0
  110. package/dist/test/mocks/cloudflare-workers.js.map +1 -0
  111. package/dist/vite.d.ts +117 -0
  112. package/dist/vite.d.ts.map +1 -0
  113. package/dist/vite.js +125 -0
  114. package/dist/vite.js.map +1 -0
  115. package/dist/vitest-web.d.ts +73 -0
  116. package/dist/vitest-web.d.ts.map +1 -0
  117. package/dist/vitest-web.js +72 -0
  118. package/dist/vitest-web.js.map +1 -0
  119. package/dist/vitest.d.ts +92 -0
  120. package/dist/vitest.d.ts.map +1 -0
  121. package/dist/vitest.js +128 -0
  122. package/dist/vitest.js.map +1 -0
  123. package/package.json +58 -3
  124. package/src/eslint-boundaries.ts +169 -0
  125. package/src/eslint-rules.ts +276 -0
  126. package/src/eslint.test.ts +175 -0
  127. package/src/eslint.ts +257 -0
  128. package/src/functions/pages-proxy.test.ts +72 -0
  129. package/src/functions/pages-proxy.ts +187 -0
  130. package/src/github/actions/retry-step/action.yml +39 -0
  131. package/src/github/actions/setup-env/action.yml +20 -0
  132. package/src/github/dependabot.yml +30 -0
  133. package/src/github/workflows/backup-main.yml +46 -0
  134. package/src/github/workflows/continuous-deployment.yml +188 -0
  135. package/src/github/workflows/continuous-integration.yml +259 -0
  136. package/src/github/workflows/scheduled-version-update.yml +38 -0
  137. package/src/github/workflows/upstream-sync.yml +56 -0
  138. package/src/index.ts +42 -0
  139. package/src/scripts/backup/backup-rules.test.ts +105 -0
  140. package/src/scripts/backup/d1-target.ts +54 -0
  141. package/src/scripts/backup/destination-config.ts +92 -0
  142. package/src/scripts/backup/encrypt-backup.ts +107 -0
  143. package/src/scripts/backup/naming.ts +62 -0
  144. package/src/scripts/check-god-files.test.ts +131 -0
  145. package/src/scripts/check-god-files.ts +327 -0
  146. package/src/scripts/ensure-spa-shell-stub.ts +34 -0
  147. package/src/scripts/init-secrets.ts +265 -0
  148. package/src/scripts/lib/cli-args.ts +154 -0
  149. package/src/scripts/lib/github-actions.ts +41 -0
  150. package/src/scripts/lib/wrangler-table.ts +105 -0
  151. package/src/scripts/migrations-lock.ts +52 -0
  152. package/src/scripts/prepare-wrangler-config.ts +51 -0
  153. package/src/scripts/spa-shell.test.ts +91 -0
  154. package/src/scripts/spa-shell.ts +179 -0
  155. package/src/scripts/validate-locales.test.ts +89 -0
  156. package/src/scripts/validate-locales.ts +380 -0
  157. package/src/scripts/verify-migrations.test.ts +71 -0
  158. package/src/scripts/verify-migrations.ts +364 -0
  159. package/src/scripts/verify-spa-shell.ts +56 -0
  160. package/src/scripts/wrangler-config/cli.ts +51 -0
  161. package/src/scripts/wrangler-config/patches.ts +157 -0
  162. package/src/scripts/wrangler-config/resources.ts +330 -0
  163. package/src/scripts/wrangler-config/types.ts +113 -0
  164. package/src/test/integration-migrations.test.ts +169 -0
  165. package/src/test/integration-migrations.ts +267 -0
  166. package/src/test/mocks/cloudflare-workers.ts +115 -0
  167. package/src/vite.test.ts +83 -0
  168. package/src/vite.ts +202 -0
  169. package/src/vitest-web.ts +109 -0
  170. package/src/vitest.ts +185 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rexezuge
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 CHANGED
@@ -1,3 +1,298 @@
1
- # Temporary Holding Version
1
+ # @rexezuge/tooling
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Config factories, repo scripts, test helpers, and CI templates for the
4
+ Rexezuge-CloudflareWorkers family.
5
+
6
+ **Zero internal dependencies.** The config factories, the Pages proxy and the test
7
+ helpers import nothing from another `@rexezuge/*` package. The single exception is
8
+ `scripts/backup/encrypt-backup.ts`, which uses `@rexezuge/d1`'s AES-GCM so a backup
9
+ and the workers share one definition of "what an encryption key looks like"; that
10
+ dependency is declared on this package and is only ever resolved in a consumer
11
+ repo, where the kit is installed alongside `@rexezuge/d1`.
12
+
13
+ Everything here is a convergence of the same file copied across ten monorepos. Each
14
+ export carries a header comment naming the repos it came from and the decision the
15
+ convergence made — those headers are the documentation.
16
+
17
+ ---
18
+
19
+ ## 1. Config factories
20
+
21
+ Four factories, four lines of consumer wiring each.
22
+
23
+ ### ESLint
24
+
25
+ ```js
26
+ // eslint.config.mjs — the whole file
27
+ import { defineRepoLintConfig } from '@rexezuge/tooling/eslint';
28
+
29
+ export default defineRepoLintConfig({ scope: '@my-app' });
30
+ ```
31
+
32
+ Converged from Durable-DAV's 436-line `eslint.config.mjs` (the layered
33
+ `no-restricted-imports` boundary rules), Edge-Sonic's 730-line config (which lints
34
+ `test/**`, turns `unicorn/prefer-ternary` off with its reasons, and covers the
35
+ `import()` hole in `no-restricted-imports` with a matching `no-restricted-syntax`
36
+ rule), Mail-Meow's hard-error formatting gate, and AWS's `allowTypeImports` DAO
37
+ boundary.
38
+
39
+ The layer table is parameterized by `scope`, so `@my-app/api` is banned from
40
+ `apps/api` exactly as `@durable-dav/api` was. Canonical decisions:
41
+
42
+ - `prettier/prettier` is an **error**, not a warning. `pnpm run lint` is
43
+ `eslint --fix --quiet` in most repos, and `--quiet` discards warnings — so a
44
+ `prettier/prettier` warning is a silent no-op. Edge-Sonic had 242 files in that
45
+ state: the gate had never run.
46
+ - `test/**` is **linted**, not ignored, with a narrow override block for Vitest
47
+ idioms. The source repos ignored it, and ~200 KB of test code had accumulated
48
+ violations nothing reported.
49
+ - `apps/web` gets `globals.node` **plus** `globals.browser`, and is banned from
50
+ every `@scope/*` package — the SPA's own copy of a shared helper is the
51
+ convention, and a lint gate that does not say so is a convention.
52
+
53
+ A consumer that needs `eslint-plugin-react-hooks` appends it, because the kit stays
54
+ zero-dependency and the plugin belongs to the app:
55
+
56
+ ```js
57
+ import reactHooks from 'eslint-plugin-react-hooks';
58
+
59
+ export default [
60
+ ...defineRepoLintConfig({ scope: '@my-app' }),
61
+ {
62
+ files: ['apps/web/**/*.{ts,tsx}'],
63
+ plugins: { 'react-hooks': reactHooks },
64
+ rules: reactHooks.configs['recommended-latest'].rules,
65
+ },
66
+ ];
67
+ ```
68
+
69
+ ### Vitest — the worker and packages half
70
+
71
+ ```ts
72
+ // vitest.config.mts
73
+ import { defineUnitVitestConfig } from '@rexezuge/tooling/vitest';
74
+
75
+ export default defineUnitVitestConfig({
76
+ coverageInclude: ['apps/api/src/**/*.ts', 'apps/background/src/**/*.ts', 'packages/**/src/**/*.ts'],
77
+ thresholds: { statements: 60, branches: 55, functions: 65, lines: 60 },
78
+ });
79
+ ```
80
+
81
+ v8 coverage with the converged include/exclude patterns (scripts, tests, barrels,
82
+ generated files, type-only modules), `cloudflare:workers` / `cloudflare:sockets`
83
+ aliased to the bundled Node stand-ins, and `test/integration/**` plus every
84
+ `web-*` suite excluded so this config and the next one partition the suite rather
85
+ than each running all of it. Vitest 4 removed `coverage.all`; an explicit
86
+ `include` now reports files no test imported, which is the behaviour it was there
87
+ to get.
88
+
89
+ **Thresholds are a consumer argument, not a kit default.** A floor is a per-repo
90
+ ratchet measured against that repo's suite; the kit cannot know a number, and
91
+ guessing one is how floors get lowered.
92
+
93
+ ### Vitest — the SPA half
94
+
95
+ ```ts
96
+ // vitest.web.config.mts
97
+ import { defineWebVitestConfig } from '@rexezuge/tooling/vitest-web';
98
+
99
+ export default defineWebVitestConfig({
100
+ plugins: [react()],
101
+ thresholds: { statements: 55, branches: 52, functions: 54, lines: 56 },
102
+ });
103
+ ```
104
+
105
+ Converged from Durable-DAV-Router's and Mail-Otter's `vitest.web.config.mts`:
106
+ jsdom for every file, `pool: 'threads'` bounded rather than a fork per file (a
107
+ jsdom document per fork is what exhausts memory and dies with a bare "Worker
108
+ exited unexpectedly"), and both suite-naming conventions collected
109
+ (`test/**/web-*.test.{ts,tsx}` and `apps/web/**/*.test.{ts,tsx}`).
110
+
111
+ ### Vite — the SPA build
112
+
113
+ ```ts
114
+ // apps/web/vite.config.ts
115
+ import react from '@vitejs/plugin-react';
116
+ import { defineWebViteConfig, spaShellEmbedPlugin } from '@rexezuge/tooling/vite';
117
+
118
+ export default defineWebViteConfig({
119
+ plugins: [react(), tailwindcss()],
120
+ proxy: {
121
+ '/user': { target: 'http://localhost:8787', changeOrigin: true },
122
+ '/rest': { target: 'http://localhost:8787', changeOrigin: true },
123
+ },
124
+ // The ONE repo-specific piece, passed in rather than hardcoded: the worker and
125
+ // the generated-module path differ per repo.
126
+ spaShell: { path: 'apps/api/src/generated/spa-shell.ts' },
127
+ });
128
+ ```
129
+
130
+ `spaShellEmbedPlugin(target)` is the converged body of all eight copies of the
131
+ `spa-shell-embed` plugin, including Edge-Sonic's fix of skipping a build that
132
+ emitted no `dist/index.html` rather than throwing. Paths resolve from the directory
133
+ the build runs in — **not** from this module, which would point inside
134
+ `node_modules`.
135
+
136
+ ---
137
+
138
+ ## 2. Pages proxy
139
+
140
+ ```ts
141
+ // functions/[[path]].ts
142
+ import { createPagesProxy } from '@rexezuge/tooling/functions/pages-proxy';
143
+
144
+ export const onRequest = createPagesProxy();
145
+ ```
146
+
147
+ The converged `functions/[[path]].ts` shim, verified against Durable-DAV,
148
+ Mail-Meow and Edge-Git. It strips hop-by-hop and client-forwarding headers, sets
149
+ `X-Forwarded-Host/Proto/Uri` from values it reads off the request rather than the
150
+ client, replaces `X-Forwarded-For` from `CF-Connecting-IP` (never conditionally —
151
+ that is the defect Mail-Meow recorded fixing), forwards the body as a stream with
152
+ `duplex: 'half'` for anything but GET/HEAD, and hands the request to
153
+ `env.API_WORKER`.
154
+
155
+ Structural `Fetcher` / `PagesFunction` types: no `@cloudflare/workers-types`
156
+ import, so the module typechecks without a generated `worker-configuration.d.ts`.
157
+
158
+ `createPagesProxy({ forwardClientIp: false })` is the Edge-Sonic variant, for a
159
+ worker that refuses `X-Forwarded-For` outright.
160
+
161
+ ---
162
+
163
+ ## 3. Test helpers
164
+
165
+ ### `test/mocks/cloudflare-workers` — the `cloudflare:workers` stand-in
166
+
167
+ ```ts
168
+ // test/mocks/cloudflare-workers.ts (copy this file verbatim)
169
+ // vitest.config.mts — defineUnitVitestConfig already aliases it
170
+ { find: 'cloudflare:workers', replacement: 'test/mocks/cloudflare-workers.ts' }
171
+ ```
172
+
173
+ Converged from the eight copies: `DurableObject`, `RpcTarget` (present because
174
+ `dofs`' `Fs` extends it, and its absence is a module-load failure rather than a
175
+ test failure), and `WorkflowEntrypoint`. The ambient `DurableObjectState` /
176
+ `ExecutionContext` shapes are declared locally, so it typechecks inside the kit and
177
+ still typechecks against a consumer's generated types.
178
+
179
+ ### `test/integration-migrations` — applying the migration directory
180
+
181
+ ```ts
182
+ import { createMigrationHelper } from '@rexezuge/tooling/test/integration-migrations';
183
+
184
+ const migrations = createMigrationHelper({ migrationsDir: new URL('../../migrations', import.meta.url).pathname });
185
+
186
+ beforeAll(async () => {
187
+ await migrations.applyMigrations(env.DB); // every file, in wrangler's order
188
+ });
189
+
190
+ it('upgrades a pre-0004 database', async () => {
191
+ await migrations.applyMigrations(env.DB, { to: '0003_href_prefix_mode.sql' }); // seed the old shape
192
+ await migrations.applyMigrations(env.DB, { from: '0004_user_identity.sql' }); // then the one under test
193
+ });
194
+ ```
195
+
196
+ A factory, because every source repo hardcodes where its migrations live. Generic
197
+ over a structural `D1DatabaseLike` — `prepare`, and `batch` when the binding has it
198
+ — so a fake D1, a real D1 under workerd, and Miniflare's D1 all satisfy it. The
199
+ splitter is trigger-aware, and each file is one `batch()` whose failure names the
200
+ file and the statement.
201
+
202
+ ---
203
+
204
+ ## 4. Scripts
205
+
206
+ Every script is a standalone `.ts` entrypoint. Copy the `scripts/` tree into the
207
+ repo (that is the intended use — they resolve paths relative to the repository they
208
+ run in), or run it straight out of the installed package:
209
+
210
+ ```bash
211
+ pnpm exec tsx node_modules/@rexezuge/tooling/src/scripts/<name>.ts [args]
212
+ ```
213
+
214
+ | Script | What it does | Usage |
215
+ | --- | --- | --- |
216
+ | `check-god-files.ts` | 300-line warn / 400-line critical guard, warn-only by default, with an allowlist | `--root <dir>` `--allowlist <file>` `--soft 300` `--hard 400` `--fail-on-hard` |
217
+ | `ensure-spa-shell-stub.ts` | writes the empty `spa-shell.ts` stub when absent; never overwrites | `[repo-root]` |
218
+ | `verify-spa-shell.ts` | rejects a missing, stubbed, or half-refreshed SPA shell | `[repo-root]` |
219
+ | `validate-locales.ts` | two-way key parity, empty values, `{{placeholder}}` parity, base-locale bundle | `[locales-dir]` |
220
+ | `verify-migrations.ts` | migration names, numbering, digests against the lock; read-only | `[migrations-dir]` |
221
+ | `migrations-lock.ts` | `--write` for the above: add-only lock update | `[migrations-dir]` |
222
+ | `prepare-wrangler-config.ts` | materializes `wrangler.jsonc` and provisions the resources it names | env-driven |
223
+ | `init-secrets.ts` | creates the declared Secrets Store entries, generate-if-absent | env-driven |
224
+ | `backup/encrypt-backup.ts` | compresses and AES-GCM-encrypts an exported dump | `BACKUP_ENCRYPTION_KEY` |
225
+
226
+ The rules each script enforces live in a module beside it, so the gates are unit
227
+ tested here: `god-files` rules in `check-god-files.ts`, `spa-shell` rules in
228
+ `spa-shell.ts`, locale rules in `validate-locales.ts`, migration rules in
229
+ `verify-migrations.ts`.
230
+
231
+ ### The three that are gates, not chores
232
+
233
+ **`check-god-files.ts`** keeps every source repo's thresholds but makes the verdict
234
+ warn-only until the repo opts in with `--fail-on-hard`. The reason is in the
235
+ module: a ceiling set above the largest file in the tree measures nothing, and
236
+ Durable-DAV's own notes record ten files just over 300 with a hard limit that had
237
+ never fired. `scripts/god-files.allowlist.json` (a JSON array of repo-relative
238
+ paths, or `dir/` prefixes) records the known offenders so the report names only
239
+ what is new.
240
+
241
+ **`verify-migrations.ts` / `migrations-lock.ts`** exist because D1 records which
242
+ migrations it applied but not what they contained, so editing an applied migration
243
+ is invisible to every other tool. The lock is forward-only, `--write` is add-only
244
+ (an `edited` finding survives a write), and a squashed baseline exempts exactly the
245
+ set a squash rewrites or absorbs.
246
+
247
+ **`verify-spa-shell.ts`** is the only thing standing between a fresh clone and a
248
+ deploy that serves a blank page: both `apps/web/dist/` and the generated
249
+ `spa-shell.ts` are gitignored, and `postinstall` writes a deliberately empty stub.
250
+ Known limit, stated in the module: a *stale but self-consistent* pair passes.
251
+
252
+ ### The backup helpers
253
+
254
+ `backup/{d1-target,destination-config,naming,retention,encrypt-backup}.ts` are the
255
+ generic half of Edge-Sonic's backup suite — the rules, not the upload jobs.
256
+ `d1-target.ts` refuses to export a database that was auto-provisioned moments ago,
257
+ `destination-config.ts` fails closed on an unencrypted destination, `naming.ts`
258
+ pins the artifact format (the workflow's upload glob and this prefix are one fact in
259
+ two places), `retention.ts` rejects a bad window rather than defaulting one, and
260
+ `encrypt-backup.ts` does the crypto in-process with the kit's own AES-GCM.
261
+
262
+ **Left local, deliberately:** `s3-prune.ts`, `upload-s3.ts`, `upload-webdav.ts`,
263
+ `webdav-target.ts` (destination-specific: S3 credentials, rclone, and a WebDAV
264
+ client are per-repo configuration rather than shared rules), `compare-reference.ts`
265
+ and `change-email.ts` (business logic and personal-data operations, respectively —
266
+ neither is tooling).
267
+
268
+ ---
269
+
270
+ ## 5. GitHub templates
271
+
272
+ `src/github/` holds the converged family workflows and composite actions:
273
+
274
+ ```
275
+ src/github/
276
+ workflows/continuous-integration.yml # the superset of nine repos' CI jobs
277
+ workflows/continuous-deployment.yml # workflow_run-gated deploy, worker + pages
278
+ workflows/scheduled-version-update.yml # .github/.version bump
279
+ workflows/backup-main.yml # secret-gated mirrors to Azure DevOps / GitLab
280
+ actions/setup-env/action.yml # pnpm + node store cache
281
+ actions/retry-step/action.yml # retry a bash command, annotate the attempts
282
+ dependabot.yml # weekly, with the @rexezuge/* group
283
+ ```
284
+
285
+ Copy the tree to the repo root. Placeholders are marked `REPLACE` and listed at the
286
+ top of each file; the one that must change for the workflows to run at all is the
287
+ `pnpm --filter @<scope>/web build` invocation in `continuous-integration.yml` and
288
+ `continuous-deployment.yml`.
289
+
290
+ ---
291
+
292
+ ## 6. Verifying this package
293
+
294
+ ```bash
295
+ pnpm --filter @rexezuge/tooling exec tsc -p tsconfig.json
296
+ pnpm --filter @rexezuge/tooling typecheck
297
+ pnpm --filter @rexezuge/tooling test
298
+ ```
@@ -0,0 +1,75 @@
1
+ import type tseslint from 'typescript-eslint';
2
+ /**
3
+ * The layer table behind the repo lint config's import boundaries.
4
+ *
5
+ * Provenance: the seven `no-restricted-imports` blocks in Durable-DAV's
6
+ * `eslint.config.mjs`, with `@durable-dav/` replaced by the caller's scope.
7
+ * Extracted from `eslint.ts` so the boundary rules — the part a repo actually
8
+ * argues about — are their own module rather than the tail of a 640-line
9
+ * config file.
10
+ *
11
+ * The table is data rather than inline blocks so the dynamic-`import()` guard
12
+ * is generated from the same list: a rule that derived its banned set from
13
+ * another rule's configuration would be a second source of truth for the layer
14
+ * table, and that table is what the docs describe.
15
+ */
16
+ /**
17
+ * The directories the boundary rules cover, in the canonical layout.
18
+ *
19
+ * Kept here, beside the table it scopes: `defineRepoLintConfig` filters the
20
+ * canonical layers by `projectDirs`, and a reader of the layer table should not
21
+ * have to open the config assembly to learn which directories it covers.
22
+ */
23
+ export declare const DEFAULT_PROJECT_DIRS: readonly string[];
24
+ /**
25
+ * One layer's ban list, as the repo-relative package suffixes it may not import.
26
+ *
27
+ * Converged from the four source stacks. `backend-data` and `backend-services`
28
+ * appear in all four; `webdav`/`dav-store` are Durable-DAV's, `provider-clients`
29
+ * is Mail-Meow's and AWS's. A layer only bans what exists in its repo, so the
30
+ * union is safe: a repo without `provider-clients` simply never imports it.
31
+ */
32
+ export interface LayerBan {
33
+ /** Directory the rule is scoped to. */
34
+ readonly dir: string;
35
+ /** Package suffixes (relative to `scope`) that may not be imported from here. */
36
+ readonly ban: readonly string[];
37
+ /** Message shown when the rule fires. */
38
+ readonly message: string;
39
+ /** Type-only imports are allowed, for the DAO boundary in `apps/api`. */
40
+ readonly allowTypeImports?: boolean;
41
+ }
42
+ /**
43
+ * The layer table, parameterized by `scope`.
44
+ */
45
+ export declare function layerBans(scope: string): readonly LayerBan[];
46
+ /**
47
+ * One argument to `tseslint.config()`, named here so the boundary builders
48
+ * return the assembled type rather than a lookalike the assembly then has to
49
+ * be argued into accepting.
50
+ */
51
+ type ConfigInput = Parameters<typeof tseslint.config>[number];
52
+ /**
53
+ * The `no-restricted-imports` entries for one layer.
54
+ *
55
+ * Written as `group` patterns rather than exact `paths`, so `@myapp/backend-data`
56
+ * and `@myapp/backend-data/dao` are both caught by one entry. `allowTypeImports`
57
+ * is passed through only where the source repos set it, because `apps/api`
58
+ * legitimately types its filters and models against DAO shapes.
59
+ */
60
+ export declare function restrictedImports(bans: readonly LayerBan[]): ConfigInput[];
61
+ /**
62
+ * The dynamic-`import()` guard, generated from the same table.
63
+ *
64
+ * `no-restricted-imports` reads `ImportDeclaration` nodes only; a dynamic
65
+ * `import()` is an `ImportExpression`, and this ESLint version does not report it
66
+ * under that rule at all. Edge-Sonic verified this rather than assumed it — a
67
+ * static import of a restricted specifier from the same file is an `error`, and
68
+ * the identical specifier reached through `await import(...)` is silent. That
69
+ * hole was load-bearing there: the credential path reached a banned module
70
+ * through it. `no-restricted-syntax` sees `ImportExpression`, so the boundary is
71
+ * stated a second way.
72
+ */
73
+ export declare function restrictedDynamicImports(bans: readonly LayerBan[]): ConfigInput[];
74
+ export {};
75
+ //# sourceMappingURL=eslint-boundaries.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eslint-boundaries.d.ts","sourceRoot":"","sources":["../src/eslint-boundaries.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,mBAAmB,CAAC;AAE9C;;;;;;;;;;;;;GAaG;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,EAAE,SAAS,MAAM,EASjD,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,WAAW,QAAQ;IACvB,uCAAuC;IACvC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,iFAAiF;IACjF,QAAQ,CAAC,GAAG,EAAE,SAAS,MAAM,EAAE,CAAC;IAChC,yCAAyC;IACzC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,yEAAyE;IACzE,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAC;CACrC;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,QAAQ,EAAE,CAmD5D;AAED;;;;GAIG;AACH,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC;AAE9D;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,SAAS,QAAQ,EAAE,GAAG,WAAW,EAAE,CAgB1E;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,SAAS,QAAQ,EAAE,GAAG,WAAW,EAAE,CAajF"}
@@ -0,0 +1,138 @@
1
+ /**
2
+ * The layer table behind the repo lint config's import boundaries.
3
+ *
4
+ * Provenance: the seven `no-restricted-imports` blocks in Durable-DAV's
5
+ * `eslint.config.mjs`, with `@durable-dav/` replaced by the caller's scope.
6
+ * Extracted from `eslint.ts` so the boundary rules — the part a repo actually
7
+ * argues about — are their own module rather than the tail of a 640-line
8
+ * config file.
9
+ *
10
+ * The table is data rather than inline blocks so the dynamic-`import()` guard
11
+ * is generated from the same list: a rule that derived its banned set from
12
+ * another rule's configuration would be a second source of truth for the layer
13
+ * table, and that table is what the docs describe.
14
+ */
15
+ /**
16
+ * The directories the boundary rules cover, in the canonical layout.
17
+ *
18
+ * Kept here, beside the table it scopes: `defineRepoLintConfig` filters the
19
+ * canonical layers by `projectDirs`, and a reader of the layer table should not
20
+ * have to open the config assembly to learn which directories it covers.
21
+ */
22
+ export const DEFAULT_PROJECT_DIRS = [
23
+ 'packages/shared',
24
+ 'packages/backend-errors',
25
+ 'packages/backend-runtime',
26
+ 'packages/backend-data',
27
+ 'packages/backend-services',
28
+ 'apps/api',
29
+ 'apps/background',
30
+ 'apps/web',
31
+ ];
32
+ /**
33
+ * The layer table, parameterized by `scope`.
34
+ */
35
+ export function layerBans(scope) {
36
+ const at = (name) => `${scope}/${name}`;
37
+ return [
38
+ {
39
+ dir: 'packages/shared',
40
+ ban: [at('*')],
41
+ message: `${scope} packages must not import from other ${scope} packages — this is a zero-dependency base layer`,
42
+ },
43
+ {
44
+ dir: 'packages/backend-errors',
45
+ ban: [at('*')],
46
+ message: `${scope} packages must not import from other ${scope} packages — this is a zero-dependency base layer`,
47
+ },
48
+ {
49
+ dir: 'packages/backend-runtime',
50
+ ban: [at('backend-data'), at('webdav'), at('backend-services'), at('api'), at('background')],
51
+ message: 'backend-runtime must not import from a higher layer',
52
+ },
53
+ {
54
+ dir: 'packages/backend-data',
55
+ ban: [at('backend-runtime'), at('webdav'), at('backend-services'), at('api'), at('background')],
56
+ message: 'backend-data must not import from a higher layer',
57
+ },
58
+ {
59
+ dir: 'packages/backend-services',
60
+ ban: [at('api'), at('background')],
61
+ message: 'backend-services must not import from apps',
62
+ },
63
+ {
64
+ dir: 'apps/api',
65
+ ban: [at('backend-data/dao'), at('backend-data')],
66
+ allowTypeImports: true,
67
+ message: 'apps/api must not import DAOs or backend-data values directly; use backend-services instead',
68
+ },
69
+ {
70
+ dir: 'apps/background',
71
+ ban: [at('api')],
72
+ message: 'apps/background must not import from apps/api',
73
+ },
74
+ // The browser half, and the only layer that bans the namespace wholesale
75
+ // rather than a list of siblings: a backend package imported by the SPA
76
+ // typechecks and then fails at bundle time, with the error pointing at the
77
+ // bundler rather than at the boundary. Edge-Sonic is the source; the others had
78
+ // no block for it at all, which is how their layer table's "may import: the
79
+ // browser" was a convention instead of a gate.
80
+ {
81
+ dir: 'apps/web',
82
+ ban: [at('*')],
83
+ message: `${at('apps/web')} ships zero backend dependencies — this module is the SPA's own copy`,
84
+ },
85
+ ];
86
+ }
87
+ /**
88
+ * The `no-restricted-imports` entries for one layer.
89
+ *
90
+ * Written as `group` patterns rather than exact `paths`, so `@myapp/backend-data`
91
+ * and `@myapp/backend-data/dao` are both caught by one entry. `allowTypeImports`
92
+ * is passed through only where the source repos set it, because `apps/api`
93
+ * legitimately types its filters and models against DAO shapes.
94
+ */
95
+ export function restrictedImports(bans) {
96
+ return bans.map(({ dir, ban, message, allowTypeImports }) => ({
97
+ files: [`${dir}/**/*.{ts,js,mts,cts,mjs,cjs}`],
98
+ rules: {
99
+ 'no-restricted-imports': [
100
+ 'error',
101
+ {
102
+ patterns: ban.map((group) => ({
103
+ group: [group, `${group}/*`],
104
+ message,
105
+ ...(allowTypeImports === true && { allowTypeImports: true }),
106
+ })),
107
+ },
108
+ ],
109
+ },
110
+ }));
111
+ }
112
+ /**
113
+ * The dynamic-`import()` guard, generated from the same table.
114
+ *
115
+ * `no-restricted-imports` reads `ImportDeclaration` nodes only; a dynamic
116
+ * `import()` is an `ImportExpression`, and this ESLint version does not report it
117
+ * under that rule at all. Edge-Sonic verified this rather than assumed it — a
118
+ * static import of a restricted specifier from the same file is an `error`, and
119
+ * the identical specifier reached through `await import(...)` is silent. That
120
+ * hole was load-bearing there: the credential path reached a banned module
121
+ * through it. `no-restricted-syntax` sees `ImportExpression`, so the boundary is
122
+ * stated a second way.
123
+ */
124
+ export function restrictedDynamicImports(bans) {
125
+ return bans.map(({ dir, ban, message }) => ({
126
+ files: [`${dir}/**/*.{ts,js,mts,cts,mjs,cjs}`],
127
+ rules: {
128
+ 'no-restricted-syntax': [
129
+ 'error',
130
+ ...ban.map((specifier) => ({
131
+ selector: `ImportExpression[source.type='Literal'][source.value=/^${specifier.replaceAll('/', '\\/')}/]`,
132
+ message: `A dynamic import() is not covered by no-restricted-imports, so this layer reaches \`${specifier}\` through the one syntax that gate does not see. Both say no: ${message}`,
133
+ })),
134
+ ],
135
+ },
136
+ }));
137
+ }
138
+ //# sourceMappingURL=eslint-boundaries.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eslint-boundaries.js","sourceRoot":"","sources":["../src/eslint-boundaries.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The rule-override blocks of the repo lint config.
3
+ *
4
+ * Extracted from `eslint.ts` with the boundary rules: each block is a plain
5
+ * rules object with the provenance comment that says *why* it is set to this
6
+ * value, because a per-rule decision with no reason attached is a decision the
7
+ * next reader re-litigates. `eslint.ts` composes these; this file is where a
8
+ * rule's story lives.
9
+ */
10
+ /**
11
+ * TypeScript rules, identical in all four source stacks.
12
+ */
13
+ export declare const TYPESCRIPT_RULES: Readonly<Record<string, unknown>>;
14
+ /**
15
+ * Unicorn and SonarJS rules carried from the source repos, each off for a
16
+ * stated reason.
17
+ *
18
+ * This is the family's "deliberately does not follow" block: every entry is
19
+ * off because complying would make the code worse for what the project is,
20
+ * and each is listed rather than dropped so the decision stays reviewable.
21
+ */
22
+ export declare const UNICORN_RULES: Readonly<Record<string, unknown>>;
23
+ /**
24
+ * SonarJS rules the sources turned from errors into advisories, with the
25
+ * threshold raised to a value that has actually held.
26
+ */
27
+ export declare const SONARJS_RULES: Readonly<Record<string, unknown>>;
28
+ /**
29
+ * Test-suite overrides.
30
+ *
31
+ * `test/**` is deliberately *not* in the config's ignores list. Durable-DAV,
32
+ * AWS and Mail-Meow all ignored it, and ~200 KB of test code had accumulated
33
+ * violations that nothing reported. This block puts the suite back under the
34
+ * same rules as the rest of the workspace and switches off only the handful
35
+ * whose findings are structural artefacts of test code rather than defects.
36
+ * It does not disable correctness rules: the unused-variable, dead-store,
37
+ * regex and sorting findings in the suite are real and are fixed, not
38
+ * suppressed.
39
+ */
40
+ export declare const TEST_RULES: Readonly<Record<string, unknown>>;
41
+ //# sourceMappingURL=eslint-rules.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"eslint-rules.d.ts","sourceRoot":"","sources":["../src/eslint-rules.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH;;GAEG;AACH,eAAO,MAAM,gBAAgB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAsB9D,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAsI3D,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAoB3D,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,UAAU,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAwDxD,CAAC"}