@acmekit/acmekit 2.13.89 → 2.13.91
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.
- package/dist/commands/plugin/develop.d.ts.map +1 -1
- package/dist/commands/plugin/develop.js +92 -68
- package/dist/commands/plugin/develop.js.map +1 -1
- package/dist/templates/app/.claude/rules/admin-components.md +131 -111
- package/dist/templates/app/.claude/rules/admin-data.md +2 -0
- package/dist/templates/app/.claude/rules/admin-patterns.md +39 -31
- package/dist/templates/app/.claude/rules/admin-ui.md +28 -0
- package/dist/templates/app/.claude/rules/api-routes.md +30 -9
- package/dist/templates/app/.claude/rules/modules.md +119 -2
- package/dist/templates/app/.claude/skills/build-feature/SKILL.md +2 -0
- package/dist/templates/app/CLAUDE.md +1 -0
- package/dist/templates/plugin/.claude/rules/admin-components.md +131 -111
- package/dist/templates/plugin/.claude/rules/admin-data.md +2 -0
- package/dist/templates/plugin/.claude/rules/admin-patterns.md +39 -31
- package/dist/templates/plugin/.claude/rules/admin-ui.md +28 -0
- package/dist/templates/plugin/.claude/rules/api-routes.md +30 -9
- package/dist/templates/plugin/.claude/rules/modules.md +118 -1
- package/dist/templates/plugin/.claude/skills/build-feature/SKILL.md +2 -0
- package/dist/templates/plugin/CLAUDE.md +1 -0
- package/package.json +39 -39
|
@@ -867,7 +867,17 @@ import { sdk } from "../../../../lib/sdk"
|
|
|
867
867
|
type Currency = { code: string; name: string }
|
|
868
868
|
const columnHelper = createDataTableColumnHelper<Currency>()
|
|
869
869
|
|
|
870
|
+
// Page component — renders RouteFocusModal, delegates form to child
|
|
870
871
|
const AddCurrenciesPage = () => {
|
|
872
|
+
return (
|
|
873
|
+
<RouteFocusModal>
|
|
874
|
+
<AddCurrenciesForm />
|
|
875
|
+
</RouteFocusModal>
|
|
876
|
+
)
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Form component — useRouteModal() is valid here (inside RouteFocusModal)
|
|
880
|
+
const AddCurrenciesForm = () => {
|
|
871
881
|
const queryClient = useQueryClient()
|
|
872
882
|
const { handleSuccess } = useRouteModal()
|
|
873
883
|
|
|
@@ -924,37 +934,35 @@ const AddCurrenciesPage = () => {
|
|
|
924
934
|
})
|
|
925
935
|
|
|
926
936
|
return (
|
|
927
|
-
<RouteFocusModal>
|
|
928
|
-
<
|
|
929
|
-
<
|
|
930
|
-
<
|
|
931
|
-
|
|
932
|
-
{form.formState.errors.currencies
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
<
|
|
938
|
-
<DataTable
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
<
|
|
946
|
-
<
|
|
947
|
-
<
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
</RouteFocusModal.Form>
|
|
957
|
-
</RouteFocusModal>
|
|
937
|
+
<RouteFocusModal.Form form={form}>
|
|
938
|
+
<KeyboundForm onSubmit={handleSubmit} className="flex h-full flex-col overflow-hidden">
|
|
939
|
+
<RouteFocusModal.Header>
|
|
940
|
+
<div className="flex flex-1 items-center justify-between">
|
|
941
|
+
{form.formState.errors.currencies && (
|
|
942
|
+
<Hint variant="error">{form.formState.errors.currencies.message}</Hint>
|
|
943
|
+
)}
|
|
944
|
+
</div>
|
|
945
|
+
</RouteFocusModal.Header>
|
|
946
|
+
<RouteFocusModal.Body className="flex flex-1 flex-col overflow-hidden">
|
|
947
|
+
<DataTable instance={table}>
|
|
948
|
+
<DataTable.Table emptyState={{
|
|
949
|
+
empty: { heading: "No currencies available" },
|
|
950
|
+
}} />
|
|
951
|
+
<DataTable.Pagination />
|
|
952
|
+
</DataTable>
|
|
953
|
+
</RouteFocusModal.Body>
|
|
954
|
+
<RouteFocusModal.Footer>
|
|
955
|
+
<div className="flex items-center justify-end gap-x-2">
|
|
956
|
+
<RouteFocusModal.Close asChild>
|
|
957
|
+
<Button size="small" variant="secondary">Cancel</Button>
|
|
958
|
+
</RouteFocusModal.Close>
|
|
959
|
+
<Button size="small" type="submit" isLoading={isPending}>
|
|
960
|
+
Save
|
|
961
|
+
</Button>
|
|
962
|
+
</div>
|
|
963
|
+
</RouteFocusModal.Footer>
|
|
964
|
+
</KeyboundForm>
|
|
965
|
+
</RouteFocusModal.Form>
|
|
958
966
|
)
|
|
959
967
|
}
|
|
960
968
|
|
|
@@ -422,6 +422,34 @@ const SettingsPage = () => {
|
|
|
422
422
|
}
|
|
423
423
|
```
|
|
424
424
|
|
|
425
|
+
### Never Install react-hook-form, @hookform/resolvers, or zod Directly
|
|
426
|
+
|
|
427
|
+
These packages are **provided by AcmeKit** at specific pinned versions. Installing your own version causes version conflicts:
|
|
428
|
+
|
|
429
|
+
- `@hookform/resolvers@5.x` requires `zod/v4/core` (Zod v4) — AcmeKit ships Zod v3 → **build fails** with `Could not resolve "zod/v4/core"`
|
|
430
|
+
- `react-hook-form` dual instances → `useFormContext()` returns `null` → form fields crash
|
|
431
|
+
- Bare `zod` import → **build fails** in plugins with `Could not resolve "zod"`
|
|
432
|
+
|
|
433
|
+
**For plugins**: `react-hook-form` and `@hookform/resolvers` go in `peerDependencies` + `devDependencies` only (never `dependencies`). Match the host app's versions.
|
|
434
|
+
|
|
435
|
+
**For apps**: Do not add these packages at all — they come from `@acmekit/ui`.
|
|
436
|
+
|
|
437
|
+
**Imports**:
|
|
438
|
+
- Admin UI: `import * as zod from "@acmekit/deps/zod"` — never bare `"zod"`
|
|
439
|
+
- Backend: `import { z } from "@acmekit/framework/zod"` — never bare `"zod"`
|
|
440
|
+
- `useForm` from `react-hook-form` and `zodResolver` from `@hookform/resolvers/zod` — these are provided by the host
|
|
441
|
+
|
|
442
|
+
### Vite "Outdated Optimize Dep" 504 Errors
|
|
443
|
+
|
|
444
|
+
After changing plugin dependencies, Vite's pre-bundled dependency cache becomes stale. The browser shows `504 (Outdated Optimize Dep)` for multiple modules.
|
|
445
|
+
|
|
446
|
+
**Fix**: Stop the dev server, delete `.vite/deps/` in the host app's `node_modules/`, and restart:
|
|
447
|
+
|
|
448
|
+
```bash
|
|
449
|
+
rm -rf node_modules/.vite/deps
|
|
450
|
+
# restart dev server
|
|
451
|
+
```
|
|
452
|
+
|
|
425
453
|
---
|
|
426
454
|
|
|
427
455
|
## Key Rules
|
|
@@ -42,7 +42,7 @@ Co-locate validation, query config, response schemas, rate limiting, and RBAC po
|
|
|
42
42
|
When extending generated CRUD routes, keep the existing `query-config.ts` / `helpers.ts` structure and only adjust fields, filters, workflow calls, or refetch logic.
|
|
43
43
|
|
|
44
44
|
```typescript
|
|
45
|
-
import { z } from "zod"
|
|
45
|
+
import { z } from "@acmekit/framework/zod"
|
|
46
46
|
import {
|
|
47
47
|
defineRouteConfig,
|
|
48
48
|
AuthenticatedAcmeKitTypedRequest,
|
|
@@ -532,8 +532,9 @@ createProductsWorkflow.hooks.productsCreated(
|
|
|
532
532
|
|
|
533
533
|
## Zod Import Note
|
|
534
534
|
|
|
535
|
-
-
|
|
536
|
-
-
|
|
535
|
+
- **Always** use `import { z } from "@acmekit/framework/zod"` — never bare `"zod"`
|
|
536
|
+
- Bare `"zod"` may resolve in apps but **fails in plugins** (zod is not a direct dependency)
|
|
537
|
+
- This applies to both route files and `middlewares.ts`
|
|
537
538
|
|
|
538
539
|
## Common Imports
|
|
539
540
|
|
|
@@ -546,8 +547,7 @@ import {
|
|
|
546
547
|
defineMiddlewares,
|
|
547
548
|
AcmeKitNextFunction,
|
|
548
549
|
} from "@acmekit/framework/http"
|
|
549
|
-
import { z } from "zod"
|
|
550
|
-
import { z as frameworkZod } from "@acmekit/framework/zod" // for middlewares.ts
|
|
550
|
+
import { z } from "@acmekit/framework/zod"
|
|
551
551
|
import { ContainerRegistrationKeys, AcmeKitError } from "@acmekit/framework/utils"
|
|
552
552
|
```
|
|
553
553
|
|
|
@@ -737,11 +737,10 @@ export const POST = async (req, res) => {
|
|
|
737
737
|
// RIGHT — use :param format
|
|
738
738
|
{ matcher: "/custom/:id", middlewares: [...] } // ✅
|
|
739
739
|
|
|
740
|
-
// WRONG —
|
|
741
|
-
import { z } from "zod" // ❌
|
|
742
|
-
additionalDataValidator: { brand: z.string() }
|
|
740
|
+
// WRONG — bare "zod" import (fails in plugins, not a direct dependency)
|
|
741
|
+
import { z } from "zod" // ❌ never use bare zod
|
|
743
742
|
|
|
744
|
-
// RIGHT — use framework
|
|
743
|
+
// RIGHT — always use framework re-export
|
|
745
744
|
import { z } from "@acmekit/framework/zod" // ✅
|
|
746
745
|
|
|
747
746
|
// WRONG — no .strict() on body schema (allows extra fields through)
|
|
@@ -772,4 +771,26 @@ const { enabled, ...filters } = req.validatedQuery // ❌ crashes if request ha
|
|
|
772
771
|
|
|
773
772
|
// RIGHT — always provide fallback when destructuring validatedQuery
|
|
774
773
|
const { enabled, ...filters } = req.validatedQuery ?? {} // ✅ safe when no query params
|
|
774
|
+
|
|
775
|
+
// WRONG — seeding/syncing data inside a route handler (runs on every request)
|
|
776
|
+
export const GET = async (req, res) => {
|
|
777
|
+
const service = req.scope.resolve(MY_MODULE)
|
|
778
|
+
await service.syncProviders() // ❌ data mutation on every GET — slow, side-effectful
|
|
779
|
+
const data = await query.graph({ entity: "my_entity" })
|
|
780
|
+
res.json({ items: data })
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// RIGHT — routes are pure query/mutation handlers, no seeding or syncing
|
|
784
|
+
// Seed data via: createProvidersLoader onRegistered, onPluginStart, or scripts
|
|
785
|
+
export const GET = async (req, res) => {
|
|
786
|
+
const data = await query.graph({ entity: "my_entity" })
|
|
787
|
+
res.json({ items: data }) // ✅ just reads data
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// WRONG — importing service class directly in route file for type
|
|
791
|
+
import type MyModuleService from "../../../modules/my/service" // ❌ tight coupling
|
|
792
|
+
const svc = req.scope.resolve<MyModuleService>(MY_MODULE)
|
|
793
|
+
|
|
794
|
+
// RIGHT — resolve via module constant, type-cast loosely if needed
|
|
795
|
+
const svc: any = req.scope.resolve(MY_MODULE) // ✅
|
|
775
796
|
```
|
|
@@ -97,7 +97,7 @@ export const BLOG_MODULE = "blog"
|
|
|
97
97
|
export default Module(BLOG_MODULE, { service: BlogModuleService })
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
-
The generator auto-registers modules in `acmekit-config.ts`. Verify registration after generation
|
|
100
|
+
The generator auto-registers modules in `acmekit-config.ts`. Verify registration after generation.
|
|
101
101
|
|
|
102
102
|
## Constructor Pattern
|
|
103
103
|
|
|
@@ -192,7 +192,7 @@ export default class MyModuleService {
|
|
|
192
192
|
|
|
193
193
|
**When to use loaders:** Register external connections at startup; validate required options (throw on missing to prevent startup). Loaders are idempotent — check `container.hasRegistration("key")` before re-registering.
|
|
194
194
|
|
|
195
|
-
**Don't use loaders for:** Scheduled tasks → use jobs. Event-triggered logic → use subscribers.
|
|
195
|
+
**Don't use loaders for:** Scheduled tasks → use jobs. Event-triggered logic → use subscribers. Database mutations (INSERT/UPDATE/DELETE) → use scripts, subscribers, or seed data (exception: `onRegistered` in `createProvidersLoader` is designed for DB seeding via service methods). Never resolve `PG_CONNECTION` for raw SQL in loaders — use service methods instead.
|
|
196
196
|
|
|
197
197
|
## Module Options Validation
|
|
198
198
|
|
|
@@ -262,6 +262,73 @@ export default Module(RELAYER_MODULE, {
|
|
|
262
262
|
})
|
|
263
263
|
```
|
|
264
264
|
|
|
265
|
+
### createProvidersLoader — Full API
|
|
266
|
+
|
|
267
|
+
`createProvidersLoader` accepts a `ProvidersConfig` object with these options:
|
|
268
|
+
|
|
269
|
+
| Option | Required | Default | Purpose |
|
|
270
|
+
|--------|----------|---------|---------|
|
|
271
|
+
| `prefix` | Yes | — | Container registration key prefix (e.g., `"pp_"`, `"cr_"`) |
|
|
272
|
+
| `identifiersKey` | Yes | — | Key for tracking registered provider IDs |
|
|
273
|
+
| `moduleName` | No | `"module"` | Label for log/error messages |
|
|
274
|
+
| `keyFormat` | No | `"prefixId"` | `"prefixId"` → key=`{prefix}{id}`, or `"prefixIdentifierId"` → key=`{prefix}{identifier}_{id}` |
|
|
275
|
+
| `defaults` | No | `[]` | Built-in providers shipped with the module (see below) |
|
|
276
|
+
| `defaultProviderKey` | No | — | Container key to register the elected default provider ID |
|
|
277
|
+
| `requireDefault` | No | `false` | Throw at startup if no default provider is found |
|
|
278
|
+
| `onRegistered` | No | — | Async hook called after all providers are registered |
|
|
279
|
+
|
|
280
|
+
**`defaults`** — each entry is a `DefaultProviderConfig`:
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
{
|
|
284
|
+
service: ProviderClass, // must have static `identifier`
|
|
285
|
+
id: string, // registration ID
|
|
286
|
+
options?: Record | ((moduleOptions) => Record), // static or derived from module options
|
|
287
|
+
condition?: (moduleOptions) => boolean, // skip this default if returns false
|
|
288
|
+
}
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
**`onRegistered`** — receives `{ container, providers, registeredDefaults }`:
|
|
292
|
+
|
|
293
|
+
```typescript
|
|
294
|
+
onRegistered: async ({ container, providers, registeredDefaults }) => {
|
|
295
|
+
// container: module-scoped AcmeKitContainer (module's own service is resolvable here)
|
|
296
|
+
// providers: user-configured ModuleProvider[] from config
|
|
297
|
+
// registeredDefaults: Array<{ id, options }> — only defaults that passed condition
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
**Example with defaults and onRegistered** (from core modules):
|
|
302
|
+
|
|
303
|
+
```typescript
|
|
304
|
+
createProvidersLoader({
|
|
305
|
+
prefix: PAYMENT_PROVIDER_PREFIX,
|
|
306
|
+
identifiersKey: PAYMENT_PROVIDER_IDENTIFIER_KEY,
|
|
307
|
+
moduleName: PAYMENT_MODULE,
|
|
308
|
+
defaults: [
|
|
309
|
+
{
|
|
310
|
+
service: SystemProvider,
|
|
311
|
+
id: "system",
|
|
312
|
+
condition: (opts) => opts.enableSystemProvider !== false,
|
|
313
|
+
},
|
|
314
|
+
],
|
|
315
|
+
onRegistered: async ({ container, registeredDefaults }) => {
|
|
316
|
+
// Sync DB rows for registered providers via service methods
|
|
317
|
+
const service: any = container.resolve(PAYMENT_MODULE)
|
|
318
|
+
for (const provider of registeredDefaults) {
|
|
319
|
+
const existing = await service.listPaymentProviders({ id: [provider.id] })
|
|
320
|
+
if (!existing.length) {
|
|
321
|
+
await service.createPaymentProviders([
|
|
322
|
+
{ id: provider.id, is_enabled: true },
|
|
323
|
+
])
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
})
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
**Important**: Even inside `onRegistered`, always use service methods for DB mutations — never raw SQL via `PG_CONNECTION`.
|
|
331
|
+
|
|
265
332
|
### Service (resolving providers)
|
|
266
333
|
|
|
267
334
|
Use the leading `TProvider` generic to get a typed `this.providers` registry.
|
|
@@ -767,4 +834,54 @@ AcmeKitService({ providers: { prefix: "cr_", identifiersKey: "chain_relayer_prov
|
|
|
767
834
|
|
|
768
835
|
// RIGHT — define constants once in constants.ts, import everywhere
|
|
769
836
|
import { CHAIN_RELAYER_PROVIDER_PREFIX, CHAIN_RELAYER_IDENTIFIER_KEY } from "./constants"
|
|
837
|
+
|
|
838
|
+
// WRONG — using raw SQL / PG_CONNECTION for data mutations
|
|
839
|
+
const pgConnection = container.resolve(ContainerRegistrationKeys.PG_CONNECTION)
|
|
840
|
+
await pgConnection.raw(
|
|
841
|
+
`INSERT INTO payment_provider (id) VALUES (?)`, // ❌ bypasses ORM, service layer, events
|
|
842
|
+
[id]
|
|
843
|
+
)
|
|
844
|
+
|
|
845
|
+
// RIGHT — use the module's service methods for all data mutations
|
|
846
|
+
const service = container.resolve(PAYMENT_MODULE)
|
|
847
|
+
await service.createPaymentProviders([{ id: "pp_system", is_enabled: true }]) // ✅
|
|
848
|
+
|
|
849
|
+
// WRONG — using raw SQL in onRegistered to seed provider rows
|
|
850
|
+
onRegistered: async ({ container, registeredDefaults }) => {
|
|
851
|
+
const pg = container.resolve(ContainerRegistrationKeys.PG_CONNECTION)
|
|
852
|
+
await pg.raw(`INSERT INTO ...`, [id]) // ❌ bypasses ORM
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// RIGHT — use service methods in onRegistered
|
|
856
|
+
onRegistered: async ({ container, registeredDefaults }) => {
|
|
857
|
+
const service: any = container.resolve(MY_MODULE)
|
|
858
|
+
for (const provider of registeredDefaults) {
|
|
859
|
+
const existing = await service.listMyProviders({ id: [provider.id] })
|
|
860
|
+
if (!existing.length) {
|
|
861
|
+
await service.createMyProviders([{ id: provider.id, is_enabled: true }]) // ✅
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// WRONG — lazy-syncing / seeding data inside a GET route handler
|
|
867
|
+
export const GET = async (req, res) => {
|
|
868
|
+
const service = req.scope.resolve(PAYMENT_MODULE)
|
|
869
|
+
await service.syncRegisteredProviders() // ❌ runs on EVERY request
|
|
870
|
+
const { data } = await query.graph({ entity: "payment_provider" })
|
|
871
|
+
res.json({ items: data })
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// RIGHT — seed data once at startup (onPluginStart) or via script, route just queries
|
|
875
|
+
export const GET = async (req, res) => {
|
|
876
|
+
const { data } = await query.graph({ entity: "payment_provider" })
|
|
877
|
+
res.json({ items: data }) // ✅ pure query, no side effects
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
// WRONG — importing service class directly in route file
|
|
881
|
+
import type PaymentModuleService from "../../../modules/payment/service" // ❌
|
|
882
|
+
const service = req.scope.resolve<PaymentModuleService>(PAYMENT_MODULE)
|
|
883
|
+
|
|
884
|
+
// RIGHT — resolve with module constant, cast to interface if needed
|
|
885
|
+
const service: any = req.scope.resolve(PAYMENT_MODULE) // ✅
|
|
886
|
+
// or use the generated interface type from @types if available
|
|
770
887
|
```
|
|
@@ -497,6 +497,7 @@ src/admin/routes/<feature>/
|
|
|
497
497
|
- Fields: `grid grid-cols-1 gap-4 md:grid-cols-2`
|
|
498
498
|
- Footer: `<div className="flex items-center justify-end gap-x-2">`, submit `type="submit"`
|
|
499
499
|
- All buttons `size="small"`, use `useRouteModal()` for `handleSuccess`
|
|
500
|
+
- **IMPORTANT**: `useRouteModal()` must be in a **child component** rendered inside `<RouteFocusModal>` — split into page (renders modal) + form (calls hook)
|
|
500
501
|
|
|
501
502
|
**Edit form** (`[id]/edit/page.tsx`):
|
|
502
503
|
- `<RouteDrawer>` (auto-opens, navigates back on close)
|
|
@@ -505,6 +506,7 @@ src/admin/routes/<feature>/
|
|
|
505
506
|
- Body fields: single column `flex flex-col gap-y-4` (drawer is narrow)
|
|
506
507
|
- Footer: same as create
|
|
507
508
|
- Loading guard wraps the `<KeyboundForm>`, not just body content
|
|
509
|
+
- **IMPORTANT**: `useRouteModal()` must be in a **child component** rendered inside `<RouteDrawer>` — split into page (renders drawer) + form (calls hook)
|
|
508
510
|
|
|
509
511
|
#### Multi-Entity Feature
|
|
510
512
|
|
|
@@ -13,6 +13,7 @@ You are a senior AcmeKit framework developer. Build features using code generato
|
|
|
13
13
|
- Use string literals for container keys — use `ContainerRegistrationKeys.*` and `Modules.*`
|
|
14
14
|
- Call services directly for mutations in routes — use workflows
|
|
15
15
|
- Put business logic in workflow steps — steps are thin wrappers; service methods own orchestration
|
|
16
|
+
- Use raw SQL via `PG_CONNECTION` / `pgConnection.raw()` for data mutations — use service methods
|
|
16
17
|
|
|
17
18
|
### Ask First
|
|
18
19
|
|