@warlock.js/core 5.13.0 → 5.14.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.
- package/CHANGELOG.md +13 -0
- package/esm/cli/commands/build.command.mjs.map +1 -1
- package/esm/cli/commands/dev-server.command.mjs +2 -0
- package/esm/cli/commands/dev-server.command.mjs.map +1 -1
- package/esm/dev-server/files-watcher.mjs +6 -3
- package/esm/dev-server/files-watcher.mjs.map +1 -1
- package/esm/errors/esbuild-binary-missing-error.mjs +20 -0
- package/esm/errors/esbuild-binary-missing-error.mjs.map +1 -0
- package/esm/generations/features/bull-board.feature.mjs +65 -0
- package/esm/generations/features/bull-board.feature.mjs.map +1 -0
- package/esm/generations/features/index.mjs +2 -0
- package/esm/generations/features/index.mjs.map +1 -1
- package/esm/generations/features/queue.feature.mjs +4 -1
- package/esm/generations/features/queue.feature.mjs.map +1 -1
- package/esm/generations/features/shared/insert-connector-entry.mjs +68 -0
- package/esm/generations/features/shared/insert-connector-entry.mjs.map +1 -0
- package/esm/generations/features/shared/insert-queue-dashboard-block.mjs +55 -0
- package/esm/generations/features/shared/insert-queue-dashboard-block.mjs.map +1 -0
- package/esm/generations/features/web.feature.mjs +4 -1
- package/esm/generations/features/web.feature.mjs.map +1 -1
- package/esm/generations/stubs.mjs +4 -4
- package/esm/generations/stubs.mjs.map +1 -1
- package/esm/http/middleware/cache-response-middleware.d.mts +12 -0
- package/esm/http/middleware/cache-response-middleware.d.mts.map +1 -1
- package/esm/http/middleware/cache-response-middleware.mjs +15 -3
- package/esm/http/middleware/cache-response-middleware.mjs.map +1 -1
- package/esm/production/esbuild-preflight.mjs +23 -13
- package/esm/production/esbuild-preflight.mjs.map +1 -1
- package/llms-full.txt +32 -3
- package/llms.txt +2 -2
- package/package.json +11 -11
- package/skills/run-app/SKILL.md +6 -2
- package/skills/use-middleware/SKILL.md +24 -0
- package/skills/write-cli-command/SKILL.md +2 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stubs.mjs","names":[],"sources":["../../../../../../../core/src/generations/stubs.ts"],"sourcesContent":["export const accessConfigStub = `import { type AccessConfigurations } from \"@warlock.js/access\";\r\nimport { DatabaseAccessResolver } from \"app/access/services/access-resolver\";\r\n\r\n/**\r\n * Authorization configuration — read by @warlock.js/access on boot.\r\n *\r\n * The resolver is the one required piece: it tells the engine how to read a\r\n * user's roles + permissions. The ejected DatabaseAccessResolver reads roles\r\n * from the user_roles table and maps them through the roles catalog table (so\r\n * roles + their permissions are managed at runtime, in the DB).\r\n *\r\n * For a fixed, code-defined catalog with no tables, swap in DefaultAccessResolver:\r\n * import { DefaultAccessResolver } from \"@warlock.js/access\";\r\n * resolver: new DefaultAccessResolver({ admin: [\"*\"], editor: [\"orders.*\"] }),\r\n *\r\n * Multi-tenant? Add a \\`resolveTenant()\\` to the resolver to read the active\r\n * tenant from the request; checks then scope to it automatically.\r\n */\r\nconst access: AccessConfigurations = {\r\n resolver: new DatabaseAccessResolver(),\r\n\r\n // Cache resolved permission sets (default \"10m\").\r\n // cache: { ttl: \"10m\" },\r\n};\r\n\r\nexport default access;\r\n`;\r\n\r\nexport const aiConfigStub = `import type { AIConfig } from \"@warlock.js/ai\";\r\n\r\n// >>> warlock:ai-packages (auto-managed) >>>\r\n// Satellite packages augment the \"ai\" object on import — e.g. ai.workspace,\r\n// ai.tools / ai.mcp, and panoptic's ai.config({ panoptic }) wiring. The command\r\n// \"warlock add ai-workspace | ai-tools | ai-panoptic\" adds the matching\r\n// side-effect import below; keep them so the augmentation + runtime registration\r\n// load before the ai connector applies this config.\r\n// <<< warlock:ai-packages <<<\r\n\r\n/**\r\n * AI configuration — applied on boot by the ai connector, which calls\r\n * ai.config(...) with the object below. Cross-cutting defaults live here\r\n * (shared cache / snapshot stores, observability); per-call options always win.\r\n *\r\n * Wire a default model from a provider you installed, e.g.:\r\n * import { OpenAISDK } from \"@warlock.js/ai-openai\";\r\n * const openai = OpenAISDK({ apiKey: env(\"OPENAI_API_KEY\") });\r\n * // then pass openai.model({ name: \"gpt-4o-mini\" }) into your agents.\r\n */\r\nconst ai: Partial<AIConfig> = {\r\n // Default cache driver for cache-backed AI features (semantic cache, rag / memory vector stores).\r\n // defaultStore: cache.driver(\"redis\", { client }),\r\n\r\n // Observability — requires \"warlock add ai-panoptic\". Exporters + the local dashboard.\r\n // panoptic: { exporters: [], dashboard: false, observeAll: false },\r\n};\r\n\r\nexport default ai;\r\n`;\r\n\r\nexport const accessRoleModelStub = `import { Model, RegisterModel } from \"@warlock.js/cascade\";\r\nimport { type Infer, v } from \"@warlock.js/seal\";\r\n\r\n/**\r\n * Validation schema for the roles catalog — mirrors the migration columns\r\n * (snake_case). Each row is a role name plus the permission strings it grants;\r\n * wildcards work (\"orders.*\", \"*\"). The DatabaseAccessResolver maps a user's\r\n * assigned role names through this table to their effective permissions.\r\n */\r\nexport const roleSchema = v.object({\r\n name: v.string(),\r\n permissions: v.array(v.string()).default([]),\r\n});\r\n\r\nexport type RoleSchema = Infer<typeof roleSchema>;\r\n\r\n/**\r\n * The roles catalog — role name → the permissions it grants. Managed at runtime\r\n * (admins add roles + edit their permissions), unlike a fixed code map. Read by\r\n * DatabaseAccessResolver.resolvePermissions to expand a user's roles to permissions.\r\n */\r\n@RegisterModel()\r\nexport class Role extends Model<RoleSchema> {\r\n public static table = \"roles\";\r\n\r\n public static schema = roleSchema;\r\n\r\n /** The permission strings this role grants. */\r\n public get permissions(): string[] {\r\n return this.get<string[]>(\"permissions\", []);\r\n }\r\n}\r\n`;\r\n\r\nexport const accessRoleModelIndexStub = `export * from \"./role.model\";\r\n`;\r\n\r\nexport const accessRoleMigrationStub = `import { arrayText, Migration, text } from \"@warlock.js/cascade\";\r\nimport { Role } from \"../role.model\";\r\n\r\n/**\r\n * Roles catalog table. \\`name\\` is unique (one row per role); \\`permissions\\` is a\r\n * text array of the permission strings the role grants.\r\n */\r\nexport default Migration.create(Role, {\r\n name: text().notNullable().unique(),\r\n permissions: arrayText().nullable(),\r\n});\r\n`;\r\n\r\nexport const accessUserRoleModelStub = `import { access } from \"@warlock.js/access\";\r\nimport type { Auth } from \"@warlock.js/auth\";\r\nimport { Model, RegisterModel } from \"@warlock.js/cascade\";\r\nimport { type Infer, v } from \"@warlock.js/seal\";\r\n\r\n/**\r\n * Validation schema for a role assignment — mirrors the migration columns\r\n * (snake_case). \\`tenant\\` is nullable: a null tenant is a GLOBAL assignment.\r\n */\r\nexport const userRoleSchema = v.object({\r\n user_id: v.string(),\r\n user_type: v.string(),\r\n role: v.string(),\r\n tenant: v.string().optional(),\r\n});\r\n\r\nexport type UserRoleSchema = Infer<typeof userRoleSchema>;\r\n\r\n/**\r\n * The role-assignment table — which roles a user holds, optionally per tenant.\r\n * Read by DatabaseAccessResolver.resolveRoles; mutated via the statics below.\r\n * \\`assign\\` / \\`revoke\\` flush the cached permission set automatically, so callers\r\n * never need to call \\`access.flush(user, tenant)\\` themselves.\r\n */\r\n@RegisterModel()\r\nexport class UserRole extends Model<UserRoleSchema> {\r\n public static table = \"user_roles\";\r\n\r\n public static schema = userRoleSchema;\r\n\r\n /**\r\n * Role names assigned to the user in the given tenant.\r\n *\r\n * An unresolved tenant (\\`undefined\\`) scopes to GLOBAL roles only — the rows\r\n * stored with no tenant (\\`null\\`) — never the union across every tenant. The\r\n * union would be a privilege-escalation: a user who is \\`owner\\` in one tenant\r\n * must not be treated as \\`owner\\` everywhere just because a check didn't carry\r\n * a tenant. This mirrors how \\`assign(user, role)\\` stores a global row.\r\n */\r\n public static async rolesFor(user: Auth, tenant?: string): Promise<string[]> {\r\n const rows = await this.query()\r\n .where({\r\n user_id: user.id,\r\n user_type: user.userType,\r\n tenant: tenant ?? null,\r\n })\r\n .get();\r\n\r\n // De-dupe so a duplicate row (a concurrent assign that slipped past the\r\n // existence check) can't distort the resolved set.\r\n return [...new Set(rows.map((row) => row.get(\"role\") as string))];\r\n }\r\n\r\n /**\r\n * Assign a role to the user. No-op if the assignment already exists.\r\n * Flushes the user's cached permission set automatically.\r\n */\r\n public static async assign(user: Auth, role: string, tenant?: string): Promise<void> {\r\n const existing = await this.first({\r\n user_id: user.id,\r\n user_type: user.userType,\r\n role,\r\n tenant: tenant ?? null,\r\n });\r\n\r\n if (existing) return;\r\n\r\n await this.create({\r\n user_id: user.id,\r\n user_type: user.userType,\r\n role,\r\n tenant,\r\n });\r\n\r\n await access.flush(user, tenant);\r\n }\r\n\r\n /**\r\n * Remove a role assignment from the user.\r\n * Flushes the user's cached permission set automatically.\r\n */\r\n public static async revoke(user: Auth, role: string, tenant?: string): Promise<void> {\r\n await this.delete({\r\n user_id: user.id,\r\n user_type: user.userType,\r\n role,\r\n tenant: tenant ?? null,\r\n });\r\n\r\n await access.flush(user, tenant);\r\n }\r\n}\r\n`;\r\n\r\nexport const accessUserRoleModelIndexStub = `export * from \"./user-role.model\";\r\n`;\r\n\r\nexport const accessUserRoleMigrationStub = `import { Migration, text, uuid } from \"@warlock.js/cascade\";\r\nimport { UserRole } from \"../user-role.model\";\r\n\r\n/**\r\n * Role-assignment table. \\`user_id\\` is a UUID — override this migration if your\r\n * user ids are integers. The composite index powers the per-user (per-tenant)\r\n * lookup the resolver runs on every check.\r\n */\r\nexport default Migration.create(\r\n UserRole,\r\n {\r\n user_id: uuid().notNullable().index(),\r\n user_type: text().notNullable(),\r\n role: text().notNullable().index(),\r\n tenant: text().nullable().index(),\r\n },\r\n {\r\n index: [{ columns: [\"user_id\", \"user_type\", \"tenant\"] }],\r\n },\r\n);\r\n`;\r\n\r\nexport const accessResolverStub = `import type { AccessResolver } from \"@warlock.js/access\";\r\nimport type { Auth } from \"@warlock.js/auth\";\r\nimport { Role } from \"app/access/models/role\";\r\nimport { UserRole } from \"app/access/models/user-role\";\r\n\r\n/**\r\n * The app's access adapter — connects @warlock.js/access to the ejected role\r\n * tables. Roles come from the user_roles assignment table; permissions are\r\n * expanded by mapping those role names through the roles catalog table. Both\r\n * are managed at runtime (in the DB), so admins can add roles + edit their\r\n * permissions without a deploy.\r\n *\r\n * The engine owns the hard parts (wildcard matching, caching, fail-closed); this\r\n * resolver only fetches — keep it dumb, never cache inside it.\r\n */\r\nexport class DatabaseAccessResolver implements AccessResolver {\r\n /** The role names this user holds (powers \\`hasRole\\` / \\`hasAnyRole\\`). */\r\n public async resolveRoles(user: Auth, tenant?: string): Promise<string[]> {\r\n return UserRole.rolesFor(user, tenant);\r\n }\r\n\r\n /** The effective permission strings this user has (powers \\`can\\` / \\`authorize\\`). */\r\n public async resolvePermissions(user: Auth, tenant?: string): Promise<string[]> {\r\n const names = await this.resolveRoles(user, tenant);\r\n\r\n if (names.length === 0) return [];\r\n\r\n const roles = await Role.query().whereIn(\"name\", names).get();\r\n\r\n // Flatten + de-dupe so two roles granting the same permission yield one entry.\r\n return [...new Set(roles.flatMap((role) => role.permissions))];\r\n }\r\n\r\n /**\r\n * Optional. Resolve the ambient tenant when a check doesn't pass one\r\n * explicitly — derive it from the authenticated user (safer than reading\r\n * client request input, which a caller could spoof). Uncomment + adapt for a\r\n * multi-tenant app (single-tenant apps leave this off and return undefined).\r\n */\r\n // public resolveTenant(user: Auth): string | undefined {\r\n // return user.get(\"organization_id\");\r\n // }\r\n}\r\n`;\r\n\r\nexport const socketConfigStub = `import type { SocketOptions } from \"@warlock.js/core\";\r\n\r\n/**\r\n * Socket.IO configuration — read by the framework's socket connector\r\n * on boot. When the HTTP server is running the socket server attaches\r\n * to it; otherwise it listens on its own configured port.\r\n *\r\n * Remove this file to disable the socket server entirely.\r\n */\r\nexport default {\r\n options: {\r\n cors: {\r\n origin: \"*\",\r\n },\r\n },\r\n} as SocketOptions;\r\n`;\r\n\r\nexport const communicatorsConfigStub = `import { env } from \"@warlock.js/core\";\r\nimport type { BrokerConfigurations, RabbitMQClientOptions } from \"@warlock.js/herald\";\r\n\r\nconst heraldConfigurations: BrokerConfigurations<RabbitMQClientOptions> = {\r\n driver: \"rabbitmq\",\r\n name: \"default\",\r\n isDefault: true,\r\n\r\n // ============================================================================\r\n // Connection Settings\r\n // ============================================================================\r\n\r\n host: env(\"RABBITMQ_HOST\", \"localhost\"),\r\n port: env(\"RABBITMQ_PORT\", 5672),\r\n username: env(\"RABBITMQ_USERNAME\", \"guest\"),\r\n password: env(\"RABBITMQ_PASSWORD\", \"guest\"),\r\n vhost: env(\"RABBITMQ_VHOST\", \"/\"),\r\n\r\n // Or use connection URI (takes precedence over host/port)\r\n // uri: env(\"RABBITMQ_URL\"),\r\n\r\n // ============================================================================\r\n // Connection Options\r\n // ============================================================================\r\n\r\n /** Heartbeat interval in seconds */\r\n heartbeat: 60,\r\n\r\n /** Connection timeout in milliseconds */\r\n connectionTimeout: 10000,\r\n\r\n /** Enable automatic reconnection on disconnect */\r\n reconnect: true,\r\n\r\n /** Delay between reconnection attempts in milliseconds */\r\n reconnectDelay: 5_000,\r\n\r\n // ============================================================================\r\n // Consumer Options\r\n // ============================================================================\r\n\r\n /** Default prefetch count (number of unacknowledged messages per consumer) */\r\n prefetch: 10,\r\n\r\n // ============================================================================\r\n // Client Options (Native amqplib options)\r\n // ============================================================================\r\n // These options are passed directly to amqplib.connect()\r\n // for low-level configuration like frame size, TLS, socket options, etc.\r\n // ============================================================================\r\n clientOptions: {\r\n // Frame max size in bytes (0 = no limit)\r\n // frameMax: 0,\r\n\r\n // Channel max (0 = unlimited)\r\n // channelMax: 0,\r\n\r\n // Socket options\r\n socket: {\r\n // Enable TCP keep-alive\r\n keepAlive: true,\r\n\r\n // Disable Nagle's algorithm for lower latency\r\n noDelay: true,\r\n\r\n // Socket timeout (in addition to heartbeat)\r\n // timeout: 30000,\r\n },\r\n\r\n // TLS/SSL options (uncomment for secure connections)\r\n // socket: {\r\n // ca: fs.readFileSync('/path/to/ca.pem'),\r\n // cert: fs.readFileSync('/path/to/cert.pem'),\r\n // key: fs.readFileSync('/path/to/key.pem'),\r\n // rejectUnauthorized: true,\r\n // },\r\n },\r\n};\r\n\r\nexport default heraldConfigurations;\r\n`;\r\n\r\nexport const notificationsConfigStub = `import { type NotificationConfig, inApp, mailChannel } from \"@warlock.js/notifications\";\r\nimport { Notification } from \"app/notifications/notification.model\";\r\n\r\n/**\r\n * Notifications configuration. Auto-loaded from src/config on boot — the\r\n * framework's notifications connector reads this default export and hands it to\r\n * setNotificationConfig, so this file stays declarative (no side-effect call).\r\n *\r\n * Each channel is payload-typed, so notify.mail(...) / notify.database(...)\r\n * and defineNotification are type-checked against the registry.\r\n *\r\n * Channels enabled here:\r\n * - mail wraps @warlock.js/core sendMail; route is notifiable.email.\r\n * The \"from\" address defaults to config/mail.ts; override per\r\n * channel with mailChannel({ from: \"no-reply@yourapp.com\" }).\r\n * - database in-app store backed by the Notification model. The \"inApp\"\r\n * facade exposes the recipient-scoped read API: listUnread,\r\n * countUnread, markAsRead, dismiss, ...\r\n *\r\n * Async delivery (.queue()) is OPTIONAL: run \"npx warlock add herald\",\r\n * import { heraldQueue } from \"@warlock.js/notifications\", and uncomment the\r\n * queue line below.\r\n */\r\nconst config: NotificationConfig = {\r\n channels: {\r\n mail: mailChannel(),\r\n database: inApp.configure({ model: Notification }),\r\n },\r\n\r\n // Async queue — requires @warlock.js/herald (npx warlock add herald):\r\n // queue: heraldQueue(),\r\n};\r\n\r\nexport default config;\r\n`;\r\n\r\nexport const notificationModelStub = `import { RegisterModel } from \"@warlock.js/cascade\";\r\nimport { DatabaseNotification, type NotificationColumnMap } from \"@warlock.js/notifications\";\r\nimport { v } from \"@warlock.js/seal\";\r\n\r\n/**\r\n * Validation schema for the notifications table — mirrors the migration\r\n * columns (snake_case). Cascade validates + casts every write against it:\r\n * nullable columns use .nullish() (may be absent or null), and payload is\r\n * free-form JSON. Keep this in sync with the migration + columnMap when you\r\n * add or rename columns.\r\n */\r\nconst notificationSchema = v.object({\r\n user_id: v.string(),\r\n type: v.string(),\r\n title: v.string(),\r\n body: v.string().nullish(),\r\n payload: v.record(v.any()).nullish(),\r\n read_at: v.date().nullish(),\r\n idempotency_key: v.string().nullish(),\r\n});\r\n\r\n/**\r\n * In-app notification model.\r\n *\r\n * Extends the package's DatabaseNotification base, which provides the stable\r\n * accessors (recipientId, tenantId, isRead, readAt, markRead) — all derived\r\n * from the columnMap below. The read/write API lives on the inApp facade\r\n * (configured in config/notifications.ts); you rarely touch this class directly.\r\n */\r\n@RegisterModel()\r\nexport class Notification extends DatabaseNotification {\r\n public static table = \"notifications\";\r\n public static schema = notificationSchema;\r\n\r\n /**\r\n * Maps the in-app store's roles to your columns. This default is\r\n * single-tenant + read_at-only. Add tenant: \"organization_id\" for\r\n * multi-tenant; use isRead: \"is_read\" (instead of, or alongside, readAt) to\r\n * track a boolean read flag. The migration + accessors all follow this map.\r\n */\r\n public static columnMap: NotificationColumnMap = { readAt: \"read_at\" };\r\n}\r\n`;\r\n\r\nexport const notificationMigrationStub = `import { Migration } from \"@warlock.js/cascade\";\r\nimport { notificationColumns } from \"@warlock.js/notifications\";\r\nimport { Notification } from \"../notification.model\";\r\n\r\n/**\r\n * Notifications table.\r\n *\r\n * Columns come from notificationColumns(Notification) — the recipient / tenant\r\n * / read-state names follow the model's columnMap; type / title / body /\r\n * payload / idempotency_key are fixed. Spread it to add your own columns\r\n * (remember to mirror them in the model schema):\r\n *\r\n * import { uuid } from \"@warlock.js/cascade\";\r\n *\r\n * export default Migration.create(Notification, {\r\n * ...notificationColumns(Notification),\r\n * // category_id: uuid().index().nullable(),\r\n * });\r\n */\r\nexport default Migration.create(Notification, notificationColumns(Notification));\r\n`;\r\n\r\nexport const notificationControllersStub = `import { type RequestHandler } from \"@warlock.js/core\";\r\nimport { inApp, type Id } from \"@warlock.js/notifications\";\r\n\r\n/**\r\n * The authenticated user's notification HTTP surface — thin wrappers over the\r\n * recipient-scoped \\`inApp\\` facade (a foreign id can never touch another user's\r\n * rows). Notifications are produced by domain events, never over HTTP, so there\r\n * is no create. Trim or split these as your app grows.\r\n */\r\n\r\n/**\r\n * Read \\`id\\` off \\`request.locals.user\\` without assuming this app's\r\n * \\`RequestUser\\` augmentation declares it — \\`RequestUser\\` (declared by\r\n * \\`@warlock.js/auth\\`) is empty by default, so a narrow runtime read survives\r\n * any augmentation shape instead of assuming \\`.id\\` exists at the type level.\r\n * \\`inApp\\` only ever needs the id (it reduces a \\`Notifiable\\` to one via\r\n * \\`recipient.id\\` internally), so reading it here — rather than forwarding\r\n * \\`request.locals.user\\` itself — also skips a needless \\`Notifiable\\` cast.\r\n */\r\nfunction recipientId(user: unknown): Id {\r\n if (user && typeof user === \"object\" && \"id\" in user) {\r\n const id = (user as { id?: unknown }).id;\r\n\r\n if (typeof id === \"string\" || typeof id === \"number\") return id;\r\n }\r\n\r\n throw new Error(\"Authenticated request is missing a usable user id\");\r\n}\r\n\r\n/** GET /notifications — list, most recent first (page / limit / type / unread via query). */\r\nexport const listNotificationsController: RequestHandler = async ({ request, response }) => {\r\n const { data, pagination } = await inApp.list(recipientId(request.locals.user), request.all());\r\n\r\n return response.success({ notifications: data, pagination });\r\n};\r\n\r\nlistNotificationsController.description = \"List notifications\";\r\n\r\n/** GET /notifications/unread-count — drives the bell badge. */\r\nexport const unreadNotificationsCountController: RequestHandler = async ({\r\n request,\r\n response,\r\n}) => {\r\n const count = await inApp.countUnread(recipientId(request.locals.user));\r\n\r\n return response.success({ count });\r\n};\r\n\r\nunreadNotificationsCountController.description = \"Unread notifications count\";\r\n\r\n/** PATCH /notifications/:id/read — mark one read, return the updated row. */\r\nexport const markNotificationReadController: RequestHandler = async ({ request, response }) => {\r\n const id = request.input(\"id\");\r\n const userId = recipientId(request.locals.user);\r\n\r\n await inApp.markAsRead(userId, id);\r\n const notification = await inApp.find(userId, id);\r\n\r\n return response.success({ notification });\r\n};\r\n\r\nmarkNotificationReadController.description = \"Mark notification read\";\r\n\r\n/** PATCH /notifications/read-all — mark every unread one read. */\r\nexport const markAllNotificationsReadController: RequestHandler = async ({\r\n request,\r\n response,\r\n}) => {\r\n const count = await inApp.markAsRead(recipientId(request.locals.user));\r\n\r\n return response.success({ count });\r\n};\r\n\r\nmarkAllNotificationsReadController.description = \"Mark all notifications read\";\r\n\r\n/** DELETE /notifications — dismiss all for the user. */\r\nexport const clearNotificationsController: RequestHandler = async ({ request, response }) => {\r\n await inApp.dismiss(recipientId(request.locals.user));\r\n\r\n return response.noContent();\r\n};\r\n\r\nclearNotificationsController.description = \"Clear notifications\";\r\n\r\n/** DELETE /notifications/:id — dismiss one. */\r\nexport const deleteNotificationController: RequestHandler = async ({ request, response }) => {\r\n await inApp.dismiss(recipientId(request.locals.user), request.input(\"id\"));\r\n\r\n return response.noContent();\r\n};\r\n\r\ndeleteNotificationController.description = \"Delete notification\";\r\n`;\r\n\r\nexport const notificationRoutesStub = `import { authMiddleware } from \"@warlock.js/auth\";\r\nimport { router } from \"@warlock.js/core\";\r\nimport {\r\n clearNotificationsController,\r\n deleteNotificationController,\r\n listNotificationsController,\r\n markAllNotificationsReadController,\r\n markNotificationReadController,\r\n unreadNotificationsCountController,\r\n} from \"./controllers/notifications.controller\";\r\n\r\n/**\r\n * Notification routes — the authenticated user's read + dismiss surface.\r\n *\r\n * Notifications are produced by domain events (never created over HTTP), so\r\n * there is no POST. Every route is gated by \\`authMiddleware\\` and recipient-\r\n * scoped by \\`inApp\\` (a foreign id touches zero rows). Delete any endpoint you\r\n * don't need; if your app reads notifications over sockets/GraphQL instead,\r\n * delete this file + the controllers entirely.\r\n */\r\nrouter.group({ prefix: \"/notifications\", middleware: [authMiddleware([])] }, () => {\r\n router.get(\"/\", listNotificationsController);\r\n router.get(\"/unread-count\", unreadNotificationsCountController);\r\n router.patch(\"/read-all\", markAllNotificationsReadController);\r\n router.patch(\"/:id/read\", markNotificationReadController);\r\n router.delete(\"/\", clearNotificationsController);\r\n router.delete(\"/:id\", deleteNotificationController);\r\n});\r\n`;\r\n\r\n/**\r\n * `src/web/root.tsx` — the application root for the SSR page layer.\r\n *\r\n * Deliberately minimal. The framework ships a default root, so this exists to\r\n * give you a place to start rather than because anything requires it. The\r\n * reference app (`v5/app/src/web/root.tsx`) is where to look for the fuller\r\n * shape: middleware, an app-level loader, locales, an ErrorBoundary.\r\n */\r\nexport const webRootStub = `import type { AppProps } from \"@warlock.js/web\";\r\nimport { Head, Scripts } from \"@warlock.js/web\";\r\n\r\n/**\r\n * The application root.\r\n *\r\n * NOT async, and it receives no request/response: it renders on the server and\r\n * again in the browser during hydration, where neither exists.\r\n */\r\nexport default function App({ children }: AppProps) {\r\n return (\r\n <html lang=\"en\">\r\n <head>\r\n {/*\r\n Placement only. The framework injects the page's \\`metadata\\`, the\r\n stylesheet and preload tags for this route, and the canonical links\r\n into <head> by default — <Head /> just says WHERE they land.\r\n\r\n Do not add a <title> here: the page's \\`metadata\\` owns it, and a root\r\n that emits one too produces two.\r\n */}\r\n <Head />\r\n <link rel=\"icon\" href=\"data:,\" />\r\n </head>\r\n <body>\r\n {/*\r\n REQUIRED — this is the hydration mount point, not a styling wrapper.\r\n\r\n The browser runtime looks up \\`#root\\` and hydrates that element only.\r\n Remove this div, or rename the id, and the page still renders from the\r\n server but never becomes interactive: the runtime throws in the console\r\n and nothing on screen changes.\r\n\r\n Wrap it in your own markup freely, and put anything that must live\r\n outside the hydrated tree (a static footer, a portal target) outside\r\n it — just keep an element with \\`id=\"root\"\\` around {children}.\r\n */}\r\n <div id=\"root\">{children}</div>\r\n {/*\r\n The hydration payload and module tags. Written explicitly because\r\n placement occasionally matters — a CSP nonce, or ordering against\r\n your own scripts.\r\n */}\r\n <Scripts />\r\n </body>\r\n </html>\r\n );\r\n}\r\n`;\r\n\r\n/**\r\n * `src/app/contact/controllers/contact.controller.ts` — a real API endpoint\r\n * for the Web starter's contact form. It intentionally has no persistence\r\n * dependency: replace the acknowledgement with a mail/job/database action.\r\n */\r\nexport const webContactControllerStub = `import { type Request, type RequestHandler } from \"@warlock.js/core\";\r\nimport { type Infer, v } from \"@warlock.js/seal\";\r\n\r\nexport const contactSchema = v.object({\r\n name: v.string().min(2),\r\n email: v.email(),\r\n message: v.string().min(10),\r\n});\r\n\r\nexport type ContactSchema = Infer.Output<typeof contactSchema>;\r\n\r\n/** POST /api/contact — validates the starter contact form. */\r\nexport const contactController: RequestHandler<Request<ContactSchema>> = async ({\r\n request,\r\n response,\r\n}) => {\r\n const contact = request.validated();\r\n\r\n // Replace this with delivery/persistence for your app. Keeping the accepted\r\n // payload visible makes the endpoint useful while remaining side-effect free.\r\n return response.success({\r\n message: \"Thanks, \" + contact.name + \". Your message has been received.\",\r\n });\r\n};\r\n\r\ncontactController.validation = { schema: contactSchema };\r\n`;\r\n\r\n/** `src/app/contact/routes.ts` — discovered by the standard app route loader. */\r\nexport const webContactRoutesStub = `import { router } from \"@warlock.js/core\";\r\nimport { contactController } from \"./controllers/contact.controller\";\r\n\r\nrouter.post(\"/api/contact\", contactController);\r\n`;\r\n\r\n/**\r\n * `src/web/index.register.ts` — universal static setup for the starter page.\r\n *\r\n * The page re-exports this stable binding so Warlock's `register()` lifecycle\r\n * still sees it in both realms without making React Fast Refresh treat every\r\n * JSX edit as an incompatible function-export replacement.\r\n */\r\nexport const webHomeRegisterStub = `import { extend } from \"@mongez/localization\";\r\n\r\nexport function register() {\r\n extend(\"en\", {\r\n starter: {\r\n title: \"Your Warlock app is running.\",\r\n introduction: \"This page is rendered on the server and hydrated in the browser.\",\r\n language: \"العربية\",\r\n contact: \"Send a message\",\r\n name: \"Name\",\r\n email: \"Email\",\r\n message: \"Message\",\r\n submit: \"Send message\",\r\n sent: \"Thanks — your message has been received.\",\r\n },\r\n });\r\n extend(\"ar\", {\r\n starter: {\r\n title: \"تطبيق Warlock يعمل الآن.\",\r\n introduction: \"تُعرض هذه الصفحة على الخادم ثم تُفعَّل في المتصفح.\",\r\n language: \"English\",\r\n contact: \"أرسل رسالة\",\r\n name: \"الاسم\",\r\n email: \"البريد الإلكتروني\",\r\n message: \"الرسالة\",\r\n submit: \"إرسال الرسالة\",\r\n sent: \"شكرًا — تم استلام رسالتك.\",\r\n },\r\n });\r\n}\r\n`;\r\n\r\n/**\r\n * `src/web/index.page.tsx` — one page, so \\`warlock dev\\` has something to serve\r\n * the moment this finishes.\r\n */\r\nexport const webHomePageStub = `import { http } from \"@mongez/http\";\r\nimport { setCurrentLocaleCode } from \"@mongez/localization\";\r\nimport { Form, useFormControl, type FormControlProps } from \"@mongez/react-form\";\r\nimport { transX } from \"@mongez/react-localization\";\r\nimport { v } from \"@warlock.js/seal\";\r\nimport { Link, type PageProps } from \"@warlock.js/web\";\r\nimport { useState } from \"react\";\r\n\r\nexport { register } from \"./index.register\";\r\n\r\n/**\r\n * A page route is an ordinary Warlock route whose handler renders React\r\n * instead of returning JSON.\r\n *\r\n * The URL and stable hydration name are the ones this file DECLARES below.\r\n * This page answers \\`GET \"/\"\\` because \\`route.path = \"/\"\\`, not because of\r\n * where the file lives. A page file with\r\n * no \\`route\\` export is REFUSED by both the dev server and the build.\r\n */\r\nexport const route = { path: \"/\", name: \"index\" } as const;\r\n\r\nexport const metadata = { title: \"Home\" };\r\n\r\nconst contactSchema = v.object({\r\n name: v.string().min(2),\r\n email: v.email(),\r\n message: v.string().min(10),\r\n});\r\n\r\nfunction TextInput({ label, ...controlProps }: FormControlProps & { label: string }) {\r\n const { error, getErrorProps, getInputProps } = useFormControl(controlProps);\r\n\r\n return (\r\n <div className=\"wk-field\">\r\n <label htmlFor={controlProps.name}>{label}</label>\r\n <input {...getInputProps()} />\r\n {error && <p {...getErrorProps()}>{error}</p>}\r\n </div>\r\n );\r\n}\r\n\r\n/**\r\n * Add a \\`loader\\` export to fetch data on the server, and it arrives here as\r\n * \\`data\\`, typed:\r\n *\r\n * export const loader = (async () => ({ items: await itemsRepository.all() }));\r\n * export default function HomePage({ data }: PageProps<typeof loader>) { ... }\r\n */\r\nexport default function HomePage(_props: PageProps) {\r\n // Live state. If the button below does nothing, the page rendered on the\r\n // server but never hydrated — the runtime never mounted at \\`#root\\`. This is\r\n // deliberately here so that failure is impossible to miss.\r\n const [count, setCount] = useState(0);\r\n const [locale, setLocale] = useState<\"en\" | \"ar\">(\"en\");\r\n const [submitted, setSubmitted] = useState(false);\r\n const [submitError, setSubmitError] = useState<string | null>(null);\r\n\r\n const toggleLocale = () => {\r\n const nextLocale = locale === \"en\" ? \"ar\" : \"en\";\r\n setCurrentLocaleCode(nextLocale);\r\n setLocale(nextLocale);\r\n };\r\n\r\n return (\r\n <>\r\n {/*\r\n Self-contained, dependency-free styling: plain CSS, system fonts, and\r\n CSS custom properties, scoped to this page. No CSS framework, no utility\r\n classes, no external stylesheet — this page looks the same whether or\r\n not \\`warlock add tailwind\\` has ever been run.\r\n */}\r\n <style>{\\`\r\n .wk-home {\r\n --wk-fg: #0f172a;\r\n --wk-muted: #64748b;\r\n --wk-accent: #4f46e5;\r\n --wk-border: #e2e8f0;\r\n font-family: system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\r\n color: var(--wk-fg);\r\n max-width: 42rem;\r\n margin: 4rem auto;\r\n padding: 0 1.5rem;\r\n line-height: 1.6;\r\n }\r\n .wk-home h1 { font-size: 2.25rem; margin: 0 0 0.5rem; }\r\n .wk-home p { color: var(--wk-muted); margin: 0 0 1.5rem; }\r\n .wk-home code {\r\n font-family: ui-monospace, \"SFMono-Regular\", Menlo, monospace;\r\n background: #f1f5f9;\r\n padding: 0.1rem 0.35rem;\r\n border-radius: 0.25rem;\r\n }\r\n .wk-check {\r\n border: 1px solid var(--wk-border);\r\n border-radius: 0.75rem;\r\n padding: 1.25rem 1.5rem;\r\n margin: 2rem 0;\r\n }\r\n .wk-check strong { display: block; font-size: 1.5rem; }\r\n .wk-check button {\r\n font: inherit;\r\n cursor: pointer;\r\n background: var(--wk-accent);\r\n color: #fff;\r\n border: 0;\r\n border-radius: 0.5rem;\r\n padding: 0.5rem 1rem;\r\n margin-top: 0.75rem;\r\n }\r\n .wk-links { display: flex; gap: 1.25rem; font-size: 0.95rem; }\r\n .wk-links a { color: var(--wk-accent); text-decoration: none; }\r\n .wk-links a:hover { text-decoration: underline; }\r\n .wk-language { margin-left: auto; }\r\n .wk-contact { margin-top: 2rem; }\r\n .wk-field { display: grid; gap: 0.35rem; margin: 0.8rem 0; }\r\n .wk-field input, .wk-field textarea { font: inherit; padding: 0.55rem; }\r\n .wk-field p, .wk-submit-error { color: #b91c1c; margin: 0; }\r\n .wk-success { color: #047857; }\r\n \\`}</style>\r\n\r\n <main className=\"wk-home\" dir={locale === \"ar\" ? \"rtl\" : \"ltr\"}>\r\n <nav className=\"wk-links\" aria-label=\"Starter links\">\r\n <a href=\"https://warlock.js.org\" target=\"_blank\" rel=\"noreferrer\">\r\n Docs\r\n </a>\r\n <Link href=\"/\" aria-current=\"page\">\r\n Home\r\n </Link>\r\n <button\r\n className=\"wk-language\"\r\n type=\"button\"\r\n aria-pressed={locale === \"ar\"}\r\n onClick={toggleLocale}\r\n >\r\n {transX(\"starter.language\")}\r\n </button>\r\n </nav>\r\n\r\n <h1>{transX(\"starter.title\")}</h1>\r\n <p>{transX(\"starter.introduction\")}</p>\r\n\r\n <section className=\"wk-check\">\r\n <label>If this number goes up when you click, React is hydrated:</label>\r\n <strong>{count}</strong>\r\n <button type=\"button\" onClick={() => setCount((c) => c + 1)}>\r\n Count up\r\n </button>\r\n </section>\r\n\r\n <section className=\"wk-contact\" aria-labelledby=\"contact-heading\">\r\n <h2 id=\"contact-heading\">{transX(\"starter.contact\")}</h2>\r\n <Form<typeof contactSchema>\r\n id=\"contact-form\"\r\n schema={contactSchema}\r\n onSubmit={async ({ form, values }) => {\r\n setSubmitted(false);\r\n setSubmitError(null);\r\n const result = await http.post<{ message: string }>(\"/api/contact\", values);\r\n\r\n if (result.error) {\r\n if (result.error.isValidationError) {\r\n const body = result.error.body as {\r\n errors?: Array<{ input: string; error: string }>;\r\n message?: string;\r\n };\r\n form.setErrors(\r\n Object.fromEntries(\r\n (body.errors ?? []).map(({ input, error }) => [input, error]),\r\n ),\r\n );\r\n setSubmitError(body.message ?? \"Please correct the highlighted fields.\");\r\n } else {\r\n setSubmitError(\"Your message could not be sent. Please try again.\");\r\n }\r\n return;\r\n }\r\n\r\n setSubmitted(true);\r\n form.reset();\r\n }}\r\n >\r\n <TextInput name=\"name\" label={transX(\"starter.name\")} autoComplete=\"name\" />\r\n <TextInput\r\n name=\"email\"\r\n label={transX(\"starter.email\")}\r\n type=\"email\"\r\n autoComplete=\"email\"\r\n />\r\n <ContactMessage />\r\n <button type=\"submit\">{transX(\"starter.submit\")}</button>\r\n {submitError && (\r\n <p className=\"wk-submit-error\" role=\"alert\">\r\n {submitError}\r\n </p>\r\n )}\r\n {submitted && (\r\n <p className=\"wk-success\" role=\"status\">\r\n {transX(\"starter.sent\")}\r\n </p>\r\n )}\r\n </Form>\r\n </section>\r\n </main>\r\n </>\r\n );\r\n}\r\n\r\nfunction ContactMessage() {\r\n const { error, getErrorProps, getInputProps } = useFormControl({ name: \"message\" });\r\n\r\n return (\r\n <div className=\"wk-field\">\r\n <label htmlFor=\"message\">{transX(\"starter.message\")}</label>\r\n <textarea {...getInputProps()} rows={5} />\r\n {error && <p {...getErrorProps()}>{error}</p>}\r\n </div>\r\n );\r\n}\r\n`;\r\n"],"mappings":";AAAA,MAAa,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BhC,MAAa,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+B5B,MAAa,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCnC,MAAa,2BAA2B;;AAGxC,MAAa,0BAA0B;;;;;;;;;;;;AAavC,MAAa,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8FvC,MAAa,+BAA+B;;AAG5C,MAAa,8BAA8B;;;;;;;;;;;;;;;;;;;;;AAsB3C,MAAa,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6ClC,MAAa,mBAAmB;;;;;;;;;;;;;;;;;AAkBhC,MAAa,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkFvC,MAAa,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCvC,MAAa,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CrC,MAAa,4BAA4B;;;;;;;;;;;;;;;;;;;;;AAsBzC,MAAa,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8F3C,MAAa,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCtC,MAAa,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuD3B,MAAa,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BxC,MAAa,uBAAuB;;;;;;;;;;;;AAapC,MAAa,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCnC,MAAa,kBAAkB"}
|
|
1
|
+
{"version":3,"file":"stubs.mjs","names":[],"sources":["../../../../../../../core/src/generations/stubs.ts"],"sourcesContent":["export const accessConfigStub = `import { type AccessConfigurations } from \"@warlock.js/access\";\nimport { DatabaseAccessResolver } from \"app/access/services/access-resolver\";\n\n/**\n * Authorization configuration — read by @warlock.js/access on boot.\n *\n * The resolver is the one required piece: it tells the engine how to read a\n * user's roles + permissions. The ejected DatabaseAccessResolver reads roles\n * from the user_roles table and maps them through the roles catalog table (so\n * roles + their permissions are managed at runtime, in the DB).\n *\n * For a fixed, code-defined catalog with no tables, swap in DefaultAccessResolver:\n * import { DefaultAccessResolver } from \"@warlock.js/access\";\n * resolver: new DefaultAccessResolver({ admin: [\"*\"], editor: [\"orders.*\"] }),\n *\n * Multi-tenant? Add a \\`resolveTenant()\\` to the resolver to read the active\n * tenant from the request; checks then scope to it automatically.\n */\nconst access: AccessConfigurations = {\n resolver: new DatabaseAccessResolver(),\n\n // Cache resolved permission sets (default \"10m\").\n // cache: { ttl: \"10m\" },\n};\n\nexport default access;\n`;\n\nexport const aiConfigStub = `import type { AIConfig } from \"@warlock.js/ai\";\n\n// >>> warlock:ai-packages (auto-managed) >>>\n// Satellite packages augment the \"ai\" object on import — e.g. ai.workspace,\n// ai.tools / ai.mcp, and panoptic's ai.config({ panoptic }) wiring. The command\n// \"warlock add ai-workspace | ai-tools | ai-panoptic\" adds the matching\n// side-effect import below; keep them so the augmentation + runtime registration\n// load before the ai connector applies this config.\n// <<< warlock:ai-packages <<<\n\n/**\n * AI configuration — applied on boot by the ai connector, which calls\n * ai.config(...) with the object below. Cross-cutting defaults live here\n * (shared cache / snapshot stores, observability); per-call options always win.\n *\n * Wire a default model from a provider you installed, e.g.:\n * import { OpenAISDK } from \"@warlock.js/ai-openai\";\n * const openai = OpenAISDK({ apiKey: env(\"OPENAI_API_KEY\") });\n * // then pass openai.model({ name: \"gpt-4o-mini\" }) into your agents.\n */\nconst ai: Partial<AIConfig> = {\n // Default cache driver for cache-backed AI features (semantic cache, rag / memory vector stores).\n // defaultStore: cache.driver(\"redis\", { client }),\n\n // Observability — requires \"warlock add ai-panoptic\". Exporters + the local dashboard.\n // panoptic: { exporters: [], dashboard: false, observeAll: false },\n};\n\nexport default ai;\n`;\n\nexport const accessRoleModelStub = `import { Model, RegisterModel } from \"@warlock.js/cascade\";\nimport { type Infer, v } from \"@warlock.js/seal\";\n\n/**\n * Validation schema for the roles catalog — mirrors the migration columns\n * (snake_case). Each row is a role name plus the permission strings it grants;\n * wildcards work (\"orders.*\", \"*\"). The DatabaseAccessResolver maps a user's\n * assigned role names through this table to their effective permissions.\n */\nexport const roleSchema = v.object({\n name: v.string(),\n permissions: v.array(v.string()).default([]),\n});\n\nexport type RoleSchema = Infer<typeof roleSchema>;\n\n/**\n * The roles catalog — role name → the permissions it grants. Managed at runtime\n * (admins add roles + edit their permissions), unlike a fixed code map. Read by\n * DatabaseAccessResolver.resolvePermissions to expand a user's roles to permissions.\n */\n@RegisterModel()\nexport class Role extends Model<RoleSchema> {\n public static table = \"roles\";\n\n public static schema = roleSchema;\n\n /** The permission strings this role grants. */\n public get permissions(): string[] {\n return this.get<string[]>(\"permissions\", []);\n }\n}\n`;\n\nexport const accessRoleModelIndexStub = `export * from \"./role.model\";\n`;\n\nexport const accessRoleMigrationStub = `import { arrayText, Migration, text } from \"@warlock.js/cascade\";\nimport { Role } from \"../role.model\";\n\n/**\n * Roles catalog table. \\`name\\` is unique (one row per role); \\`permissions\\` is a\n * text array of the permission strings the role grants.\n */\nexport default Migration.create(Role, {\n name: text().notNullable().unique(),\n permissions: arrayText().nullable(),\n});\n`;\n\nexport const accessUserRoleModelStub = `import { access } from \"@warlock.js/access\";\nimport type { Auth } from \"@warlock.js/auth\";\nimport { Model, RegisterModel } from \"@warlock.js/cascade\";\nimport { type Infer, v } from \"@warlock.js/seal\";\n\n/**\n * Validation schema for a role assignment — mirrors the migration columns\n * (snake_case). \\`tenant\\` is nullable: a null tenant is a GLOBAL assignment.\n */\nexport const userRoleSchema = v.object({\n user_id: v.string(),\n user_type: v.string(),\n role: v.string(),\n tenant: v.string().optional(),\n});\n\nexport type UserRoleSchema = Infer<typeof userRoleSchema>;\n\n/**\n * The role-assignment table — which roles a user holds, optionally per tenant.\n * Read by DatabaseAccessResolver.resolveRoles; mutated via the statics below.\n * \\`assign\\` / \\`revoke\\` flush the cached permission set automatically, so callers\n * never need to call \\`access.flush(user, tenant)\\` themselves.\n */\n@RegisterModel()\nexport class UserRole extends Model<UserRoleSchema> {\n public static table = \"user_roles\";\n\n public static schema = userRoleSchema;\n\n /**\n * Role names assigned to the user in the given tenant.\n *\n * An unresolved tenant (\\`undefined\\`) scopes to GLOBAL roles only — the rows\n * stored with no tenant (\\`null\\`) — never the union across every tenant. The\n * union would be a privilege-escalation: a user who is \\`owner\\` in one tenant\n * must not be treated as \\`owner\\` everywhere just because a check didn't carry\n * a tenant. This mirrors how \\`assign(user, role)\\` stores a global row.\n */\n public static async rolesFor(user: Auth, tenant?: string): Promise<string[]> {\n const rows = await this.query()\n .where({\n user_id: user.id,\n user_type: user.userType,\n tenant: tenant ?? null,\n })\n .get();\n\n // De-dupe so a duplicate row (a concurrent assign that slipped past the\n // existence check) can't distort the resolved set.\n return [...new Set(rows.map((row) => row.get(\"role\") as string))];\n }\n\n /**\n * Assign a role to the user. No-op if the assignment already exists.\n * Flushes the user's cached permission set automatically.\n */\n public static async assign(user: Auth, role: string, tenant?: string): Promise<void> {\n const existing = await this.first({\n user_id: user.id,\n user_type: user.userType,\n role,\n tenant: tenant ?? null,\n });\n\n if (existing) return;\n\n await this.create({\n user_id: user.id,\n user_type: user.userType,\n role,\n tenant,\n });\n\n await access.flush(user, tenant);\n }\n\n /**\n * Remove a role assignment from the user.\n * Flushes the user's cached permission set automatically.\n */\n public static async revoke(user: Auth, role: string, tenant?: string): Promise<void> {\n await this.delete({\n user_id: user.id,\n user_type: user.userType,\n role,\n tenant: tenant ?? null,\n });\n\n await access.flush(user, tenant);\n }\n}\n`;\n\nexport const accessUserRoleModelIndexStub = `export * from \"./user-role.model\";\n`;\n\nexport const accessUserRoleMigrationStub = `import { Migration, text, uuid } from \"@warlock.js/cascade\";\nimport { UserRole } from \"../user-role.model\";\n\n/**\n * Role-assignment table. \\`user_id\\` is a UUID — override this migration if your\n * user ids are integers. The composite index powers the per-user (per-tenant)\n * lookup the resolver runs on every check.\n */\nexport default Migration.create(\n UserRole,\n {\n user_id: uuid().notNullable().index(),\n user_type: text().notNullable(),\n role: text().notNullable().index(),\n tenant: text().nullable().index(),\n },\n {\n index: [{ columns: [\"user_id\", \"user_type\", \"tenant\"] }],\n },\n);\n`;\n\nexport const accessResolverStub = `import type { AccessResolver } from \"@warlock.js/access\";\nimport type { Auth } from \"@warlock.js/auth\";\nimport { Role } from \"app/access/models/role\";\nimport { UserRole } from \"app/access/models/user-role\";\n\n/**\n * The app's access adapter — connects @warlock.js/access to the ejected role\n * tables. Roles come from the user_roles assignment table; permissions are\n * expanded by mapping those role names through the roles catalog table. Both\n * are managed at runtime (in the DB), so admins can add roles + edit their\n * permissions without a deploy.\n *\n * The engine owns the hard parts (wildcard matching, caching, fail-closed); this\n * resolver only fetches — keep it dumb, never cache inside it.\n */\nexport class DatabaseAccessResolver implements AccessResolver {\n /** The role names this user holds (powers \\`hasRole\\` / \\`hasAnyRole\\`). */\n public async resolveRoles(user: Auth, tenant?: string): Promise<string[]> {\n return UserRole.rolesFor(user, tenant);\n }\n\n /** The effective permission strings this user has (powers \\`can\\` / \\`authorize\\`). */\n public async resolvePermissions(user: Auth, tenant?: string): Promise<string[]> {\n const names = await this.resolveRoles(user, tenant);\n\n if (names.length === 0) return [];\n\n const roles = await Role.query().whereIn(\"name\", names).get();\n\n // Flatten + de-dupe so two roles granting the same permission yield one entry.\n return [...new Set(roles.flatMap((role) => role.permissions))];\n }\n\n /**\n * Optional. Resolve the ambient tenant when a check doesn't pass one\n * explicitly — derive it from the authenticated user (safer than reading\n * client request input, which a caller could spoof). Uncomment + adapt for a\n * multi-tenant app (single-tenant apps leave this off and return undefined).\n */\n // public resolveTenant(user: Auth): string | undefined {\n // return user.get(\"organization_id\");\n // }\n}\n`;\n\nexport const socketConfigStub = `import type { SocketOptions } from \"@warlock.js/core\";\n\n/**\n * Socket.IO configuration — read by the framework's socket connector\n * on boot. When the HTTP server is running the socket server attaches\n * to it; otherwise it listens on its own configured port.\n *\n * Remove this file to disable the socket server entirely.\n */\nexport default {\n options: {\n cors: {\n origin: \"*\",\n },\n },\n} as SocketOptions;\n`;\n\nexport const communicatorsConfigStub = `import { env } from \"@warlock.js/core\";\nimport type { BrokerConfigurations, RabbitMQClientOptions } from \"@warlock.js/herald\";\n\nconst heraldConfigurations: BrokerConfigurations<RabbitMQClientOptions> = {\n driver: \"rabbitmq\",\n name: \"default\",\n isDefault: true,\n\n // ============================================================================\n // Connection Settings\n // ============================================================================\n\n host: env(\"RABBITMQ_HOST\", \"localhost\"),\n port: env(\"RABBITMQ_PORT\", 5672),\n username: env(\"RABBITMQ_USERNAME\", \"guest\"),\n password: env(\"RABBITMQ_PASSWORD\", \"guest\"),\n vhost: env(\"RABBITMQ_VHOST\", \"/\"),\n\n // Or use connection URI (takes precedence over host/port)\n // uri: env(\"RABBITMQ_URL\"),\n\n // ============================================================================\n // Connection Options\n // ============================================================================\n\n /** Heartbeat interval in seconds */\n heartbeat: 60,\n\n /** Connection timeout in milliseconds */\n connectionTimeout: 10000,\n\n /** Enable automatic reconnection on disconnect */\n reconnect: true,\n\n /** Delay between reconnection attempts in milliseconds */\n reconnectDelay: 5_000,\n\n // ============================================================================\n // Consumer Options\n // ============================================================================\n\n /** Default prefetch count (number of unacknowledged messages per consumer) */\n prefetch: 10,\n\n // ============================================================================\n // Client Options (Native amqplib options)\n // ============================================================================\n // These options are passed directly to amqplib.connect()\n // for low-level configuration like frame size, TLS, socket options, etc.\n // ============================================================================\n clientOptions: {\n // Frame max size in bytes (0 = no limit)\n // frameMax: 0,\n\n // Channel max (0 = unlimited)\n // channelMax: 0,\n\n // Socket options\n socket: {\n // Enable TCP keep-alive\n keepAlive: true,\n\n // Disable Nagle's algorithm for lower latency\n noDelay: true,\n\n // Socket timeout (in addition to heartbeat)\n // timeout: 30000,\n },\n\n // TLS/SSL options (uncomment for secure connections)\n // socket: {\n // ca: fs.readFileSync('/path/to/ca.pem'),\n // cert: fs.readFileSync('/path/to/cert.pem'),\n // key: fs.readFileSync('/path/to/key.pem'),\n // rejectUnauthorized: true,\n // },\n },\n};\n\nexport default heraldConfigurations;\n`;\n\nexport const notificationsConfigStub = `import { type NotificationConfig, inApp, mailChannel } from \"@warlock.js/notifications\";\nimport { Notification } from \"app/notifications/notification.model\";\n\n/**\n * Notifications configuration. Auto-loaded from src/config on boot — the\n * framework's notifications connector reads this default export and hands it to\n * setNotificationConfig, so this file stays declarative (no side-effect call).\n *\n * Each channel is payload-typed, so notify.mail(...) / notify.database(...)\n * and defineNotification are type-checked against the registry.\n *\n * Channels enabled here:\n * - mail wraps @warlock.js/core sendMail; route is notifiable.email.\n * The \"from\" address defaults to config/mail.ts; override per\n * channel with mailChannel({ from: \"no-reply@yourapp.com\" }).\n * - database in-app store backed by the Notification model. The \"inApp\"\n * facade exposes the recipient-scoped read API: listUnread,\n * countUnread, markAsRead, dismiss, ...\n *\n * Async delivery (.queue()) is OPTIONAL: run \"npx warlock add herald\",\n * import { heraldQueue } from \"@warlock.js/notifications\", and uncomment the\n * queue line below.\n */\nconst config: NotificationConfig = {\n channels: {\n mail: mailChannel(),\n database: inApp.configure({ model: Notification }),\n },\n\n // Async queue — requires @warlock.js/herald (npx warlock add herald):\n // queue: heraldQueue(),\n};\n\nexport default config;\n`;\n\nexport const notificationModelStub = `import { RegisterModel } from \"@warlock.js/cascade\";\nimport { DatabaseNotification, type NotificationColumnMap } from \"@warlock.js/notifications\";\nimport { v } from \"@warlock.js/seal\";\n\n/**\n * Validation schema for the notifications table — mirrors the migration\n * columns (snake_case). Cascade validates + casts every write against it:\n * nullable columns use .nullish() (may be absent or null), and payload is\n * free-form JSON. Keep this in sync with the migration + columnMap when you\n * add or rename columns.\n */\nconst notificationSchema = v.object({\n user_id: v.string(),\n type: v.string(),\n title: v.string(),\n body: v.string().nullish(),\n payload: v.record(v.any()).nullish(),\n read_at: v.date().nullish(),\n idempotency_key: v.string().nullish(),\n});\n\n/**\n * In-app notification model.\n *\n * Extends the package's DatabaseNotification base, which provides the stable\n * accessors (recipientId, tenantId, isRead, readAt, markRead) — all derived\n * from the columnMap below. The read/write API lives on the inApp facade\n * (configured in config/notifications.ts); you rarely touch this class directly.\n */\n@RegisterModel()\nexport class Notification extends DatabaseNotification {\n public static table = \"notifications\";\n public static schema = notificationSchema;\n\n /**\n * Maps the in-app store's roles to your columns. This default is\n * single-tenant + read_at-only. Add tenant: \"organization_id\" for\n * multi-tenant; use isRead: \"is_read\" (instead of, or alongside, readAt) to\n * track a boolean read flag. The migration + accessors all follow this map.\n */\n public static columnMap: NotificationColumnMap = { readAt: \"read_at\" };\n}\n`;\n\nexport const notificationMigrationStub = `import { Migration } from \"@warlock.js/cascade\";\nimport { notificationColumns } from \"@warlock.js/notifications\";\nimport { Notification } from \"../notification.model\";\n\n/**\n * Notifications table.\n *\n * Columns come from notificationColumns(Notification) — the recipient / tenant\n * / read-state names follow the model's columnMap; type / title / body /\n * payload / idempotency_key are fixed. Spread it to add your own columns\n * (remember to mirror them in the model schema):\n *\n * import { uuid } from \"@warlock.js/cascade\";\n *\n * export default Migration.create(Notification, {\n * ...notificationColumns(Notification),\n * // category_id: uuid().index().nullable(),\n * });\n */\nexport default Migration.create(Notification, notificationColumns(Notification));\n`;\n\nexport const notificationControllersStub = `import { type RequestHandler } from \"@warlock.js/core\";\nimport { inApp, type Id } from \"@warlock.js/notifications\";\n\n/**\n * The authenticated user's notification HTTP surface — thin wrappers over the\n * recipient-scoped \\`inApp\\` facade (a foreign id can never touch another user's\n * rows). Notifications are produced by domain events, never over HTTP, so there\n * is no create. Trim or split these as your app grows.\n */\n\n/**\n * Read \\`id\\` off \\`request.locals.user\\` without assuming this app's\n * \\`RequestUser\\` augmentation declares it — \\`RequestUser\\` (declared by\n * \\`@warlock.js/auth\\`) is empty by default, so a narrow runtime read survives\n * any augmentation shape instead of assuming \\`.id\\` exists at the type level.\n * \\`inApp\\` only ever needs the id (it reduces a \\`Notifiable\\` to one via\n * \\`recipient.id\\` internally), so reading it here — rather than forwarding\n * \\`request.locals.user\\` itself — also skips a needless \\`Notifiable\\` cast.\n */\nfunction recipientId(user: unknown): Id {\n if (user && typeof user === \"object\" && \"id\" in user) {\n const id = (user as { id?: unknown }).id;\n\n if (typeof id === \"string\" || typeof id === \"number\") return id;\n }\n\n throw new Error(\"Authenticated request is missing a usable user id\");\n}\n\n/** GET /notifications — list, most recent first (page / limit / type / unread via query). */\nexport const listNotificationsController: RequestHandler = async ({ request, response }) => {\n const { data, pagination } = await inApp.list(recipientId(request.locals.user), request.all());\n\n return response.success({ notifications: data, pagination });\n};\n\nlistNotificationsController.description = \"List notifications\";\n\n/** GET /notifications/unread-count — drives the bell badge. */\nexport const unreadNotificationsCountController: RequestHandler = async ({\n request,\n response,\n}) => {\n const count = await inApp.countUnread(recipientId(request.locals.user));\n\n return response.success({ count });\n};\n\nunreadNotificationsCountController.description = \"Unread notifications count\";\n\n/** PATCH /notifications/:id/read — mark one read, return the updated row. */\nexport const markNotificationReadController: RequestHandler = async ({ request, response }) => {\n const id = request.input(\"id\");\n const userId = recipientId(request.locals.user);\n\n await inApp.markAsRead(userId, id);\n const notification = await inApp.find(userId, id);\n\n return response.success({ notification });\n};\n\nmarkNotificationReadController.description = \"Mark notification read\";\n\n/** PATCH /notifications/read-all — mark every unread one read. */\nexport const markAllNotificationsReadController: RequestHandler = async ({\n request,\n response,\n}) => {\n const count = await inApp.markAsRead(recipientId(request.locals.user));\n\n return response.success({ count });\n};\n\nmarkAllNotificationsReadController.description = \"Mark all notifications read\";\n\n/** DELETE /notifications — dismiss all for the user. */\nexport const clearNotificationsController: RequestHandler = async ({ request, response }) => {\n await inApp.dismiss(recipientId(request.locals.user));\n\n return response.noContent();\n};\n\nclearNotificationsController.description = \"Clear notifications\";\n\n/** DELETE /notifications/:id — dismiss one. */\nexport const deleteNotificationController: RequestHandler = async ({ request, response }) => {\n await inApp.dismiss(recipientId(request.locals.user), request.input(\"id\"));\n\n return response.noContent();\n};\n\ndeleteNotificationController.description = \"Delete notification\";\n`;\n\nexport const notificationRoutesStub = `import { authMiddleware } from \"@warlock.js/auth\";\nimport { router } from \"@warlock.js/core\";\nimport {\n clearNotificationsController,\n deleteNotificationController,\n listNotificationsController,\n markAllNotificationsReadController,\n markNotificationReadController,\n unreadNotificationsCountController,\n} from \"./controllers/notifications.controller\";\n\n/**\n * Notification routes — the authenticated user's read + dismiss surface.\n *\n * Notifications are produced by domain events (never created over HTTP), so\n * there is no POST. Every route is gated by \\`authMiddleware\\` and recipient-\n * scoped by \\`inApp\\` (a foreign id touches zero rows). Delete any endpoint you\n * don't need; if your app reads notifications over sockets/GraphQL instead,\n * delete this file + the controllers entirely.\n */\nrouter.group({ prefix: \"/notifications\", middleware: [authMiddleware([])] }, () => {\n router.get(\"/\", listNotificationsController);\n router.get(\"/unread-count\", unreadNotificationsCountController);\n router.patch(\"/read-all\", markAllNotificationsReadController);\n router.patch(\"/:id/read\", markNotificationReadController);\n router.delete(\"/\", clearNotificationsController);\n router.delete(\"/:id\", deleteNotificationController);\n});\n`;\n\n/**\n * `src/web/root.tsx` — the application root for the SSR page layer.\n *\n * Deliberately minimal. The framework ships a default root, so this exists to\n * give you a place to start rather than because anything requires it. The\n * reference app (`v5/app/src/web/root.tsx`) is where to look for the fuller\n * shape: middleware, an app-level loader, locales, an ErrorBoundary.\n */\nexport const webRootStub = `import type { AppProps } from \"@warlock.js/web\";\nimport { Head, Scripts } from \"@warlock.js/web\";\n\n/**\n * The application root.\n *\n * NOT async, and it receives no request/response: it renders on the server and\n * again in the browser during hydration, where neither exists.\n */\nexport default function App({ children }: AppProps) {\n return (\n <html lang=\"en\">\n <head>\n {/*\n Placement only. The framework injects the page's \\`metadata\\`, the\n stylesheet and preload tags for this route, and the canonical links\n into <head> by default — <Head /> just says WHERE they land.\n\n Do not add a <title> here: the page's \\`metadata\\` owns it, and a root\n that emits one too produces two.\n */}\n <Head />\n <link rel=\"icon\" href=\"data:,\" />\n </head>\n <body>\n {/*\n REQUIRED — this is the hydration mount point, not a styling wrapper.\n\n The browser runtime looks up \\`#vessel\\` and hydrates that element only.\n Remove this div, or rename the id, and the page still renders from the\n server but never becomes interactive: the runtime throws in the console\n and nothing on screen changes.\n\n Wrap it in your own markup freely, and put anything that must live\n outside the hydrated tree (a static footer, a portal target) outside\n it — just keep an element with \\`id=\"vessel\"\\` around {children}.\n */}\n <div id=\"vessel\">{children}</div>\n {/*\n The hydration payload and module tags. Written explicitly because\n placement occasionally matters — a CSP nonce, or ordering against\n your own scripts.\n */}\n <Scripts />\n </body>\n </html>\n );\n}\n`;\n\n/**\n * `src/app/contact/controllers/contact.controller.ts` — a real API endpoint\n * for the Web starter's contact form. It intentionally has no persistence\n * dependency: replace the acknowledgement with a mail/job/database action.\n */\nexport const webContactControllerStub = `import { type Request, type RequestHandler } from \"@warlock.js/core\";\nimport { type Infer, v } from \"@warlock.js/seal\";\n\nexport const contactSchema = v.object({\n name: v.string().min(2),\n email: v.email(),\n message: v.string().min(10),\n});\n\nexport type ContactSchema = Infer.Output<typeof contactSchema>;\n\n/** POST /api/contact — validates the starter contact form. */\nexport const contactController: RequestHandler<Request<ContactSchema>> = async ({\n request,\n response,\n}) => {\n const contact = request.validated();\n\n // Replace this with delivery/persistence for your app. Keeping the accepted\n // payload visible makes the endpoint useful while remaining side-effect free.\n return response.success({\n message: \"Thanks, \" + contact.name + \". Your message has been received.\",\n });\n};\n\ncontactController.validation = { schema: contactSchema };\n`;\n\n/** `src/app/contact/routes.ts` — discovered by the standard app route loader. */\nexport const webContactRoutesStub = `import { router } from \"@warlock.js/core\";\nimport { contactController } from \"./controllers/contact.controller\";\n\nrouter.post(\"/api/contact\", contactController);\n`;\n\n/**\n * `src/web/index.register.ts` — universal static setup for the starter page.\n *\n * The page re-exports this stable binding so Warlock's `register()` lifecycle\n * still sees it in both realms without making React Fast Refresh treat every\n * JSX edit as an incompatible function-export replacement.\n */\nexport const webHomeRegisterStub = `import { extend } from \"@mongez/localization\";\n\nexport function register() {\n extend(\"en\", {\n starter: {\n title: \"Your Warlock app is running.\",\n introduction: \"This page is rendered on the server and hydrated in the browser.\",\n language: \"العربية\",\n contact: \"Send a message\",\n name: \"Name\",\n email: \"Email\",\n message: \"Message\",\n submit: \"Send message\",\n sent: \"Thanks — your message has been received.\",\n },\n });\n extend(\"ar\", {\n starter: {\n title: \"تطبيق Warlock يعمل الآن.\",\n introduction: \"تُعرض هذه الصفحة على الخادم ثم تُفعَّل في المتصفح.\",\n language: \"English\",\n contact: \"أرسل رسالة\",\n name: \"الاسم\",\n email: \"البريد الإلكتروني\",\n message: \"الرسالة\",\n submit: \"إرسال الرسالة\",\n sent: \"شكرًا — تم استلام رسالتك.\",\n },\n });\n}\n`;\n\n/**\n * `src/web/index.page.tsx` — one page, so \\`warlock dev\\` has something to serve\n * the moment this finishes.\n */\nexport const webHomePageStub = `import { http } from \"@mongez/http\";\nimport { setCurrentLocaleCode } from \"@mongez/localization\";\nimport { Form, useFormControl, type FormControlProps } from \"@mongez/react-form\";\nimport { transX } from \"@mongez/react-localization\";\nimport { v } from \"@warlock.js/seal\";\nimport { Link, type PageProps } from \"@warlock.js/web\";\nimport { useState } from \"react\";\n\nexport { register } from \"./index.register\";\n\n/**\n * A page route is an ordinary Warlock route whose handler renders React\n * instead of returning JSON.\n *\n * The URL and stable hydration name are the ones this file DECLARES below.\n * This page answers \\`GET \"/\"\\` because \\`route.path = \"/\"\\`, not because of\n * where the file lives. A page file with\n * no \\`route\\` export is REFUSED by both the dev server and the build.\n */\nexport const route = { path: \"/\", name: \"index\" } as const;\n\nexport const metadata = { title: \"Home\" };\n\nconst contactSchema = v.object({\n name: v.string().min(2),\n email: v.email(),\n message: v.string().min(10),\n});\n\nfunction TextInput({ label, ...controlProps }: FormControlProps & { label: string }) {\n const { error, getErrorProps, getInputProps } = useFormControl(controlProps);\n\n return (\n <div className=\"wk-field\">\n <label htmlFor={controlProps.name}>{label}</label>\n <input {...getInputProps()} />\n {error && <p {...getErrorProps()}>{error}</p>}\n </div>\n );\n}\n\n/**\n * Add a \\`loader\\` export to fetch data on the server, and it arrives here as\n * \\`data\\`, typed:\n *\n * export const loader = (async () => ({ items: await itemsRepository.all() }));\n * export default function HomePage({ data }: PageProps<typeof loader>) { ... }\n */\nexport default function HomePage(_props: PageProps) {\n // Live state. If the button below does nothing, the page rendered on the\n // server but never hydrated — the runtime never mounted at \\`#vessel\\`. This is\n // deliberately here so that failure is impossible to miss.\n const [count, setCount] = useState(0);\n const [locale, setLocale] = useState<\"en\" | \"ar\">(\"en\");\n const [submitted, setSubmitted] = useState(false);\n const [submitError, setSubmitError] = useState<string | null>(null);\n\n const toggleLocale = () => {\n const nextLocale = locale === \"en\" ? \"ar\" : \"en\";\n setCurrentLocaleCode(nextLocale);\n setLocale(nextLocale);\n };\n\n return (\n <>\n {/*\n Self-contained, dependency-free styling: plain CSS, system fonts, and\n CSS custom properties, scoped to this page. No CSS framework, no utility\n classes, no external stylesheet — this page looks the same whether or\n not \\`warlock add tailwind\\` has ever been run.\n */}\n <style>{\\`\n .wk-home {\n --wk-fg: #0f172a;\n --wk-muted: #64748b;\n --wk-accent: #4f46e5;\n --wk-border: #e2e8f0;\n font-family: system-ui, -apple-system, \"Segoe UI\", Roboto, sans-serif;\n color: var(--wk-fg);\n max-width: 42rem;\n margin: 4rem auto;\n padding: 0 1.5rem;\n line-height: 1.6;\n }\n .wk-home h1 { font-size: 2.25rem; margin: 0 0 0.5rem; }\n .wk-home p { color: var(--wk-muted); margin: 0 0 1.5rem; }\n .wk-home code {\n font-family: ui-monospace, \"SFMono-Regular\", Menlo, monospace;\n background: #f1f5f9;\n padding: 0.1rem 0.35rem;\n border-radius: 0.25rem;\n }\n .wk-check {\n border: 1px solid var(--wk-border);\n border-radius: 0.75rem;\n padding: 1.25rem 1.5rem;\n margin: 2rem 0;\n }\n .wk-check strong { display: block; font-size: 1.5rem; }\n .wk-check button {\n font: inherit;\n cursor: pointer;\n background: var(--wk-accent);\n color: #fff;\n border: 0;\n border-radius: 0.5rem;\n padding: 0.5rem 1rem;\n margin-top: 0.75rem;\n }\n .wk-links { display: flex; gap: 1.25rem; font-size: 0.95rem; }\n .wk-links a { color: var(--wk-accent); text-decoration: none; }\n .wk-links a:hover { text-decoration: underline; }\n .wk-language { margin-left: auto; }\n .wk-contact { margin-top: 2rem; }\n .wk-field { display: grid; gap: 0.35rem; margin: 0.8rem 0; }\n .wk-field input, .wk-field textarea { font: inherit; padding: 0.55rem; }\n .wk-field p, .wk-submit-error { color: #b91c1c; margin: 0; }\n .wk-success { color: #047857; }\n \\`}</style>\n\n <main className=\"wk-home\" dir={locale === \"ar\" ? \"rtl\" : \"ltr\"}>\n <nav className=\"wk-links\" aria-label=\"Starter links\">\n <a href=\"https://warlock.js.org\" target=\"_blank\" rel=\"noreferrer\">\n Docs\n </a>\n <Link href=\"/\" aria-current=\"page\">\n Home\n </Link>\n <button\n className=\"wk-language\"\n type=\"button\"\n aria-pressed={locale === \"ar\"}\n onClick={toggleLocale}\n >\n {transX(\"starter.language\")}\n </button>\n </nav>\n\n <h1>{transX(\"starter.title\")}</h1>\n <p>{transX(\"starter.introduction\")}</p>\n\n <section className=\"wk-check\">\n <label>If this number goes up when you click, React is hydrated:</label>\n <strong>{count}</strong>\n <button type=\"button\" onClick={() => setCount((c) => c + 1)}>\n Count up\n </button>\n </section>\n\n <section className=\"wk-contact\" aria-labelledby=\"contact-heading\">\n <h2 id=\"contact-heading\">{transX(\"starter.contact\")}</h2>\n <Form<typeof contactSchema>\n id=\"contact-form\"\n schema={contactSchema}\n onSubmit={async ({ form, values }) => {\n setSubmitted(false);\n setSubmitError(null);\n const result = await http.post<{ message: string }>(\"/api/contact\", values);\n\n if (result.error) {\n if (result.error.isValidationError) {\n const body = result.error.body as {\n errors?: Array<{ input: string; error: string }>;\n message?: string;\n };\n form.setErrors(\n Object.fromEntries(\n (body.errors ?? []).map(({ input, error }) => [input, error]),\n ),\n );\n setSubmitError(body.message ?? \"Please correct the highlighted fields.\");\n } else {\n setSubmitError(\"Your message could not be sent. Please try again.\");\n }\n return;\n }\n\n setSubmitted(true);\n form.reset();\n }}\n >\n <TextInput name=\"name\" label={transX(\"starter.name\")} autoComplete=\"name\" />\n <TextInput\n name=\"email\"\n label={transX(\"starter.email\")}\n type=\"email\"\n autoComplete=\"email\"\n />\n <ContactMessage />\n <button type=\"submit\">{transX(\"starter.submit\")}</button>\n {submitError && (\n <p className=\"wk-submit-error\" role=\"alert\">\n {submitError}\n </p>\n )}\n {submitted && (\n <p className=\"wk-success\" role=\"status\">\n {transX(\"starter.sent\")}\n </p>\n )}\n </Form>\n </section>\n </main>\n </>\n );\n}\n\nfunction ContactMessage() {\n const { error, getErrorProps, getInputProps } = useFormControl({ name: \"message\" });\n\n return (\n <div className=\"wk-field\">\n <label htmlFor=\"message\">{transX(\"starter.message\")}</label>\n <textarea {...getInputProps()} rows={5} />\n {error && <p {...getErrorProps()}>{error}</p>}\n </div>\n );\n}\n`;\n"],"mappings":";AAAA,MAAa,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BhC,MAAa,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+B5B,MAAa,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCnC,MAAa,2BAA2B;;AAGxC,MAAa,0BAA0B;;;;;;;;;;;;AAavC,MAAa,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8FvC,MAAa,+BAA+B;;AAG5C,MAAa,8BAA8B;;;;;;;;;;;;;;;;;;;;;AAsB3C,MAAa,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6ClC,MAAa,mBAAmB;;;;;;;;;;;;;;;;;AAkBhC,MAAa,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkFvC,MAAa,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCvC,MAAa,wBAAwB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4CrC,MAAa,4BAA4B;;;;;;;;;;;;;;;;;;;;;AAsBzC,MAAa,8BAA8B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8F3C,MAAa,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCtC,MAAa,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuD3B,MAAa,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BxC,MAAa,uBAAuB;;;;;;;;;;;;AAapC,MAAa,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCnC,MAAa,kBAAkB"}
|
|
@@ -31,6 +31,18 @@ type CacheMiddlewareOptions = {
|
|
|
31
31
|
* @default cache manager
|
|
32
32
|
*/
|
|
33
33
|
driver?: string;
|
|
34
|
+
/**
|
|
35
|
+
* Tags this cached response is stored under, mirroring `route.cache.tags`
|
|
36
|
+
* on `@warlock.js/web`'s page cache (`PageCacheOptIn.tags`,
|
|
37
|
+
* `web/src/routing/route-identity.ts`) so the two caches share one mental
|
|
38
|
+
* model: `cache.tags([...]).invalidate()` from `@warlock.js/cache` evicts
|
|
39
|
+
* a tagged API response the same way it evicts a tagged page. Either a
|
|
40
|
+
* static list, or a function of the request — resolved once per request,
|
|
41
|
+
* right before the response is stored.
|
|
42
|
+
*
|
|
43
|
+
* @default undefined (untagged — behaves exactly as before this option existed)
|
|
44
|
+
*/
|
|
45
|
+
tags?: string[] | ((request: Request) => string[]);
|
|
34
46
|
};
|
|
35
47
|
declare function cacheMiddleware(responseCacheOptions: CacheMiddlewareOptions | string): Middleware;
|
|
36
48
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache-response-middleware.d.mts","names":[],"sources":["../../../../../../../../core/src/http/middleware/cache-response-middleware.ts"],"mappings":";;;;KAsBY,sBAAA;;AAAZ;;EAIE,QAAA,aAAqB,OAAA,EAAS,OAAA,iBAAwB,OAAA,EAAS,OAAA,KAAY,OAAA;EAA7C;;;;;;EAO9B,UAAA;
|
|
1
|
+
{"version":3,"file":"cache-response-middleware.d.mts","names":[],"sources":["../../../../../../../../core/src/http/middleware/cache-response-middleware.ts"],"mappings":";;;;KAsBY,sBAAA;;AAAZ;;EAIE,QAAA,aAAqB,OAAA,EAAS,OAAA,iBAAwB,OAAA,EAAS,OAAA,KAAY,OAAA;EAA7C;;;;;;EAO9B,UAAA;EAP8B;;;;;EAa9B,IAAA;EAAA;;;EAIA,GAAA;EAmB6B;;;AAAO;AAyDtC;;EArEE,MAAA;EAqEgG;;;;;AAAA;;;;;;EAzDhG,IAAA,gBAAoB,OAAA,EAAS,OAAA;AAAA;AAAA,iBAyDf,eAAA,CAAgB,oBAAA,EAAsB,sBAAA,YAAkC,UAAU"}
|
|
@@ -4,12 +4,24 @@ import { cache } from "@warlock.js/cache";
|
|
|
4
4
|
|
|
5
5
|
//#region ../core/src/http/middleware/cache-response-middleware.ts
|
|
6
6
|
const defaultCacheOptions = { withLocale: true };
|
|
7
|
+
/**
|
|
8
|
+
* Resolves `CacheMiddlewareOptions.tags` — a static list or a function of
|
|
9
|
+
* the request — into a concrete list at store time. Mirrors
|
|
10
|
+
* `resolveCacheTags` in `@warlock.js/web`'s `create-page-route-handler.ts`,
|
|
11
|
+
* the equivalent seam for `route.cache.tags`.
|
|
12
|
+
*/
|
|
13
|
+
function resolveCacheTags(tags, request) {
|
|
14
|
+
if (tags === void 0) return [];
|
|
15
|
+
return typeof tags === "function" ? tags(request) : tags;
|
|
16
|
+
}
|
|
7
17
|
async function parseCacheOptions(cacheOptions, request) {
|
|
8
18
|
if (typeof cacheOptions === "string") cacheOptions = { cacheKey: cacheOptions };
|
|
9
19
|
if (typeof cacheOptions.cacheKey === "function") cacheOptions.cacheKey = await cacheOptions.cacheKey(request);
|
|
20
|
+
const tags = resolveCacheTags(cacheOptions.tags, request);
|
|
10
21
|
const finalCacheOptions = {
|
|
11
22
|
...defaultCacheOptions,
|
|
12
|
-
...cacheOptions
|
|
23
|
+
...cacheOptions,
|
|
24
|
+
tags
|
|
13
25
|
};
|
|
14
26
|
if (finalCacheOptions.withLocale) {
|
|
15
27
|
const locale = request.getLocaleCode();
|
|
@@ -20,7 +32,7 @@ async function parseCacheOptions(cacheOptions, request) {
|
|
|
20
32
|
}
|
|
21
33
|
function cacheMiddleware(responseCacheOptions) {
|
|
22
34
|
return async function({ request, response }) {
|
|
23
|
-
const { ttl, omit, cacheKey, driver } = await parseCacheOptions(responseCacheOptions, request);
|
|
35
|
+
const { ttl, omit, cacheKey, driver, tags } = await parseCacheOptions(responseCacheOptions, request);
|
|
24
36
|
const cacheDriver = driver ? await cache.use(driver) : cache;
|
|
25
37
|
const content = await cacheDriver.get(cacheKey);
|
|
26
38
|
if (content) return response.replay({
|
|
@@ -36,7 +48,7 @@ function cacheMiddleware(responseCacheOptions) {
|
|
|
36
48
|
data: except(response.parsedBody, omit),
|
|
37
49
|
contentType: typeof sentContentType === "string" ? sentContentType : void 0
|
|
38
50
|
};
|
|
39
|
-
cacheDriver.set(cacheKey, content, ttl).catch((error) => {
|
|
51
|
+
(tags.length > 0 ? cacheDriver.tags(tags).set(cacheKey, content, ttl) : cacheDriver.set(cacheKey, content, ttl)).catch((error) => {
|
|
40
52
|
log.error("cache-middleware", "set", error);
|
|
41
53
|
});
|
|
42
54
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache-response-middleware.mjs","names":[],"sources":["../../../../../../../../core/src/http/middleware/cache-response-middleware.ts"],"sourcesContent":["import { except } from \"@mongez/reinforcements\";\nimport { cache } from \"@warlock.js/cache\";\nimport { log } from \"@warlock.js/logger\";\nimport type { Middleware } from \"../../router/types\";\nimport type { Request } from \"./../request\";\nimport type { Response } from \"./../response\";\n\n/**\n * Shape persisted to the cache for a cached response. Stores the status and\n * content-type alongside the body so the HIT path can replay the response\n * faithfully via {@link Response.replay} instead of re-entering `send()`.\n */\ntype CachedResponsePayload = {\n status: number;\n data: unknown;\n contentType?: string;\n};\n\n// TODO: Add option to determine whether to cache the response or not\n// TODO: add option to determine what to be cached from the response\n// TODO: add cache middleware config options for example to set the default driver, ttl, etc\n\nexport type CacheMiddlewareOptions = {\n /**\n * Cache key\n */\n cacheKey: string | ((request: Request) => string) | ((request: Request) => Promise<string>);\n /**\n * If true, then the response will be cached based on the current locale code\n * This is useful when you have a multi-language website, and you want to cache the response based on the current locale\n *\n * @default true\n */\n withLocale?: boolean;\n /**\n * List of keys from the response object to omit from the cached response\n *\n * @default ['user']\n */\n omit?: string[];\n /**\n * Expires after number of seconds\n */\n ttl?: number;\n /**\n * Cache driver\n *\n * @see config/cache.ts: drivers object\n * @default cache manager\n */\n driver?: string;\n};\n\nconst defaultCacheOptions: Partial<CacheMiddlewareOptions> = {\n withLocale: true,\n};\n\ntype ParsedCacheOptions = Required<CacheMiddlewareOptions
|
|
1
|
+
{"version":3,"file":"cache-response-middleware.mjs","names":[],"sources":["../../../../../../../../core/src/http/middleware/cache-response-middleware.ts"],"sourcesContent":["import { except } from \"@mongez/reinforcements\";\nimport { cache } from \"@warlock.js/cache\";\nimport { log } from \"@warlock.js/logger\";\nimport type { Middleware } from \"../../router/types\";\nimport type { Request } from \"./../request\";\nimport type { Response } from \"./../response\";\n\n/**\n * Shape persisted to the cache for a cached response. Stores the status and\n * content-type alongside the body so the HIT path can replay the response\n * faithfully via {@link Response.replay} instead of re-entering `send()`.\n */\ntype CachedResponsePayload = {\n status: number;\n data: unknown;\n contentType?: string;\n};\n\n// TODO: Add option to determine whether to cache the response or not\n// TODO: add option to determine what to be cached from the response\n// TODO: add cache middleware config options for example to set the default driver, ttl, etc\n\nexport type CacheMiddlewareOptions = {\n /**\n * Cache key\n */\n cacheKey: string | ((request: Request) => string) | ((request: Request) => Promise<string>);\n /**\n * If true, then the response will be cached based on the current locale code\n * This is useful when you have a multi-language website, and you want to cache the response based on the current locale\n *\n * @default true\n */\n withLocale?: boolean;\n /**\n * List of keys from the response object to omit from the cached response\n *\n * @default ['user']\n */\n omit?: string[];\n /**\n * Expires after number of seconds\n */\n ttl?: number;\n /**\n * Cache driver\n *\n * @see config/cache.ts: drivers object\n * @default cache manager\n */\n driver?: string;\n /**\n * Tags this cached response is stored under, mirroring `route.cache.tags`\n * on `@warlock.js/web`'s page cache (`PageCacheOptIn.tags`,\n * `web/src/routing/route-identity.ts`) so the two caches share one mental\n * model: `cache.tags([...]).invalidate()` from `@warlock.js/cache` evicts\n * a tagged API response the same way it evicts a tagged page. Either a\n * static list, or a function of the request — resolved once per request,\n * right before the response is stored.\n *\n * @default undefined (untagged — behaves exactly as before this option existed)\n */\n tags?: string[] | ((request: Request) => string[]);\n};\n\nconst defaultCacheOptions: Partial<CacheMiddlewareOptions> = {\n withLocale: true,\n};\n\ntype ParsedCacheOptions = Required<Omit<CacheMiddlewareOptions, \"tags\">> & {\n cacheKey: string;\n /** Resolved, concrete tag list — see {@link resolveCacheTags}. */\n tags: string[];\n};\n\n/**\n * Resolves `CacheMiddlewareOptions.tags` — a static list or a function of\n * the request — into a concrete list at store time. Mirrors\n * `resolveCacheTags` in `@warlock.js/web`'s `create-page-route-handler.ts`,\n * the equivalent seam for `route.cache.tags`.\n */\nfunction resolveCacheTags(tags: CacheMiddlewareOptions[\"tags\"], request: Request): string[] {\n if (tags === undefined) return [];\n\n return typeof tags === \"function\" ? tags(request) : tags;\n}\n\nasync function parseCacheOptions(cacheOptions: CacheMiddlewareOptions | string, request: Request) {\n if (typeof cacheOptions === \"string\") {\n cacheOptions = {\n cacheKey: cacheOptions,\n };\n }\n\n if (typeof cacheOptions.cacheKey === \"function\") {\n cacheOptions.cacheKey = await cacheOptions.cacheKey(request);\n }\n\n const tags = resolveCacheTags(cacheOptions.tags, request);\n\n const finalCacheOptions = {\n ...defaultCacheOptions,\n ...cacheOptions,\n tags,\n } as ParsedCacheOptions;\n\n if (finalCacheOptions.withLocale) {\n const locale = request.getLocaleCode();\n\n finalCacheOptions.cacheKey = `${finalCacheOptions.cacheKey}:${locale}`;\n }\n\n if (!finalCacheOptions.omit) {\n finalCacheOptions.omit = [\"user\", \"settings\"];\n }\n\n return finalCacheOptions;\n}\n\nexport function cacheMiddleware(responseCacheOptions: CacheMiddlewareOptions | string): Middleware {\n // The `Middleware` return annotation is load-bearing: without it, tsc never\n // checks this factory's calling convention, which is how the positional v4\n // shape survived an earlier refactor unnoticed.\n return async function ({ request, response }) {\n const { ttl, omit, cacheKey, driver, tags } = await parseCacheOptions(\n responseCacheOptions,\n request,\n );\n const cacheDriver = driver ? await cache.use(driver) : cache;\n\n const content = (await cacheDriver.get(cacheKey)) as CachedResponsePayload | null;\n\n if (content) {\n // Replay through the standard pipeline (status + content-type preserved)\n // instead of `baseResponse.send()`, which would re-enter Response.send()\n // on an already-sent reply, trip the double-send guard, and drop the\n // status / content-type.\n return response.replay({\n status: content.status ?? 200,\n body: content.data,\n contentType: content.contentType,\n });\n }\n\n response.onSent((response: Response) => {\n if (!response.isOk || response.request.path !== request.path) {\n return;\n }\n\n const sentContentType = response.contentType;\n\n const content: CachedResponsePayload = {\n status: response.statusCode,\n data: except(response.parsedBody, omit),\n contentType: typeof sentContentType === \"string\" ? sentContentType : undefined,\n };\n\n // `set` is fire-and-forget inside `onSent`; without a `.catch` a rejected\n // write (e.g. Redis down) would surface as an unhandledRejection.\n //\n // Tagged and untagged writes go through separate calls rather than a\n // shared branch-free path so an untagged route's write stays byte\n // identical to what it was before `tags` existed.\n const write =\n tags.length > 0\n ? cacheDriver.tags(tags).set(cacheKey, content, ttl)\n : cacheDriver.set(cacheKey, content, ttl);\n\n write.catch((error: unknown) => {\n log.error(\"cache-middleware\", \"set\", error);\n });\n });\n };\n}\n"],"mappings":";;;;;AAiEA,MAAM,sBAAuD,EAC3D,YAAY,KACd;;;;;;;AAcA,SAAS,iBAAiB,MAAsC,SAA4B;CAC1F,IAAI,SAAS,QAAW,OAAO,CAAC;CAEhC,OAAO,OAAO,SAAS,aAAa,KAAK,OAAO,IAAI;AACtD;AAEA,eAAe,kBAAkB,cAA+C,SAAkB;CAChG,IAAI,OAAO,iBAAiB,UAC1B,eAAe,EACb,UAAU,aACZ;CAGF,IAAI,OAAO,aAAa,aAAa,YACnC,aAAa,WAAW,MAAM,aAAa,SAAS,OAAO;CAG7D,MAAM,OAAO,iBAAiB,aAAa,MAAM,OAAO;CAExD,MAAM,oBAAoB;EACxB,GAAG;EACH,GAAG;EACH;CACF;CAEA,IAAI,kBAAkB,YAAY;EAChC,MAAM,SAAS,QAAQ,cAAc;EAErC,kBAAkB,WAAW,GAAG,kBAAkB,SAAS,GAAG;CAChE;CAEA,IAAI,CAAC,kBAAkB,MACrB,kBAAkB,OAAO,CAAC,QAAQ,UAAU;CAG9C,OAAO;AACT;AAEA,SAAgB,gBAAgB,sBAAmE;CAIjG,OAAO,eAAgB,EAAE,SAAS,YAAY;EAC5C,MAAM,EAAE,KAAK,MAAM,UAAU,QAAQ,SAAS,MAAM,kBAClD,sBACA,OACF;EACA,MAAM,cAAc,SAAS,MAAM,MAAM,IAAI,MAAM,IAAI;EAEvD,MAAM,UAAW,MAAM,YAAY,IAAI,QAAQ;EAE/C,IAAI,SAKF,OAAO,SAAS,OAAO;GACrB,QAAQ,QAAQ,UAAU;GAC1B,MAAM,QAAQ;GACd,aAAa,QAAQ;EACvB,CAAC;EAGH,SAAS,QAAQ,aAAuB;GACtC,IAAI,CAAC,SAAS,QAAQ,SAAS,QAAQ,SAAS,QAAQ,MACtD;GAGF,MAAM,kBAAkB,SAAS;GAEjC,MAAM,UAAiC;IACrC,QAAQ,SAAS;IACjB,MAAM,OAAO,SAAS,YAAY,IAAI;IACtC,aAAa,OAAO,oBAAoB,WAAW,kBAAkB;GACvE;GAaA,CAJE,KAAK,SAAS,IACV,YAAY,KAAK,IAAI,CAAC,CAAC,IAAI,UAAU,SAAS,GAAG,IACjD,YAAY,IAAI,UAAU,SAAS,GAAG,EAEvC,CAAC,OAAO,UAAmB;IAC9B,IAAI,MAAM,oBAAoB,OAAO,KAAK;GAC5C,CAAC;EACH,CAAC;CACH;AACF"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { EsbuildBinaryMissingError } from "../errors/esbuild-binary-missing-error.mjs";
|
|
1
2
|
import esbuild from "esbuild";
|
|
2
3
|
|
|
3
4
|
//#region ../core/src/production/esbuild-preflight.ts
|
|
@@ -13,24 +14,33 @@ import esbuild from "esbuild";
|
|
|
13
14
|
*/
|
|
14
15
|
const UNLINKED_BINARY_SIGNATURE = "could not be found, and is needed by esbuild";
|
|
15
16
|
/**
|
|
16
|
-
*
|
|
17
|
+
* The default probe: a trivial `transformSync` call. Trivial input keeps the
|
|
18
|
+
* cost negligible while still forcing esbuild to resolve and run its native
|
|
19
|
+
* binary, which is the only way an unlinked binary actually surfaces.
|
|
20
|
+
*/
|
|
21
|
+
function runDefaultProbe() {
|
|
22
|
+
esbuild.transformSync("", { loader: "js" });
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Fail fast, before dev or build does any other work, when esbuild's native
|
|
26
|
+
* binary is not linked.
|
|
17
27
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
28
|
+
* A healthy esbuild install is a no-op. A binary genuinely missing — because
|
|
29
|
+
* a platform package was never installed, or a package manager blocked its
|
|
30
|
+
* postinstall script — is turned into {@link EsbuildBinaryMissingError}, a
|
|
31
|
+
* message that names the cause and the fix. Any other failure (a real syntax
|
|
32
|
+
* error, an unrelated platform mismatch, …) is rethrown unchanged — this
|
|
33
|
+
* preflight only owns the one known failure mode.
|
|
22
34
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* mismatch, …) is rethrown unchanged — this preflight only owns the one
|
|
27
|
-
* known failure mode.
|
|
35
|
+
* @param probe Overrides the default `esbuild.transformSync` invocation.
|
|
36
|
+
* Intended for tests only.
|
|
37
|
+
* @throws {EsbuildBinaryMissingError} when the platform binary is missing or unlinked.
|
|
28
38
|
*/
|
|
29
|
-
function assertEsbuildBinaryIsLinked() {
|
|
39
|
+
function assertEsbuildBinaryIsLinked(probe = runDefaultProbe) {
|
|
30
40
|
try {
|
|
31
|
-
|
|
41
|
+
probe();
|
|
32
42
|
} catch (error) {
|
|
33
|
-
if (isUnlinkedBinaryError(error)) throw new
|
|
43
|
+
if (isUnlinkedBinaryError(error)) throw new EsbuildBinaryMissingError({ cause: error });
|
|
34
44
|
throw error;
|
|
35
45
|
}
|
|
36
46
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"esbuild-preflight.mjs","names":[],"sources":["../../../../../../../core/src/production/esbuild-preflight.ts"],"sourcesContent":["import esbuild from \"esbuild\";\n\n/**\n * The exact substring esbuild's own loader throws when the platform-specific\n * binary package (e.g. `@esbuild/win32-x64`) never got installed. This is\n * the signature pnpm's build-script approval gate leaves behind: it blocks\n * esbuild's postinstall script, so the binary is never linked into place,\n * and any later call into esbuild dies with this message instead of a\n * project-shaped one.\n *\n * Source: `pkgAndSubpathForCurrentPlatform` in esbuild's `lib/main.js`.\n */\nconst UNLINKED_BINARY_SIGNATURE = \"could not be found, and is needed by esbuild\";\n\n/**\n *
|
|
1
|
+
{"version":3,"file":"esbuild-preflight.mjs","names":[],"sources":["../../../../../../../core/src/production/esbuild-preflight.ts"],"sourcesContent":["import esbuild from \"esbuild\";\nimport { EsbuildBinaryMissingError } from \"../errors/esbuild-binary-missing-error\";\n\nexport { EsbuildBinaryMissingError };\n\n/**\n * The exact substring esbuild's own loader throws when the platform-specific\n * binary package (e.g. `@esbuild/win32-x64`) never got installed. This is\n * the signature pnpm's build-script approval gate leaves behind: it blocks\n * esbuild's postinstall script, so the binary is never linked into place,\n * and any later call into esbuild dies with this message instead of a\n * project-shaped one.\n *\n * Source: `pkgAndSubpathForCurrentPlatform` in esbuild's `lib/main.js`.\n */\nconst UNLINKED_BINARY_SIGNATURE = \"could not be found, and is needed by esbuild\";\n\n/**\n * A cheap operation that forces esbuild to resolve and invoke its platform\n * binary. Injectable so tests can simulate a missing binary without deleting\n * anything from a real install.\n */\nexport type EsbuildProbe = () => void;\n\n/**\n * The default probe: a trivial `transformSync` call. Trivial input keeps the\n * cost negligible while still forcing esbuild to resolve and run its native\n * binary, which is the only way an unlinked binary actually surfaces.\n */\nfunction runDefaultProbe(): void {\n esbuild.transformSync(\"\", { loader: \"js\" });\n}\n\n/**\n * Fail fast, before dev or build does any other work, when esbuild's native\n * binary is not linked.\n *\n * A healthy esbuild install is a no-op. A binary genuinely missing — because\n * a platform package was never installed, or a package manager blocked its\n * postinstall script — is turned into {@link EsbuildBinaryMissingError}, a\n * message that names the cause and the fix. Any other failure (a real syntax\n * error, an unrelated platform mismatch, …) is rethrown unchanged — this\n * preflight only owns the one known failure mode.\n *\n * @param probe Overrides the default `esbuild.transformSync` invocation.\n * Intended for tests only.\n * @throws {EsbuildBinaryMissingError} when the platform binary is missing or unlinked.\n */\nexport function assertEsbuildBinaryIsLinked(probe: EsbuildProbe = runDefaultProbe): void {\n try {\n probe();\n } catch (error) {\n if (isUnlinkedBinaryError(error)) {\n throw new EsbuildBinaryMissingError({ cause: error });\n }\n\n throw error;\n }\n}\n\n/**\n * Narrow an unknown thrown value down to esbuild's known \"binary not\n * linked\" failure, identified by the fixed substring esbuild itself throws.\n */\nfunction isUnlinkedBinaryError(error: unknown): boolean {\n return error instanceof Error && error.message.includes(UNLINKED_BINARY_SIGNATURE);\n}\n"],"mappings":";;;;;;;;;;;;;;AAeA,MAAM,4BAA4B;;;;;;AAclC,SAAS,kBAAwB;CAC/B,QAAQ,cAAc,IAAI,EAAE,QAAQ,KAAK,CAAC;AAC5C;;;;;;;;;;;;;;;;AAiBA,SAAgB,4BAA4B,QAAsB,iBAAuB;CACvF,IAAI;EACF,MAAM;CACR,SAAS,OAAO;EACd,IAAI,sBAAsB,KAAK,GAC7B,MAAM,IAAI,0BAA0B,EAAE,OAAO,MAAM,CAAC;EAGtD,MAAM;CACR;AACF;;;;;AAMA,SAAS,sBAAsB,OAAyB;CACtD,OAAO,iBAAiB,SAAS,MAAM,QAAQ,SAAS,yBAAyB;AACnF"}
|
package/llms-full.txt
CHANGED
|
@@ -3520,7 +3520,7 @@ const result = await measure("publish-event", () =>
|
|
|
3520
3520
|
|
|
3521
3521
|
---
|
|
3522
3522
|
name: run-app
|
|
3523
|
-
description: 'Three operational commands — `warlock dev` (HMR + type-gen + health checks), `warlock build` (esbuild bundle), `warlock start` (spawn the production bundle). All flags, all `warlock.config.ts` knobs that shape them. Triggers: `warlock dev`, `warlock build`, `warlock start`, `devServer`, `--fresh`, `--skip-typings`, `--skip-health`, `outdir`, `outFile`, `sourcemap`, `PortInUseError`, `assertPortIsAvailable`, `EADDRINUSE`; "start the dev server", "build for production", "run the bundle", "skip type generation", "tune watch globs", "dev server keyboard shortcuts", "press r to restart", "press q to quit", "restart the dev server", "port already in use"; typical config `warlock.config.ts > devServer / build`. Skip: writing a custom CLI — `@warlock.js/core/write-cli-command/SKILL.md`; config shape — `@warlock.js/core/configure-app/SKILL.md`; competing tooling `nodemon`, `tsx`, `ts-node-dev`, `esbuild` direct.'
|
|
3523
|
+
description: 'Three operational commands — `warlock dev` (HMR + type-gen + health checks), `warlock build` (esbuild bundle), `warlock start` (spawn the production bundle). All flags, all `warlock.config.ts` knobs that shape them. Triggers: `warlock dev`, `warlock build`, `warlock start`, `devServer`, `--fresh`, `--skip-typings`, `--skip-health`, `outdir`, `outFile`, `sourcemap`, `PortInUseError`, `assertPortIsAvailable`, `EADDRINUSE`, `EsbuildBinaryMissingError`; "start the dev server", "build for production", "run the bundle", "skip type generation", "tune watch globs", "dev server keyboard shortcuts", "press r to restart", "press q to quit", "restart the dev server", "port already in use"; typical config `warlock.config.ts > devServer / build`. Skip: writing a custom CLI — `@warlock.js/core/write-cli-command/SKILL.md`; config shape — `@warlock.js/core/configure-app/SKILL.md`; competing tooling `nodemon`, `tsx`, `ts-node-dev`, `esbuild` direct.'
|
|
3524
3524
|
---
|
|
3525
3525
|
|
|
3526
3526
|
# Warlock — run the app
|
|
@@ -3663,6 +3663,7 @@ export default defineConfig({
|
|
|
3663
3663
|
restartOnConfigChange: true, // restart when warlock.config.ts / .env* changes
|
|
3664
3664
|
healthCheckers: [...] /* or false */,
|
|
3665
3665
|
transpileCacheDebug: false, // name cache files <slug>.<hash>.js w/ // @source markers
|
|
3666
|
+
timings: false, // print a per-phase reload timing breakdown
|
|
3666
3667
|
},
|
|
3667
3668
|
});
|
|
3668
3669
|
```
|
|
@@ -3673,6 +3674,7 @@ export default defineConfig({
|
|
|
3673
3674
|
- **`transpileCacheDebug`** — diagnostic only. Names `.warlock/transpile/*.js` files `<slug>.<hash>.js` and appends `// @source <path>` markers so you can eyeball which cache entry came from which source. Leave off in normal use.
|
|
3674
3675
|
- **`checkForUpdates`** — on `warlock dev` start, check npm for a newer `@warlock.js/core` and print a one-line notice if one exists. Best-effort and non-blocking; auto-skipped in CI and non-TTY shells. In an interactive terminal the notice arms a **`u` shortcut** that updates every `@warlock.js/*` package, installs, and restarts the server; elsewhere it prints `npx warlock update` instead. The registry answer is cached for 24h in `.warlock/update-check.json`, so a day of restarts costs one lookup. See [`update-packages/SKILL.md`](../update-packages/SKILL.md).
|
|
3675
3676
|
- **`restartOnConfigChange`** — restart the dev server when `warlock.config.ts` or any `.env*` changes (default `true`). Set `false` to get a warning instead and restart by hand. Neither file can be hot-reloaded, so without a restart the running services keep the old values.
|
|
3677
|
+
- **`timings`** — print a one-line, per-phase breakdown next to the `hmr update` line on every hot reload: `watcher`, `debounce`, `graph`, `reimport`, `connectors`. `watcher` is the raw-fs-notification-to-stabilised-event gap (chokidar's `awaitWriteFinish` window); `debounce` is the handler's own adaptive wait; the rest are self-explanatory. Opt-in, off by default — a disabled flag costs one boolean check per reload, since the watcher-settle bookkeeping only runs when this is on. Use it to see which phase a slow reload is actually spending time in.
|
|
3676
3678
|
|
|
3677
3679
|
## `warlock build` — production bundle
|
|
3678
3680
|
|
|
@@ -3778,7 +3780,7 @@ Means `docker stop` / `kubectl delete pod` works as expected: SIGTERM reaches th
|
|
|
3778
3780
|
|
|
3779
3781
|
### pnpm needs esbuild's install script allowed
|
|
3780
3782
|
|
|
3781
|
-
pnpm 10+ will not run a dependency's install script unless the app names it. esbuild's script links its platform-native binary, and `warlock build`
|
|
3783
|
+
pnpm 10+ will not run a dependency's install script unless the app names it. esbuild's script links its platform-native binary, and both `warlock build` and `warlock dev` shell out to that binary — so the app installs cleanly and then cannot build or start dev:
|
|
3782
3784
|
|
|
3783
3785
|
```
|
|
3784
3786
|
[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: esbuild@0.27.7
|
|
@@ -3793,6 +3795,8 @@ allowBuilds:
|
|
|
3793
3795
|
|
|
3794
3796
|
Note pnpm reads this from `pnpm-workspace.yaml`, **not** from `package.json`'s `pnpm` field — pnpm 11 warns that the field is ignored and then carries on, so settings left there fail silently.
|
|
3795
3797
|
|
|
3798
|
+
`warlock dev` checks for esbuild's native binary before starting (as `warlock build` already did) and fails fast with `EsbuildBinaryMissingError`, naming the same fix, instead of surfacing an opaque low-level error later from inside the bundler.
|
|
3799
|
+
|
|
3796
3800
|
Nothing else is needed for pnpm. Warlock never requires an app to declare a package it does not import: generated code is checked at build time against the app's own `dependencies`, so `warlock build` failing over an unfamiliar package name is a framework bug, not a missing dependency.
|
|
3797
3801
|
|
|
3798
3802
|
### Output streams — what a supervisor may trust
|
|
@@ -7033,6 +7037,30 @@ router.get("/analytics/summary", summaryController, {
|
|
|
7033
7037
|
|
|
7034
7038
|
`cacheKey` can be a string OR a function `(request) => string | Promise<string>` for per-request keys. Excludes failures and omits `["user", "settings"]` from the cached body by default.
|
|
7035
7039
|
|
|
7040
|
+
### Tag-based invalidation
|
|
7041
|
+
|
|
7042
|
+
Give it `tags` — a static list, or a function of the request — to evict the entry early with `cache.tags([...]).invalidate()` (`@warlock.js/cache`), instead of waiting out `ttl`. Mirrors `route.cache.tags` on `@warlock.js/web`'s page cache, so an API response and a page can share the same tag and be invalidated together:
|
|
7043
|
+
|
|
7044
|
+
```ts
|
|
7045
|
+
import { middleware } from "@warlock.js/core";
|
|
7046
|
+
import { cache } from "@warlock.js/cache";
|
|
7047
|
+
|
|
7048
|
+
router.get("/orders/:id", getOrderController, {
|
|
7049
|
+
middleware: [
|
|
7050
|
+
middleware.cache({
|
|
7051
|
+
cacheKey: (request) => `orders.${request.params.id}`,
|
|
7052
|
+
ttl: 300,
|
|
7053
|
+
tags: (request) => [`order.${request.params.id}`],
|
|
7054
|
+
}),
|
|
7055
|
+
],
|
|
7056
|
+
});
|
|
7057
|
+
|
|
7058
|
+
// Elsewhere, after the order changes:
|
|
7059
|
+
await cache.tags([`order.${orderId}`]).invalidate();
|
|
7060
|
+
```
|
|
7061
|
+
|
|
7062
|
+
Untagged entries (no `tags` given) behave exactly as before — they only expire via `ttl`.
|
|
7063
|
+
|
|
7036
7064
|
## Composed example
|
|
7037
7065
|
|
|
7038
7066
|
Tight cap on logins, concurrency + idempotency on AI calls:
|
|
@@ -8947,7 +8975,7 @@ CLI commands, scheduled jobs, queue workers — anything running outside an HTTP
|
|
|
8947
8975
|
|
|
8948
8976
|
---
|
|
8949
8977
|
name: write-cli-command
|
|
8950
|
-
description: 'Author a custom `warlock <my-cmd>` command via the `command()` factory — name, description, action, options, preload, then register in `warlock.config.ts > cli.commands` or drop in `src/app/<module>/commands/`. Also covers built-in `warlock add` feature scaffolding, including the Web starter and `index.register.ts`. Triggers: `command`, `CLICommand`, `CLICommandPreload`, `CLICommandOption`, `preload`, `preAction`, `persistent`, `colors`, `warlock add`, `index.register.ts`; "write a custom warlock command", "one-off maintenance task", "ship a CLI from a package", "framework built-in commands"; typical import `import { command } from "@warlock.js/core"`. Skip: framework dev/build/start — `@warlock.js/core/run-app/SKILL.md`; warlock.config.ts wiring — `@warlock.js/core/configure-app/SKILL.md`; competing libs `commander`, `yargs`, `oclif`.'
|
|
8978
|
+
description: 'Author a custom `warlock <my-cmd>` command via the `command()` factory — name, description, action, options, preload, then register in `warlock.config.ts > cli.commands` or drop in `src/app/<module>/commands/`. Also covers built-in `warlock add` feature scaffolding, including the Web starter and `index.register.ts`. Triggers: `command`, `CLICommand`, `CLICommandPreload`, `CLICommandOption`, `preload`, `preAction`, `persistent`, `colors`, `warlock add`, `warlock add bull-board`, `index.register.ts`; "write a custom warlock command", "one-off maintenance task", "ship a CLI from a package", "framework built-in commands"; typical import `import { command } from "@warlock.js/core"`. Skip: framework dev/build/start — `@warlock.js/core/run-app/SKILL.md`; warlock.config.ts wiring — `@warlock.js/core/configure-app/SKILL.md`; competing libs `commander`, `yargs`, `oclif`.'
|
|
8951
8979
|
---
|
|
8952
8980
|
|
|
8953
8981
|
# Warlock — write a CLI command
|
|
@@ -9154,6 +9182,7 @@ warlock generate (alias: g) — interactive picker
|
|
|
9154
9182
|
| `react-email` | `react-email` + `@react-email/components` + `@react-email/render` + `@react-email/tailwind`; drops a `welcome-email.tsx` sample; patches `tsconfig.json` |
|
|
9155
9183
|
| `web` | Warlock Web + React stack; scaffolds the application page, localization, contact endpoint, and Web configuration |
|
|
9156
9184
|
| `react` | `react` + `react-dom` + types |
|
|
9185
|
+
| `bull-board` | `@bull-board/api` + `@bull-board/fastify`; writes a `dashboard` block to `src/config/queue.ts`. `requires: ["queue"]` — adds the `queue` feature first automatically when it's missing |
|
|
9157
9186
|
| `image` | `sharp` (for the `Image` class) |
|
|
9158
9187
|
| `mail` | `nodemailer` + types |
|
|
9159
9188
|
| `ses` | `@aws-sdk/client-sesv2` |
|
package/llms.txt
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
- [request-tracing](@warlock.js/core/request-tracing/SKILL.md): Vendor-neutral request tracing hooks (`http.tracing`) — observe request start/end and named phase spans (`route.match`, `middleware`, `validation`, `handler`, `response.write`) without adopting an OTel/vendor dependency in `core`. Covers the `TracingHooks` shape, trace id derivation from an inbound W3C `traceparent` vs `request.id`, that a throwing hook is caught and reported once (never breaks the request), and zero overhead when disabled. Triggers: `http.tracing`, `TracingHooks`, `onRequestStart`, `onRequestEnd`, `onPhase`, `traceparent`, `traceId`, `dispatchPhase`, "instrument requests", "trace a request", "slow phase logging", "APM / OTel bridge for Warlock". Skip: request-id header echo/inheritance (`X-Request-Id`) — `@warlock.js/core/use-middleware/SKILL.md`; app-level structured logging — `@warlock.js/logger/logger-basics/SKILL.md`; competing libs `@opentelemetry/api` direct instrumentation, `express-request-id`, hand-rolled `X-Trace-Id` middleware.
|
|
25
25
|
- [resolve-path](@warlock.js/core/resolve-path/SKILL.md): Path helpers anchored at `process.cwd()` — `rootPath`, `srcPath`, `appPath`, `configPath`, `publicPath`, `storagePath`, `uploadsPath`, `cachePath`, `logsPath`, `tempPath`, `warlockPath`, `sanitizePath`. Optional `uploads.root` config overrides the uploads anchor. Triggers: `appPath`, `configPath`, `uploadsPath`, `storagePath`, `publicPath`, `cachePath`, `logsPath`, `tempPath`, `sanitizePath`, `paths`; "resolve a path inside src/app", "absolute upload destination", "sanitize a user filename", "ship uploads to a mounted volume"; typical import `import { appPath, uploadsPath } from "@warlock.js/core"`. Skip: HTTP URL helpers — `@warlock.js/core/build-url/SKILL.md`; app metadata — `@warlock.js/core/use-app-context/SKILL.md`; storage abstraction — `@warlock.js/core/store-file/SKILL.md`; competing patterns: `path.join(process.cwd(), ...)`, hand-rolled directory constants.
|
|
26
26
|
- [retry-operation](@warlock.js/core/retry-operation/SKILL.md): Wrap a flaky operation with `retry(fn, options)` — now provided by `@mongez/reinforcements` (not `@warlock.js/core`). `attempts` total tries, `delay` + `backoff` (linear/exponential/fn), `maxDelay`, `jitter`, `shouldRetry` to bail on permanent errors, `signal` to cancel, plus `retryable()` to pre-bind options. Triggers: `retry`, `retryable`, `RetryOptions`, `attempts`, `backoff`, `jitter`, `maxDelay`, `shouldRetry`, `signal`; "retry a flaky API call", "handle transient errors", "exponential backoff with jitter", "wrap an external request"; typical import `import { retry } from "@mongez/reinforcements"`. Skip: timing the retried op — `@warlock.js/core/benchmark-code/SKILL.md`; use-case-level `retry` option — `@warlock.js/core/write-use-case/SKILL.md`; competing libs `p-retry`, `async-retry`, `cockatiel`.
|
|
27
|
-
- [run-app](@warlock.js/core/run-app/SKILL.md): Three operational commands — `warlock dev` (HMR + type-gen + health checks), `warlock build` (esbuild bundle), `warlock start` (spawn the production bundle). All flags, all `warlock.config.ts` knobs that shape them. Triggers: `warlock dev`, `warlock build`, `warlock start`, `devServer`, `--fresh`, `--skip-typings`, `--skip-health`, `outdir`, `outFile`, `sourcemap`, `PortInUseError`, `assertPortIsAvailable`, `EADDRINUSE`; "start the dev server", "build for production", "run the bundle", "skip type generation", "tune watch globs", "dev server keyboard shortcuts", "press r to restart", "press q to quit", "restart the dev server", "port already in use"; typical config `warlock.config.ts > devServer / build`. Skip: writing a custom CLI — `@warlock.js/core/write-cli-command/SKILL.md`; config shape — `@warlock.js/core/configure-app/SKILL.md`; competing tooling `nodemon`, `tsx`, `ts-node-dev`, `esbuild` direct.
|
|
27
|
+
- [run-app](@warlock.js/core/run-app/SKILL.md): Three operational commands — `warlock dev` (HMR + type-gen + health checks), `warlock build` (esbuild bundle), `warlock start` (spawn the production bundle). All flags, all `warlock.config.ts` knobs that shape them. Triggers: `warlock dev`, `warlock build`, `warlock start`, `devServer`, `--fresh`, `--skip-typings`, `--skip-health`, `outdir`, `outFile`, `sourcemap`, `PortInUseError`, `assertPortIsAvailable`, `EADDRINUSE`, `EsbuildBinaryMissingError`; "start the dev server", "build for production", "run the bundle", "skip type generation", "tune watch globs", "dev server keyboard shortcuts", "press r to restart", "press q to quit", "restart the dev server", "port already in use"; typical config `warlock.config.ts > devServer / build`. Skip: writing a custom CLI — `@warlock.js/core/write-cli-command/SKILL.md`; config shape — `@warlock.js/core/configure-app/SKILL.md`; competing tooling `nodemon`, `tsx`, `ts-node-dev`, `esbuild` direct.
|
|
28
28
|
- [send-mail](@warlock.js/core/send-mail/SKILL.md): Send transactional email — `Mail` fluent builder, `sendMail()` direct call, React Email components. Test mode auto-captures into an in-memory mailbox; dev mode logs. Triggers: `Mail.to`, `sendMail`, `setMailMode`, `mailEvents`, `assertMailSent`, `getTestMailbox`, `wasMailSentTo`, `closeAllMailers`; "send a transactional email", "build a React Email template", "configure SMTP or SES", "assert an email was sent in tests"; typical import `import { Mail, sendMail } from "@warlock.js/core"`. Skip: per-config wiring — `@warlock.js/core/configure-app/SKILL.md`; layered service patterns — `@warlock.js/core/warlock-conventions/SKILL.md`; competing libs `nodemailer` direct, `@sendgrid/mail`, `resend`, `mailgun.js`.
|
|
29
29
|
- [send-response](@warlock.js/core/send-response/SKILL.md): Send HTTP responses via @warlock.js/core's Response helpers — success/error variants, status helpers, redirects, files, streams, and SSE. Picking the right helper carries the HTTP semantic without manual status codes. Triggers: `response.success`, `response.successCreate`, `response.notFound`, `response.forbidden`, `response.badRequest`, `response.sendFile`, `response.stream`, `response.sse`, `response.replay`, `ResourceNotFoundError`, `ForbiddenError`; "return a 201 from a controller", "send a file", "stream Server-Sent Events", "throw HTTP-shaped errors from services"; typical import `import type { RequestHandler, Response } from "@warlock.js/core"`. Skip: controller shape — `@warlock.js/core/create-controller/SKILL.md`; route registration — `@warlock.js/core/register-route/SKILL.md`; competing patterns: hand-rolled status codes via `reply.code(404).send(...)`, raw Fastify reply.
|
|
30
30
|
- [store-file](@warlock.js/core/store-file/SKILL.md): Read/write/delete files via the `storage` singleton — disks, drivers (local/S3/R2/DO Spaces), `storage.use(name)`, `StorageFile` handles, presigned URLs. Triggers: `storage.put`, `storage.get`, `storage.use`, `StorageFile`, `storageConfigurations`, `getPresignedUrl`, `getPresignedUploadUrl`; "save an uploaded file", "switch between local and S3", "generate a presigned URL", "read file metadata"; typical import `import { storage } from "@warlock.js/core"`. Skip: multipart parsing + image chain — `@warlock.js/core/upload-file/SKILL.md`; image transforms — `@warlock.js/core/process-image/SKILL.md`; storage config shape — `@warlock.js/core/configure-app/SKILL.md`; competing libs `@aws-sdk/client-s3`, `multer`, `formidable`.
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
- [warlock-doctor](@warlock.js/core/warlock-doctor/SKILL.md): Run `warlock doctor` — a read-only diagnostics command that checks routes / config / connectors / optional-peers / health endpoints / release hygiene and prints a pass/warn/fail report, exiting non-zero on any failure. Add your own probe with the `DoctorCheck` contract and `runChecks` / `formatReportLines`. Triggers: `warlock doctor`, `doctorCommand`, `DoctorCheck`, `CheckResult`, `CheckStatus`, `DoctorReport`, `runChecks`, `formatReportLines`, `printReport`, `defaultDoctorChecks`; "diagnose my app", "preflight / preflight check", "is the app healthy", "why are there 0 routes", "pre-release sanity check", "CI smoke check"; run as `npx warlock doctor`. Skip: the live `/health` + `/ready` HTTP probes — `@warlock.js/core/health-checks/SKILL.md`; authoring a general CLI command — `@warlock.js/core/write-cli-command/SKILL.md`; releasing the package — `releasing-warlock-monorepo`; competing tools `npm doctor`, `nest info`, hand-rolled preflight scripts.
|
|
44
44
|
- [warlock-routes](@warlock.js/core/warlock-routes/SKILL.md): Run `warlock routes` — a read-only command that lists the registered HTTP routes as a verb-colored table (method / path / name / action / middleware-count / source), a sibling of `warlock doctor`. Filter with `--method` / `--path` / `--name`, or emit normalized rows as JSON with `--json`. Also covers `warlock routes:diff`, which compares live page routes against the last `warlock build`'s route snapshot and exits non-zero on drift. Triggers: `warlock routes`, `routesCommand`, `warlock routes:diff`, `routesDiffCommand`, "list my routes", "show all routes", "route table", "what endpoints does my app expose", "dump routes as JSON", "which routes have middleware", "route map for CI", "did my page routes drift from the last build"; run as `npx warlock routes` / `npx warlock routes:diff`. Skip: read-only health/preflight checks — `@warlock.js/core/warlock-doctor/SKILL.md`; defining/naming/grouping routes — `@warlock.js/core/register-route/SKILL.md`; authoring a general CLI command — `@warlock.js/core/write-cli-command/SKILL.md`; competing tools `nest`/`express` route listers, `php artisan route:list`.
|
|
45
45
|
- [wire-socket](@warlock.js/core/wire-socket/SKILL.md): Configure Socket.IO via `src/config/socket.ts`, reach the live server through `getSocketServer()` (or `app.socket` post-bootstrap), register `connection` handlers once the late-phase socket connector has booted, emit from controllers/services, use rooms and namespaces. Triggers: `app.socket`, `getSocketServer`, `SocketOptions`, `socket.io` `Server`, `socket.join`, `socket.to`, `io.of`, `io.use`; "add realtime chat", "emit socket events from a service", "use rooms and namespaces", "per-socket JWT auth". Skip: connector lifecycle — `@warlock.js/core/add-connector/SKILL.md`; app context accessors — `@warlock.js/core/use-app-context/SKILL.md`; competing libs `ws`, `socket.io` direct without Warlock connector, `uWebSockets.js`.
|
|
46
|
-
- [write-cli-command](@warlock.js/core/write-cli-command/SKILL.md): Author a custom `warlock <my-cmd>` command via the `command()` factory — name, description, action, options, preload, then register in `warlock.config.ts > cli.commands` or drop in `src/app/<module>/commands/`. Also covers built-in `warlock add` feature scaffolding, including the Web starter and `index.register.ts`. Triggers: `command`, `CLICommand`, `CLICommandPreload`, `CLICommandOption`, `preload`, `preAction`, `persistent`, `colors`, `warlock add`, `index.register.ts`; "write a custom warlock command", "one-off maintenance task", "ship a CLI from a package", "framework built-in commands"; typical import `import { command } from "@warlock.js/core"`. Skip: framework dev/build/start — `@warlock.js/core/run-app/SKILL.md`; warlock.config.ts wiring — `@warlock.js/core/configure-app/SKILL.md`; competing libs `commander`, `yargs`, `oclif`.
|
|
46
|
+
- [write-cli-command](@warlock.js/core/write-cli-command/SKILL.md): Author a custom `warlock <my-cmd>` command via the `command()` factory — name, description, action, options, preload, then register in `warlock.config.ts > cli.commands` or drop in `src/app/<module>/commands/`. Also covers built-in `warlock add` feature scaffolding, including the Web starter and `index.register.ts`. Triggers: `command`, `CLICommand`, `CLICommandPreload`, `CLICommandOption`, `preload`, `preAction`, `persistent`, `colors`, `warlock add`, `warlock add bull-board`, `index.register.ts`; "write a custom warlock command", "one-off maintenance task", "ship a CLI from a package", "framework built-in commands"; typical import `import { command } from "@warlock.js/core"`. Skip: framework dev/build/start — `@warlock.js/core/run-app/SKILL.md`; warlock.config.ts wiring — `@warlock.js/core/configure-app/SKILL.md`; competing libs `commander`, `yargs`, `oclif`.
|
|
47
47
|
- [write-middleware](@warlock.js/core/write-middleware/SKILL.md): Author HTTP middleware for @warlock.js/core — the `({ request, response })` signature, short-circuit by returning a response, enrich the request with extra fields, register per-route, per-group, or app-wide. Triggers: `Middleware`, `MiddlewareResponse`, `router.group`, `guarded`, `request.detectIp`, `authMiddleware`; "write a custom middleware", "short-circuit a request", "enrich the request with extra fields", "per-route vs per-group middleware"; typical import `import type { Middleware } from "@warlock.js/core"`. Skip: built-in middleware catalog — `@warlock.js/core/use-middleware/SKILL.md`; route attachment — `@warlock.js/core/register-route/SKILL.md`; response helpers — `@warlock.js/core/send-response/SKILL.md`; competing patterns: `express` `(req, res, next)` middleware, Fastify `preHandler` hooks.
|
|
48
48
|
- [write-seeder](@warlock.js/core/write-seeder/SKILL.md): Author a seed file under `src/app/<module>/seeds/<name>.ts` using the `seeder()` factory — `name`, `dependsOn`, `once`, `order`, `batchSize`, `run({ track, now, batchSize })`. Auto-discovered by `warlock seed`; tracked in a `seeds` table; per-record refs in `seed_records` so `warlock seed --drop` can undo a seed. Triggers: `seeder`, `Seeder`, `SeedResult`, `SeedContext`, `SeedClock`, `track`, `now`, `batchSize`, `SeedersManager`, `warlock seed`, `--fresh`, `--drop`, `--list`, `--path`; "seed default roles", "undo a seed", "one-time data migration", "auto-discovered seeds", "order seeds by dependency", "deterministic seed timestamps", "inject a seed clock"; typical import `import { seeder } from "@warlock.js/core"`. Skip: module folder layout — `@warlock.js/core/create-module/SKILL.md`; repository CRUD — `@warlock.js/core/use-repository/SKILL.md`; CLI flags — `@warlock.js/core/write-cli-command/SKILL.md`; competing patterns: hand-rolled `node scripts/seed.js`, `typeorm-seeding`.
|
|
49
49
|
- [write-use-case](@warlock.js/core/write-use-case/SKILL.md): Author `useCase()` pipelines for business logic — guards, schema, before/after middleware, retry, benchmark, broadcast, lifecycle callbacks; transport-agnostic and observable by default. Input is inferred from the `schema`. Triggers: `useCase`, `UseCaseContext`, `UseCaseResult`, `retry`, `benchmark`, `broadcast`, `description`, `globalUseCasesEvents`, `UseCaseBroadcastChannel`; "encapsulate a business operation", "share logic between HTTP and CLI", "add guards and lifecycle hooks", "broadcast a use case result", "transport-agnostic pipeline"; typical import `import { useCase } from "@warlock.js/core"`. Skip: thin handler shape — `@warlock.js/core/create-controller/SKILL.md`; schema details — `@warlock.js/core/validate-input/SKILL.md`; the standalone retry util — `@warlock.js/core/retry-operation/SKILL.md`; competing libs `@nestjs/cqrs`, `inversify`, hand-rolled service classes.
|
package/package.json
CHANGED
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
"@mongez/slug": "^1.0.7",
|
|
26
26
|
"@mongez/supportive-is": "^2.1.4",
|
|
27
27
|
"@mongez/time-wizard": "^1.0.6",
|
|
28
|
-
"@warlock.js/cache": "5.
|
|
29
|
-
"@warlock.js/cascade": "5.
|
|
30
|
-
"@warlock.js/context": "5.
|
|
31
|
-
"@warlock.js/logger": "5.
|
|
32
|
-
"@warlock.js/seal": "5.
|
|
33
|
-
"@warlock.js/fs": "5.
|
|
28
|
+
"@warlock.js/cache": "5.14.0",
|
|
29
|
+
"@warlock.js/cascade": "5.14.0",
|
|
30
|
+
"@warlock.js/context": "5.14.0",
|
|
31
|
+
"@warlock.js/logger": "5.14.0",
|
|
32
|
+
"@warlock.js/seal": "5.14.0",
|
|
33
|
+
"@warlock.js/fs": "5.14.0",
|
|
34
34
|
"chokidar": "^5.0.0",
|
|
35
35
|
"dayjs": "^1.11.19",
|
|
36
36
|
"es-module-lexer": "^2.0.0",
|
|
@@ -56,10 +56,10 @@
|
|
|
56
56
|
"react": "^19.2.3",
|
|
57
57
|
"react-dom": "^19.2.3",
|
|
58
58
|
"@react-email/render": "^2.0.5",
|
|
59
|
-
"@warlock.js/herald": "5.
|
|
60
|
-
"@warlock.js/ai": "5.
|
|
61
|
-
"@warlock.js/access": "5.
|
|
62
|
-
"@warlock.js/notifications": "5.
|
|
59
|
+
"@warlock.js/herald": "5.14.0",
|
|
60
|
+
"@warlock.js/ai": "5.14.0",
|
|
61
|
+
"@warlock.js/access": "5.14.0",
|
|
62
|
+
"@warlock.js/notifications": "5.14.0"
|
|
63
63
|
},
|
|
64
64
|
"peerDependenciesMeta": {
|
|
65
65
|
"sharp": {
|
|
@@ -122,7 +122,7 @@
|
|
|
122
122
|
],
|
|
123
123
|
"author": "hassanzohdy",
|
|
124
124
|
"license": "MIT",
|
|
125
|
-
"version": "5.
|
|
125
|
+
"version": "5.14.0",
|
|
126
126
|
"type": "module",
|
|
127
127
|
"main": "./esm/index.mjs",
|
|
128
128
|
"module": "./esm/index.mjs",
|