create-warlock 5.0.1 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,44 @@ All notable changes to `create-warlock` are documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). `@warlock.js/*` packages are released in lockstep — every package shares the same version number, so a version below may list only the changes that affected this package.
6
6
 
7
+ ## 5.1.0
8
+
9
+ > **If your scaffold includes the `web` feature, upgrade.** React did not execute at all
10
+ > in published installs of `@warlock.js/web` 5.0.0 through 5.0.2 — see that package's
11
+ > changelog for the defect and its fix.
12
+
13
+ ### Added
14
+
15
+ - **The scaffold typechecks from a fresh install, and a CI gate keeps it that way.** A
16
+ newly created project previously could fail `tsc` on its own generated source.
17
+ - **A real home page**, replacing the placeholder — it includes a counter whose working
18
+ state is proof that hydration actually ran in the browser.
19
+
20
+ ### Changed
21
+
22
+ - **`src/typings.d.ts` is now the sanctioned home for `RequestLocals` / `RequestUser`
23
+ module augmentation.** The file is generated with both augmentation blocks stubbed and
24
+ commented, so there is one obvious place to declare per-request typed data.
25
+ - Replaced stale scaffold values that had been carried forward: the `wow2` project name
26
+ and the `4.15.0` dependency version no longer appear in generated projects.
27
+
28
+ ## 5.0.2 - 2026-08-25
29
+
30
+ No changes to `create-warlock`. Released in lockstep with the `@warlock.js/web` SSR fix
31
+ (`ssr.noExternal`) — see that package's changelog.
32
+
33
+ ## 5.0.1 - 2026-08-25
34
+
35
+ ### Fixed
36
+
37
+ - **The `warlock` binary was never linked in a yarn-1 scaffold.** Installing the batched
38
+ features under yarn 1 hit an *Invariant Violation* in yarn's linker, which aborted the
39
+ install before `node_modules/.bin` was written — leaving a scaffolded project whose
40
+ own `warlock` command did not exist. `App.pinViteResolution()` now writes matching
41
+ `resolutions` and `overrides` entries for vite into the generated `package.json`
42
+ *before* the batched feature install runs, so a single vite version is resolved and
43
+ the linker completes.
44
+
7
45
  ## 5.0.0 - 2026-08-25
8
46
 
9
47
  ### Added
@@ -17,6 +17,18 @@ const features = [
17
17
  hint: "Server-rendered React pages on the same server as your API — scaffolds src/web and registers the connector",
18
18
  group: "Rendering & Mail"
19
19
  },
20
+ {
21
+ key: "tailwind",
22
+ label: "Tailwind CSS",
23
+ hint: "Tailwind v4 for the SSR page layer — creates src/web/app.css (the CSS-first config) and wires it through Vite (pulls web)",
24
+ group: "Rendering & Mail"
25
+ },
26
+ {
27
+ key: "shadcn",
28
+ label: "shadcn/ui prerequisites",
29
+ hint: "components.json, cn(), design tokens, and the web/* tsconfig path so `npx shadcn add <component>` works first time (pulls tailwind)",
30
+ group: "Rendering & Mail"
31
+ },
20
32
  {
21
33
  key: "react",
22
34
  label: "React (rendering & mails)",
@@ -1 +1 @@
1
- {"version":3,"file":"features-map.mjs","names":[],"sources":["../../../../../../create-warlock/src/features/features-map.ts"],"sourcesContent":["/**\r\n * Presentation manifest for the scaffolder's feature prompts.\r\n *\r\n * This file is **display metadata only** — keys, labels, hints, grouping, and\r\n * default selections. It deliberately holds NO dependency names or versions:\r\n * the single source of truth for what each feature installs is the `add`\r\n * command's feature map in `@warlock.js/core`\r\n * (`src/generations/add-command.action.ts`). The scaffolder collects the\r\n * selections here and delegates the actual install to `warlock add`, so the\r\n * two never drift on versions again.\r\n *\r\n * Every `key` below MUST exist in core's `allowedFeatures`; a CI guard should\r\n * assert that subset relationship so a typo fails the build instead of shipping.\r\n */\r\n\r\nexport type FeatureGroup =\r\n | \"Auth & Access\"\r\n | \"Rendering & Mail\"\r\n | \"Media\"\r\n | \"Storage & Cache\"\r\n | \"Jobs & Messaging\"\r\n | \"Realtime\"\r\n | \"API Docs\"\r\n | \"Tooling\";\r\n\r\nexport type FeatureOption = {\r\n /** Must match a key in core's `add` feature map. */\r\n key: string;\r\n label: string;\r\n hint: string;\r\n group: FeatureGroup;\r\n defaultSelected?: boolean;\r\n};\r\n\r\n/**\r\n * Optional features offered in the general multiselect step. The database\r\n * driver (its own select) and AI providers + capability packages (their own\r\n * step) are intentionally NOT here — they have dedicated prompts.\r\n */\r\nexport const features: FeatureOption[] = [\r\n // Auth & Access\r\n {\r\n key: \"access\",\r\n label: \"Access (authorization)\",\r\n hint: \"RBAC permission checks, ABAC policies, and roles — pairs with auth\",\r\n group: \"Auth & Access\",\r\n },\r\n\r\n // Rendering & Mail\r\n {\r\n key: \"web\",\r\n label: \"Web (SSR React pages)\",\r\n hint: \"Server-rendered React pages on the same server as your API — scaffolds src/web and registers the connector\",\r\n group: \"Rendering & Mail\",\r\n },\r\n {\r\n key: \"react\",\r\n label: \"React (rendering & mails)\",\r\n hint: \"React + ReactDOM for non-interactive rendering and HTML/email generation\",\r\n group: \"Rendering & Mail\",\r\n defaultSelected: true,\r\n },\r\n {\r\n key: \"react-email\",\r\n label: \"React Email\",\r\n hint: \"Build email templates with React + Tailwind (pulls react + mail)\",\r\n group: \"Rendering & Mail\",\r\n },\r\n {\r\n key: \"mail\",\r\n label: \"Mail (Nodemailer)\",\r\n hint: \"Send emails via SMTP\",\r\n group: \"Rendering & Mail\",\r\n },\r\n {\r\n key: \"ses\",\r\n label: \"Amazon SES\",\r\n hint: \"Send emails via the AWS SES API\",\r\n group: \"Rendering & Mail\",\r\n },\r\n\r\n // Media\r\n {\r\n key: \"image\",\r\n label: \"Image processing (Sharp)\",\r\n hint: \"Resize, convert, and optimize images\",\r\n group: \"Media\",\r\n },\r\n\r\n // Storage & Cache\r\n {\r\n key: \"s3\",\r\n label: \"S3 storage\",\r\n hint: \"AWS S3 for cloud file storage\",\r\n group: \"Storage & Cache\",\r\n },\r\n {\r\n key: \"redis\",\r\n label: \"Redis cache\",\r\n hint: \"Redis driver for the cache layer\",\r\n group: \"Storage & Cache\",\r\n },\r\n\r\n // Jobs & Messaging\r\n {\r\n key: \"scheduler\",\r\n label: \"Scheduler\",\r\n hint: \"Background tasks and cron jobs\",\r\n group: \"Jobs & Messaging\",\r\n },\r\n {\r\n key: \"herald\",\r\n label: \"Herald (RabbitMQ)\",\r\n hint: \"Message broker for event-driven architecture\",\r\n group: \"Jobs & Messaging\",\r\n },\r\n {\r\n key: \"notifications\",\r\n label: \"Notifications\",\r\n hint: \"Multi-channel notifications — mail + in-app database, preferences, idempotency\",\r\n group: \"Jobs & Messaging\",\r\n },\r\n\r\n // Realtime\r\n {\r\n key: \"socket\",\r\n label: \"Socket.IO\",\r\n hint: \"Realtime websocket server\",\r\n group: \"Realtime\",\r\n },\r\n\r\n // API Docs (swagger / postman / openapi) — not shipped yet; they will arrive\r\n // together in the unified @warlock.js/api-docs package. Re-add the selection\r\n // once that package exists. (Removed so the wizard can't offer unbuilt features.)\r\n\r\n // Tooling\r\n {\r\n key: \"test\",\r\n label: \"Testing (Vitest)\",\r\n hint: \"Vitest + coverage + per-worker DB/cache test setup\",\r\n group: \"Tooling\",\r\n },\r\n];\r\n\r\nexport type AiProviderOption = {\r\n /** Must match a provider/package key in core's `add` feature map. */\r\n key: string;\r\n label: string;\r\n hint: string;\r\n};\r\n\r\n/**\r\n * AI providers offered in the dedicated AI step. Selecting any of these pulls\r\n * the core `@warlock.js/ai` package automatically via the provider's `requires`\r\n * in core's feature map — the scaffolder never lists `ai` as a standalone pick.\r\n *\r\n * Keys are `ai-`prefixed to mirror core's renamed provider feature keys\r\n * (`ai-openai` → `@warlock.js/ai-openai`, etc.).\r\n */\r\nexport const aiProviders: AiProviderOption[] = [\r\n { key: \"ai-openai\", label: \"OpenAI\", hint: \"GPT models via the OpenAI API\" },\r\n { key: \"ai-google\", label: \"Google (Gemini)\", hint: \"Gemini models via Google AI\" },\r\n { key: \"ai-anthropic\", label: \"Anthropic (Claude)\", hint: \"Claude models via the Anthropic API\" },\r\n { key: \"ai-bedrock\", label: \"AWS Bedrock\", hint: \"Foundation models via Amazon Bedrock\" },\r\n { key: \"ai-ollama\", label: \"Ollama\", hint: \"Local models via Ollama\" },\r\n];\r\n\r\n/**\r\n * AI capability packages offered alongside the providers in the dedicated AI\r\n * step. These are the satellite packages that sit on top of the core\r\n * `@warlock.js/ai` toolkit (each pulls the core `ai` package via its `requires`\r\n * in core's feature map, same as the providers).\r\n */\r\nexport const aiPackages: AiProviderOption[] = [\r\n {\r\n key: \"ai-tools\",\r\n label: \"AI Tools\",\r\n hint: \"Ready-made agent tools (web search, fetch, HTTP, calculator, date-time) + MCP client/server\",\r\n },\r\n {\r\n key: \"ai-panoptic\",\r\n label: \"AI Observability (Panoptic)\",\r\n hint: \"Tracing + exporters + a zero-setup local dashboard\",\r\n },\r\n {\r\n key: \"ai-workspace\",\r\n label: \"AI Workspace\",\r\n hint: \"Policy-jailed filesystem + shell for coding agents\",\r\n },\r\n];\r\n\r\n/**\r\n * Feature options for the multiselect prompt, ordered by group with the group\r\n * surfaced in the hint (keeps a flat list scannable without group widgets).\r\n */\r\nexport function getFeatureOptions() {\r\n return features.map(feature => ({\r\n value: feature.key,\r\n label: feature.label,\r\n hint: `${feature.group} — ${feature.hint}`,\r\n }));\r\n}\r\n\r\n/**\r\n * Keys pre-checked in the feature multiselect.\r\n */\r\nexport function getDefaultFeatureKeys(): string[] {\r\n return features.filter(feature => feature.defaultSelected).map(feature => feature.key);\r\n}\r\n\r\n/**\r\n * AI provider options for the dedicated AI multiselect step.\r\n */\r\nexport function getAiProviderOptions() {\r\n return aiProviders.map(provider => ({\r\n value: provider.key,\r\n label: provider.label,\r\n hint: provider.hint,\r\n }));\r\n}\r\n\r\n/**\r\n * AI capability package options for the dedicated AI multiselect step. Offered\r\n * together with the providers (see `getAiProviderOptions`).\r\n */\r\nexport function getAiPackageOptions() {\r\n return aiPackages.map(pkg => ({\r\n value: pkg.key,\r\n label: pkg.label,\r\n hint: pkg.hint,\r\n }));\r\n}\r\n\r\n/**\r\n * Every selectable key the scaffolder knows about (features + AI providers +\r\n * AI capability packages). Used to validate `--features` / `--ai` flags in\r\n * non-interactive mode.\r\n */\r\nexport function getAllFeatureKeys(): string[] {\r\n return [\r\n ...features.map(feature => feature.key),\r\n ...aiProviders.map(provider => provider.key),\r\n ...aiPackages.map(pkg => pkg.key),\r\n ];\r\n}\r\n"],"mappings":";;;;;;AAuCA,MAAa,WAA4B;CAEvC;EACE,KAAK;EACL,OAAO;EACP,MAAM;EACN,OAAO;CACT;CAGA;EACE,KAAK;EACL,OAAO;EACP,MAAM;EACN,OAAO;CACT;CACA;EACE,KAAK;EACL,OAAO;EACP,MAAM;EACN,OAAO;EACP,iBAAiB;CACnB;CACA;EACE,KAAK;EACL,OAAO;EACP,MAAM;EACN,OAAO;CACT;CACA;EACE,KAAK;EACL,OAAO;EACP,MAAM;EACN,OAAO;CACT;CACA;EACE,KAAK;EACL,OAAO;EACP,MAAM;EACN,OAAO;CACT;CAGA;EACE,KAAK;EACL,OAAO;EACP,MAAM;EACN,OAAO;CACT;CAGA;EACE,KAAK;EACL,OAAO;EACP,MAAM;EACN,OAAO;CACT;CACA;EACE,KAAK;EACL,OAAO;EACP,MAAM;EACN,OAAO;CACT;CAGA;EACE,KAAK;EACL,OAAO;EACP,MAAM;EACN,OAAO;CACT;CACA;EACE,KAAK;EACL,OAAO;EACP,MAAM;EACN,OAAO;CACT;CACA;EACE,KAAK;EACL,OAAO;EACP,MAAM;EACN,OAAO;CACT;CAGA;EACE,KAAK;EACL,OAAO;EACP,MAAM;EACN,OAAO;CACT;CAOA;EACE,KAAK;EACL,OAAO;EACP,MAAM;EACN,OAAO;CACT;AACF;;;;;;;;;AAiBA,MAAa,cAAkC;CAC7C;EAAE,KAAK;EAAa,OAAO;EAAU,MAAM;CAAgC;CAC3E;EAAE,KAAK;EAAa,OAAO;EAAmB,MAAM;CAA8B;CAClF;EAAE,KAAK;EAAgB,OAAO;EAAsB,MAAM;CAAsC;CAChG;EAAE,KAAK;EAAc,OAAO;EAAe,MAAM;CAAuC;CACxF;EAAE,KAAK;EAAa,OAAO;EAAU,MAAM;CAA0B;AACvE;;;;;;;AAQA,MAAa,aAAiC;CAC5C;EACE,KAAK;EACL,OAAO;EACP,MAAM;CACR;CACA;EACE,KAAK;EACL,OAAO;EACP,MAAM;CACR;CACA;EACE,KAAK;EACL,OAAO;EACP,MAAM;CACR;AACF;;;;;AAMA,SAAgB,oBAAoB;CAClC,OAAO,SAAS,KAAI,aAAY;EAC9B,OAAO,QAAQ;EACf,OAAO,QAAQ;EACf,MAAM,GAAG,QAAQ,MAAM,KAAK,QAAQ;CACtC,EAAE;AACJ;;;;AAKA,SAAgB,wBAAkC;CAChD,OAAO,SAAS,QAAO,YAAW,QAAQ,eAAe,CAAC,CAAC,KAAI,YAAW,QAAQ,GAAG;AACvF;;;;AAKA,SAAgB,uBAAuB;CACrC,OAAO,YAAY,KAAI,cAAa;EAClC,OAAO,SAAS;EAChB,OAAO,SAAS;EAChB,MAAM,SAAS;CACjB,EAAE;AACJ;;;;;AAMA,SAAgB,sBAAsB;CACpC,OAAO,WAAW,KAAI,SAAQ;EAC5B,OAAO,IAAI;EACX,OAAO,IAAI;EACX,MAAM,IAAI;CACZ,EAAE;AACJ;;;;;;AAOA,SAAgB,oBAA8B;CAC5C,OAAO;EACL,GAAG,SAAS,KAAI,YAAW,QAAQ,GAAG;EACtC,GAAG,YAAY,KAAI,aAAY,SAAS,GAAG;EAC3C,GAAG,WAAW,KAAI,QAAO,IAAI,GAAG;CAClC;AACF"}
1
+ {"version":3,"file":"features-map.mjs","names":[],"sources":["../../../../../../create-warlock/src/features/features-map.ts"],"sourcesContent":["/**\r\n * Presentation manifest for the scaffolder's feature prompts.\r\n *\r\n * This file is **display metadata only** — keys, labels, hints, grouping, and\r\n * default selections. It deliberately holds NO dependency names or versions:\r\n * the single source of truth for what each feature installs is the `add`\r\n * command's feature map in `@warlock.js/core`\r\n * (`src/generations/add-command.action.ts`). The scaffolder collects the\r\n * selections here and delegates the actual install to `warlock add`, so the\r\n * two never drift on versions again.\r\n *\r\n * Every `key` below MUST exist in core's `allowedFeatures`. A CI guard should\r\n * assert that, but a subset assertion ALONE is not enough and has already let a\r\n * bug ship: `--features=web,tailwind` was rejected with \"Unknown feature(s):\r\n * tailwind\" because `tailwind` and `shadcn` existed in core and were missing\r\n * from THIS file — a direction a scaffolder ⊆ core check cannot see. The guard\r\n * has to run both ways:\r\n *\r\n * 1. scaffolder ⊆ core — catches a typo or a key core renamed.\r\n * 2. core ⊆ scaffolder + an explicit omission list — catches a feature core\r\n * gained that never became scaffoldable.\r\n *\r\n * The two lists are not equal and should not be forced to be. As of this\r\n * writing core holds 28 keys and this file offers 24; the other four are\r\n * deliberate and are the entire allow-list direction (2) needs:\r\n * `mongodb`, `postgres`, `mysql` (the database driver has its own select) and\r\n * `ai` (pulled automatically by every `ai-*` key's `requires`). Anything beyond\r\n * those four is a gap, not a decision, and should fail the build.\r\n */\r\n\r\nexport type FeatureGroup =\r\n | \"Auth & Access\"\r\n | \"Rendering & Mail\"\r\n | \"Media\"\r\n | \"Storage & Cache\"\r\n | \"Jobs & Messaging\"\r\n | \"Realtime\"\r\n | \"API Docs\"\r\n | \"Tooling\";\r\n\r\nexport type FeatureOption = {\r\n /** Must match a key in core's `add` feature map. */\r\n key: string;\r\n label: string;\r\n hint: string;\r\n group: FeatureGroup;\r\n defaultSelected?: boolean;\r\n};\r\n\r\n/**\r\n * Optional features offered in the general multiselect step. The database\r\n * driver (its own select) and AI providers + capability packages (their own\r\n * step) are intentionally NOT here — they have dedicated prompts.\r\n */\r\nexport const features: FeatureOption[] = [\r\n // Auth & Access\r\n {\r\n key: \"access\",\r\n label: \"Access (authorization)\",\r\n hint: \"RBAC permission checks, ABAC policies, and roles — pairs with auth\",\r\n group: \"Auth & Access\",\r\n },\r\n\r\n // Rendering & Mail\r\n {\r\n key: \"web\",\r\n label: \"Web (SSR React pages)\",\r\n hint: \"Server-rendered React pages on the same server as your API — scaffolds src/web and registers the connector\",\r\n group: \"Rendering & Mail\",\r\n },\r\n {\r\n key: \"tailwind\",\r\n label: \"Tailwind CSS\",\r\n hint: \"Tailwind v4 for the SSR page layer — creates src/web/app.css (the CSS-first config) and wires it through Vite (pulls web)\",\r\n group: \"Rendering & Mail\",\r\n },\r\n {\r\n key: \"shadcn\",\r\n label: \"shadcn/ui prerequisites\",\r\n hint: \"components.json, cn(), design tokens, and the web/* tsconfig path so `npx shadcn add <component>` works first time (pulls tailwind)\",\r\n group: \"Rendering & Mail\",\r\n },\r\n {\r\n key: \"react\",\r\n label: \"React (rendering & mails)\",\r\n hint: \"React + ReactDOM for non-interactive rendering and HTML/email generation\",\r\n group: \"Rendering & Mail\",\r\n defaultSelected: true,\r\n },\r\n {\r\n key: \"react-email\",\r\n label: \"React Email\",\r\n hint: \"Build email templates with React + Tailwind (pulls react + mail)\",\r\n group: \"Rendering & Mail\",\r\n },\r\n {\r\n key: \"mail\",\r\n label: \"Mail (Nodemailer)\",\r\n hint: \"Send emails via SMTP\",\r\n group: \"Rendering & Mail\",\r\n },\r\n {\r\n key: \"ses\",\r\n label: \"Amazon SES\",\r\n hint: \"Send emails via the AWS SES API\",\r\n group: \"Rendering & Mail\",\r\n },\r\n\r\n // Media\r\n {\r\n key: \"image\",\r\n label: \"Image processing (Sharp)\",\r\n hint: \"Resize, convert, and optimize images\",\r\n group: \"Media\",\r\n },\r\n\r\n // Storage & Cache\r\n {\r\n key: \"s3\",\r\n label: \"S3 storage\",\r\n hint: \"AWS S3 for cloud file storage\",\r\n group: \"Storage & Cache\",\r\n },\r\n {\r\n key: \"redis\",\r\n label: \"Redis cache\",\r\n hint: \"Redis driver for the cache layer\",\r\n group: \"Storage & Cache\",\r\n },\r\n\r\n // Jobs & Messaging\r\n {\r\n key: \"scheduler\",\r\n label: \"Scheduler\",\r\n hint: \"Background tasks and cron jobs\",\r\n group: \"Jobs & Messaging\",\r\n },\r\n {\r\n key: \"herald\",\r\n label: \"Herald (RabbitMQ)\",\r\n hint: \"Message broker for event-driven architecture\",\r\n group: \"Jobs & Messaging\",\r\n },\r\n {\r\n key: \"notifications\",\r\n label: \"Notifications\",\r\n hint: \"Multi-channel notifications — mail + in-app database, preferences, idempotency\",\r\n group: \"Jobs & Messaging\",\r\n },\r\n\r\n // Realtime\r\n {\r\n key: \"socket\",\r\n label: \"Socket.IO\",\r\n hint: \"Realtime websocket server\",\r\n group: \"Realtime\",\r\n },\r\n\r\n // API Docs (swagger / postman / openapi) — not shipped yet; they will arrive\r\n // together in the unified @warlock.js/api-docs package. Re-add the selection\r\n // once that package exists. (Removed so the wizard can't offer unbuilt features.)\r\n\r\n // Tooling\r\n {\r\n key: \"test\",\r\n label: \"Testing (Vitest)\",\r\n hint: \"Vitest + coverage + per-worker DB/cache test setup\",\r\n group: \"Tooling\",\r\n },\r\n];\r\n\r\nexport type AiProviderOption = {\r\n /** Must match a provider/package key in core's `add` feature map. */\r\n key: string;\r\n label: string;\r\n hint: string;\r\n};\r\n\r\n/**\r\n * AI providers offered in the dedicated AI step. Selecting any of these pulls\r\n * the core `@warlock.js/ai` package automatically via the provider's `requires`\r\n * in core's feature map — the scaffolder never lists `ai` as a standalone pick.\r\n *\r\n * Keys are `ai-`prefixed to mirror core's renamed provider feature keys\r\n * (`ai-openai` → `@warlock.js/ai-openai`, etc.).\r\n */\r\nexport const aiProviders: AiProviderOption[] = [\r\n { key: \"ai-openai\", label: \"OpenAI\", hint: \"GPT models via the OpenAI API\" },\r\n { key: \"ai-google\", label: \"Google (Gemini)\", hint: \"Gemini models via Google AI\" },\r\n { key: \"ai-anthropic\", label: \"Anthropic (Claude)\", hint: \"Claude models via the Anthropic API\" },\r\n { key: \"ai-bedrock\", label: \"AWS Bedrock\", hint: \"Foundation models via Amazon Bedrock\" },\r\n { key: \"ai-ollama\", label: \"Ollama\", hint: \"Local models via Ollama\" },\r\n];\r\n\r\n/**\r\n * AI capability packages offered alongside the providers in the dedicated AI\r\n * step. These are the satellite packages that sit on top of the core\r\n * `@warlock.js/ai` toolkit (each pulls the core `ai` package via its `requires`\r\n * in core's feature map, same as the providers).\r\n */\r\nexport const aiPackages: AiProviderOption[] = [\r\n {\r\n key: \"ai-tools\",\r\n label: \"AI Tools\",\r\n hint: \"Ready-made agent tools (web search, fetch, HTTP, calculator, date-time) + MCP client/server\",\r\n },\r\n {\r\n key: \"ai-panoptic\",\r\n label: \"AI Observability (Panoptic)\",\r\n hint: \"Tracing + exporters + a zero-setup local dashboard\",\r\n },\r\n {\r\n key: \"ai-workspace\",\r\n label: \"AI Workspace\",\r\n hint: \"Policy-jailed filesystem + shell for coding agents\",\r\n },\r\n];\r\n\r\n/**\r\n * Feature options for the multiselect prompt, ordered by group with the group\r\n * surfaced in the hint (keeps a flat list scannable without group widgets).\r\n */\r\nexport function getFeatureOptions() {\r\n return features.map(feature => ({\r\n value: feature.key,\r\n label: feature.label,\r\n hint: `${feature.group} — ${feature.hint}`,\r\n }));\r\n}\r\n\r\n/**\r\n * Keys pre-checked in the feature multiselect.\r\n */\r\nexport function getDefaultFeatureKeys(): string[] {\r\n return features.filter(feature => feature.defaultSelected).map(feature => feature.key);\r\n}\r\n\r\n/**\r\n * AI provider options for the dedicated AI multiselect step.\r\n */\r\nexport function getAiProviderOptions() {\r\n return aiProviders.map(provider => ({\r\n value: provider.key,\r\n label: provider.label,\r\n hint: provider.hint,\r\n }));\r\n}\r\n\r\n/**\r\n * AI capability package options for the dedicated AI multiselect step. Offered\r\n * together with the providers (see `getAiProviderOptions`).\r\n */\r\nexport function getAiPackageOptions() {\r\n return aiPackages.map(pkg => ({\r\n value: pkg.key,\r\n label: pkg.label,\r\n hint: pkg.hint,\r\n }));\r\n}\r\n\r\n/**\r\n * Every selectable key the scaffolder knows about (features + AI providers +\r\n * AI capability packages). Used to validate `--features` / `--ai` flags in\r\n * non-interactive mode.\r\n */\r\nexport function getAllFeatureKeys(): string[] {\r\n return [\r\n ...features.map(feature => feature.key),\r\n ...aiProviders.map(provider => provider.key),\r\n ...aiPackages.map(pkg => pkg.key),\r\n ];\r\n}\r\n"],"mappings":";;;;;;AAsDA,MAAa,WAA4B;CAEvC;EACE,KAAK;EACL,OAAO;EACP,MAAM;EACN,OAAO;CACT;CAGA;EACE,KAAK;EACL,OAAO;EACP,MAAM;EACN,OAAO;CACT;CACA;EACE,KAAK;EACL,OAAO;EACP,MAAM;EACN,OAAO;CACT;CACA;EACE,KAAK;EACL,OAAO;EACP,MAAM;EACN,OAAO;CACT;CACA;EACE,KAAK;EACL,OAAO;EACP,MAAM;EACN,OAAO;EACP,iBAAiB;CACnB;CACA;EACE,KAAK;EACL,OAAO;EACP,MAAM;EACN,OAAO;CACT;CACA;EACE,KAAK;EACL,OAAO;EACP,MAAM;EACN,OAAO;CACT;CACA;EACE,KAAK;EACL,OAAO;EACP,MAAM;EACN,OAAO;CACT;CAGA;EACE,KAAK;EACL,OAAO;EACP,MAAM;EACN,OAAO;CACT;CAGA;EACE,KAAK;EACL,OAAO;EACP,MAAM;EACN,OAAO;CACT;CACA;EACE,KAAK;EACL,OAAO;EACP,MAAM;EACN,OAAO;CACT;CAGA;EACE,KAAK;EACL,OAAO;EACP,MAAM;EACN,OAAO;CACT;CACA;EACE,KAAK;EACL,OAAO;EACP,MAAM;EACN,OAAO;CACT;CACA;EACE,KAAK;EACL,OAAO;EACP,MAAM;EACN,OAAO;CACT;CAGA;EACE,KAAK;EACL,OAAO;EACP,MAAM;EACN,OAAO;CACT;CAOA;EACE,KAAK;EACL,OAAO;EACP,MAAM;EACN,OAAO;CACT;AACF;;;;;;;;;AAiBA,MAAa,cAAkC;CAC7C;EAAE,KAAK;EAAa,OAAO;EAAU,MAAM;CAAgC;CAC3E;EAAE,KAAK;EAAa,OAAO;EAAmB,MAAM;CAA8B;CAClF;EAAE,KAAK;EAAgB,OAAO;EAAsB,MAAM;CAAsC;CAChG;EAAE,KAAK;EAAc,OAAO;EAAe,MAAM;CAAuC;CACxF;EAAE,KAAK;EAAa,OAAO;EAAU,MAAM;CAA0B;AACvE;;;;;;;AAQA,MAAa,aAAiC;CAC5C;EACE,KAAK;EACL,OAAO;EACP,MAAM;CACR;CACA;EACE,KAAK;EACL,OAAO;EACP,MAAM;CACR;CACA;EACE,KAAK;EACL,OAAO;EACP,MAAM;CACR;AACF;;;;;AAMA,SAAgB,oBAAoB;CAClC,OAAO,SAAS,KAAI,aAAY;EAC9B,OAAO,QAAQ;EACf,OAAO,QAAQ;EACf,MAAM,GAAG,QAAQ,MAAM,KAAK,QAAQ;CACtC,EAAE;AACJ;;;;AAKA,SAAgB,wBAAkC;CAChD,OAAO,SAAS,QAAO,YAAW,QAAQ,eAAe,CAAC,CAAC,KAAI,YAAW,QAAQ,GAAG;AACvF;;;;AAKA,SAAgB,uBAAuB;CACrC,OAAO,YAAY,KAAI,cAAa;EAClC,OAAO,SAAS;EAChB,OAAO,SAAS;EAChB,MAAM,SAAS;CACjB,EAAE;AACJ;;;;;AAMA,SAAgB,sBAAsB;CACpC,OAAO,WAAW,KAAI,SAAQ;EAC5B,OAAO,IAAI;EACX,OAAO,IAAI;EACX,MAAM,IAAI;CACZ,EAAE;AACJ;;;;;;AAOA,SAAgB,oBAA8B;CAC5C,OAAO;EACL,GAAG,SAAS,KAAI,YAAW,QAAQ,GAAG;EACtC,GAAG,YAAY,KAAI,aAAY,SAAS,GAAG;EAC3C,GAAG,WAAW,KAAI,QAAO,IAAI,GAAG;CAClC;AACF"}
@@ -66,10 +66,18 @@ var App = class {
66
66
  * so an unverified pin resolves to nothing and the install dies with ETARGET.
67
67
  */
68
68
  updatePackageJson(versions = {}) {
69
- const pkg = this.package.replace("name", this.name.replaceAll("/", "-")).replaceAll("yarn", getPackageManager());
69
+ const packageManager = getPackageManager();
70
+ const pkg = this.package.replace("name", this.name.replaceAll("/", "-"));
71
+ const content = pkg.content;
72
+ if (typeof content.scripts?.serve === "string") content.scripts.serve = content.scripts.serve.replaceAll("yarn", packageManager);
73
+ const hooks = content.huskier?.hooks;
74
+ if (hooks) for (const hook of Object.keys(hooks)) {
75
+ const commands = hooks[hook];
76
+ if (!Array.isArray(commands)) continue;
77
+ hooks[hook] = commands.map((command) => typeof command === "string" ? command.replaceAll("yarn", packageManager) : command);
78
+ }
70
79
  const warlockVersion = getJsonFile(packageRoot("package.json")).version;
71
80
  const defaultRange = fallbackRange(warlockVersion);
72
- const content = pkg.content;
73
81
  for (const field of ["dependencies", "devDependencies"]) {
74
82
  const deps = content[field];
75
83
  if (!deps) continue;
@@ -1 +1 @@
1
- {"version":3,"file":"app.mjs","names":[],"sources":["../../../../../../create-warlock/src/helpers/app.ts"],"sourcesContent":["import {\r\n copyDirectory,\r\n copyFile,\r\n fileExists,\r\n getFile,\r\n getJsonFile,\r\n putFile,\r\n putJsonFile,\r\n renameFile,\r\n} from \"@warlock.js/fs\";\r\nimport { unlinkSync } from \"node:fs\";\r\nimport path from \"path\";\r\nimport { AppOptions, Application } from \"../commands/create-new-app/types\";\r\nimport { getDatabaseDriver } from \"../features/database-drivers\";\r\nimport { executeCommand, runCommand } from \"./exec\";\r\nimport { getPackageManager } from \"./package-manager\";\r\nimport { packageRoot, Template, template } from \"./paths\";\r\nimport { fallbackRange } from \"./warlock-versions\";\r\n\r\n/**\r\n * Environment for every install the scaffolder spawns.\r\n *\r\n * A shell that exports `NODE_ENV=production` makes the package managers skip\r\n * every devDependency — and still exit 0. The scaffolded project needs its dev\r\n * toolchain (typescript, vitest, eslint) to be usable at all, so the install\r\n * child is pinned to `development` regardless of the ambient environment. A\r\n * silently incomplete install that reports success is the exact failure mode\r\n * this file exists to prevent.\r\n */\r\nexport function installEnvironment(): NodeJS.ProcessEnv {\r\n return { NODE_ENV: \"development\" };\r\n}\r\n\r\nexport class App {\r\n /**\r\n * Resolved files\r\n */\r\n protected files: Record<string, FileManager> = {};\r\n\r\n /**\r\n * Resolved JSON files\r\n */\r\n protected jsonFiles: Record<string, JsonFileManager> = {};\r\n\r\n public isInstalled = false;\r\n\r\n public constructor(protected app: Application) {}\r\n\r\n public get options(): AppOptions {\r\n return this.app.options;\r\n }\r\n\r\n public use(templateName: Template) {\r\n copyDirectory(template(templateName), this.path);\r\n\r\n if (fileExists(this.path + \"/.env.example\")) {\r\n copyFile(this.path + \"/.env.example\", this.path + \"/.env\");\r\n }\r\n\r\n renameFile(this.path + \"/_.gitignore\", this.path + \"/.gitignore\");\r\n\r\n return this;\r\n }\r\n\r\n public init() {\r\n return this;\r\n }\r\n\r\n public terminate() {\r\n // No longer using outro, using showSuccessScreen instead\r\n }\r\n\r\n public install() {\r\n return runCommand(getPackageManager(), [\"install\"], this.path, {\r\n env: installEnvironment(),\r\n });\r\n }\r\n\r\n public async exec(command: string) {\r\n const [commandName, ...optionsList] = command.split(\" \");\r\n return await executeCommand(commandName, optionsList, this.path);\r\n }\r\n\r\n public async git() {\r\n const { initializeGitRepository } = await import(\r\n \"./project-builder-helpers\"\r\n );\r\n return await initializeGitRepository(this.path);\r\n }\r\n\r\n /**\r\n * Write the project's `package.json`: the project name, the chosen package\r\n * manager, and the version of every `@warlock.js/*` dependency.\r\n *\r\n * `versions` comes from {@link resolveWarlockVersions} — versions the\r\n * registry has confirmed exist. It is optional because the fluent chain is\r\n * synchronous; without it every sibling gets the caret range floored to the\r\n * scaffolder's major, which is always satisfiable by a published release.\r\n *\r\n * What it must NEVER do again is stamp the scaffolder's own version blind:\r\n * the release tooling bumps that version on every build, published or not,\r\n * so an unverified pin resolves to nothing and the install dies with ETARGET.\r\n */\r\n public updatePackageJson(versions: Record<string, string> = {}) {\r\n const pkg = this.package\r\n .replace(\"name\", this.name.replaceAll(\"/\", \"-\"))\r\n .replaceAll(\"yarn\", getPackageManager());\r\n\r\n const warlockVersion: string = (\r\n getJsonFile(packageRoot(\"package.json\")) as { version: string }\r\n ).version;\r\n const defaultRange = fallbackRange(warlockVersion);\r\n const content: any = pkg.content;\r\n\r\n for (const field of [\"dependencies\", \"devDependencies\"] as const) {\r\n const deps = content[field] as Record<string, string> | undefined;\r\n if (!deps) continue;\r\n\r\n for (const dependency of Object.keys(deps)) {\r\n if (dependency.startsWith(\"@warlock.js/\")) {\r\n deps[dependency] = versions[dependency] ?? defaultRange;\r\n }\r\n }\r\n }\r\n\r\n pkg.save();\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Configure the chosen database driver: wire `DB_DRIVER` / `DB_PORT` into\r\n * `.env`, AND pin the driver's npm package (`mongodb` / `pg`) into the\r\n * project's `package.json` dependencies.\r\n *\r\n * The dependency is written HERE — before the base `yarn install` — so the\r\n * driver is pulled deterministically by the very first install. We do NOT\r\n * rely on the post-copy `warlock add <driver> --no-install` + separate\r\n * batched install, which can be skipped or fail and leave the driver\r\n * missing (the \"mongodb package is not installed\" runtime error).\r\n */\r\n public configureDatabaseEnv(driverValue: string) {\r\n const driver = getDatabaseDriver(driverValue);\r\n\r\n if (!driver) return this;\r\n\r\n // Pin the driver package into dependencies (idempotent — never downgrade).\r\n const packageJsonPath = path.resolve(this.path, \"package.json\");\r\n const packageJson = getJsonFile(packageJsonPath) as {\r\n dependencies?: Record<string, string>;\r\n };\r\n packageJson.dependencies = packageJson.dependencies ?? {};\r\n if (!packageJson.dependencies[driver.package]) {\r\n packageJson.dependencies[driver.package] = driver.packageVersion;\r\n putJsonFile(packageJsonPath, packageJson);\r\n }\r\n\r\n let envContent = getFile(this.path + \"/.env\") as string;\r\n\r\n envContent = envContent.replace(/DB_PORT=\\d+/, `DB_PORT=${driver.defaultPort}`);\r\n\r\n if (envContent.includes(\"DB_DRIVER=\")) {\r\n envContent = envContent.replace(/DB_DRIVER=\\w*/, `DB_DRIVER=${driver.value}`);\r\n } else {\r\n envContent = envContent.replace(\r\n /DB_PORT=\\d+/,\r\n `DB_PORT=${driver.defaultPort}\\nDB_DRIVER=${driver.value}`,\r\n );\r\n }\r\n\r\n putFile(this.path + \"/.env\", envContent);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Remove the database layer for a \"no database\" scaffold.\r\n *\r\n * Deletes `src/config/database.ts` (and a `.tsx` variant if present) from the\r\n * freshly-copied template. The framework's database connector is config-gated\r\n * on that file — with it gone, `config.get(\"database\")` is undefined and the\r\n * connector no-ops, so the app boots with no database wired and no driver\r\n * package pulled. The `DB_*` lines in `.env` are left in place (harmless: no\r\n * config reads them) as a ready template for adding a database back later.\r\n */\r\n public removeDatabaseConfig() {\r\n const configDir = path.resolve(this.path, \"src/config\");\r\n\r\n for (const fileName of [\"database.ts\", \"database.tsx\"]) {\r\n const filePath = path.resolve(configDir, fileName);\r\n\r\n if (fileExists(filePath)) {\r\n unlinkSync(filePath);\r\n }\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Pick the home page implementation based on whether React was selected.\r\n *\r\n * The template ships BOTH a plain JSON controller (`home-page.controller.ts`)\r\n * and a React-rendered page (`home-page.controller.tsx` + `HomePageComponent.tsx`).\r\n * Exactly one survives the scaffold: React projects keep the `.tsx` page (its\r\n * `react`/`react-dom` deps come from the `react` feature), every other project\r\n * keeps the dependency-free JSON controller — so a fresh project never imports\r\n * `react` unless it asked for it.\r\n */\r\n public configureHomePage(useReact: boolean) {\r\n const controllers = this.path + \"/src/app/shared/controllers\";\r\n const components = this.path + \"/src/app/shared/components\";\r\n\r\n const remove = (file: string) => {\r\n if (fileExists(file)) unlinkSync(file);\r\n };\r\n\r\n if (useReact) {\r\n remove(controllers + \"/home-page.controller.ts\");\r\n } else {\r\n remove(controllers + \"/home-page.controller.tsx\");\r\n remove(components + \"/HomePageComponent.tsx\");\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Install the selected optional features by delegating to the project's own\r\n * `warlock add`. `--no-install` records every dependency in package.json and\r\n * ejects configs / scripts / setup hooks WITHOUT installing — the caller runs\r\n * one batched install afterwards. Versions come from core's feature map, so\r\n * the scaffolder never duplicates them.\r\n *\r\n * `--no-install` is passed LAST on purpose: the CLI parser treats the\r\n * positional after a bare flag as that flag's value, so it must follow the\r\n * feature list, not precede it.\r\n */\r\n public async installFeatures(features: string[]) {\r\n if (features.length === 0) return true;\r\n\r\n return this.exec(`npx warlock add ${features.join(\" \")} --no-install`);\r\n }\r\n\r\n /**\r\n * Force a single copy of `vite` when the project ends up with BOTH `vite`\r\n * (written by the `web` feature) and `vitest` (always in the template).\r\n *\r\n * `vitest` depends on vite through a wide range of its own\r\n * (`^6 || ^7 || ^8`), which resolves to a DIFFERENT major than the range the\r\n * feature map pins. Yarn 1 then has to nest the second copy under\r\n * `vitest/node_modules` and dies on its own linker invariant:\r\n *\r\n * error Invariant Violation: could not find a copy of vite to link in\r\n * <project>/node_modules/vitest/node_modules\r\n *\r\n * It aborts BEFORE `node_modules/.bin` is written, so `warlock` is never\r\n * linked and the very first `yarn dev` fails with \"'warlock' is not\r\n * recognized\". npm and pnpm nest happily and never hit this — for them the\r\n * `overrides` twin below is one copy instead of two, not a crash fix.\r\n *\r\n * The pin is READ BACK from whatever range the project's own package.json\r\n * declares, never re-stated as a literal here: the feature map in\r\n * `@warlock.js/core` owns the version, and a second copy of it would drift\r\n * the moment that one moves.\r\n *\r\n * Returns whether a pin was written, so the caller (and its spec) can assert\r\n * it instead of assuming it.\r\n */\r\n public pinViteResolution() {\r\n const packageJsonPath = path.resolve(this.path, \"package.json\");\r\n\r\n if (!fileExists(packageJsonPath)) return false;\r\n\r\n const packageJson = getJsonFile(packageJsonPath) as {\r\n dependencies?: Record<string, string>;\r\n devDependencies?: Record<string, string>;\r\n resolutions?: Record<string, string>;\r\n overrides?: Record<string, string>;\r\n };\r\n\r\n const declaredRange = (name: string) =>\r\n packageJson.dependencies?.[name] ?? packageJson.devDependencies?.[name];\r\n\r\n const viteRange = declaredRange(\"vite\");\r\n\r\n // No vite, or no vitest to conflict with it — nothing to pin. Writing a\r\n // resolution for a package the project does not depend on would only\r\n // freeze a transitive tree nobody asked us to freeze.\r\n if (!viteRange || !declaredRange(\"vitest\")) return false;\r\n\r\n // yarn 1 / yarn berry read `resolutions`; npm 8+ and pnpm read `overrides`.\r\n packageJson.resolutions = { ...packageJson.resolutions, vite: viteRange };\r\n packageJson.overrides = { ...packageJson.overrides, vite: viteRange };\r\n\r\n putJsonFile(packageJsonPath, packageJson);\r\n\r\n return true;\r\n }\r\n\r\n /**\r\n * Get package json file\r\n */\r\n public get package() {\r\n return this.json(\"package.json\");\r\n }\r\n\r\n public updateDotEnv() {\r\n this.file(\".env\").replaceAll(\"appName\", this.name).save();\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Get env file to update\r\n */\r\n public get env() {\r\n return this.file(\".env\");\r\n }\r\n\r\n public get name() {\r\n return this.app.appName;\r\n }\r\n\r\n public get path() {\r\n return this.app.appPath;\r\n }\r\n\r\n public file(relativePath: string) {\r\n const fullPath = path.resolve(this.path, relativePath);\r\n\r\n if (!this.files[fullPath]) {\r\n this.files[fullPath] = file(fullPath);\r\n }\r\n\r\n return this.files[fullPath];\r\n }\r\n\r\n public json(relativePath: string): JsonFileManager {\r\n const fullPath = path.resolve(this.path, relativePath);\r\n\r\n if (!this.jsonFiles[fullPath]) {\r\n this.jsonFiles[fullPath] = jsonFile(fullPath);\r\n }\r\n\r\n return this.jsonFiles[fullPath];\r\n }\r\n}\r\n\r\nexport function app(app: Application) {\r\n return new App(app);\r\n}\r\n\r\nexport class FileManager {\r\n public content!: string;\r\n public constructor(protected filePath: string) {\r\n this.parseContent();\r\n }\r\n\r\n protected parseContent() {\r\n this.content = getFile(this.filePath) as string;\r\n }\r\n\r\n public replace(search: string, replace: string) {\r\n this.content = this.content.replace(search, replace);\r\n\r\n return this;\r\n }\r\n\r\n public replaceAll(search: string, replace: string) {\r\n this.content = this.content.replaceAll(search, replace);\r\n\r\n return this;\r\n }\r\n\r\n public save() {\r\n putFile(this.filePath, this.content);\r\n }\r\n}\r\n\r\nexport class JsonFileManager extends FileManager {\r\n protected parseContent() {\r\n this.content = getJsonFile(this.filePath);\r\n }\r\n\r\n public save() {\r\n putJsonFile(this.filePath, this.content);\r\n }\r\n\r\n public has(key: string) {\r\n return this.content[key] !== undefined;\r\n }\r\n\r\n public replace(key: string, value: any) {\r\n this.content[key] = value;\r\n\r\n return this;\r\n }\r\n\r\n public replaceAll(key: string, value: any) {\r\n const contentAsString = JSON.stringify(this.content);\r\n\r\n this.content = JSON.parse(contentAsString.replaceAll(key, value));\r\n\r\n return this;\r\n }\r\n}\r\n\r\nexport function file(path: string) {\r\n return new FileManager(path);\r\n}\r\n\r\nexport function jsonFile(path: string) {\r\n return new JsonFileManager(path);\r\n}\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"app.mjs","names":[],"sources":["../../../../../../create-warlock/src/helpers/app.ts"],"sourcesContent":["import {\r\n copyDirectory,\r\n copyFile,\r\n fileExists,\r\n getFile,\r\n getJsonFile,\r\n putFile,\r\n putJsonFile,\r\n renameFile,\r\n} from \"@warlock.js/fs\";\r\nimport { unlinkSync } from \"node:fs\";\r\nimport path from \"path\";\r\nimport { AppOptions, Application } from \"../commands/create-new-app/types\";\r\nimport { getDatabaseDriver } from \"../features/database-drivers\";\r\nimport { executeCommand, runCommand } from \"./exec\";\r\nimport { getPackageManager } from \"./package-manager\";\r\nimport { packageRoot, Template, template } from \"./paths\";\r\nimport { fallbackRange } from \"./warlock-versions\";\r\n\r\n/**\r\n * Environment for every install the scaffolder spawns.\r\n *\r\n * A shell that exports `NODE_ENV=production` makes the package managers skip\r\n * every devDependency — and still exit 0. The scaffolded project needs its dev\r\n * toolchain (typescript, vitest, eslint) to be usable at all, so the install\r\n * child is pinned to `development` regardless of the ambient environment. A\r\n * silently incomplete install that reports success is the exact failure mode\r\n * this file exists to prevent.\r\n */\r\nexport function installEnvironment(): NodeJS.ProcessEnv {\r\n return { NODE_ENV: \"development\" };\r\n}\r\n\r\nexport class App {\r\n /**\r\n * Resolved files\r\n */\r\n protected files: Record<string, FileManager> = {};\r\n\r\n /**\r\n * Resolved JSON files\r\n */\r\n protected jsonFiles: Record<string, JsonFileManager> = {};\r\n\r\n public isInstalled = false;\r\n\r\n public constructor(protected app: Application) {}\r\n\r\n public get options(): AppOptions {\r\n return this.app.options;\r\n }\r\n\r\n public use(templateName: Template) {\r\n copyDirectory(template(templateName), this.path);\r\n\r\n if (fileExists(this.path + \"/.env.example\")) {\r\n copyFile(this.path + \"/.env.example\", this.path + \"/.env\");\r\n }\r\n\r\n renameFile(this.path + \"/_.gitignore\", this.path + \"/.gitignore\");\r\n\r\n return this;\r\n }\r\n\r\n public init() {\r\n return this;\r\n }\r\n\r\n public terminate() {\r\n // No longer using outro, using showSuccessScreen instead\r\n }\r\n\r\n public install() {\r\n return runCommand(getPackageManager(), [\"install\"], this.path, {\r\n env: installEnvironment(),\r\n });\r\n }\r\n\r\n public async exec(command: string) {\r\n const [commandName, ...optionsList] = command.split(\" \");\r\n return await executeCommand(commandName, optionsList, this.path);\r\n }\r\n\r\n public async git() {\r\n const { initializeGitRepository } = await import(\r\n \"./project-builder-helpers\"\r\n );\r\n return await initializeGitRepository(this.path);\r\n }\r\n\r\n /**\r\n * Write the project's `package.json`: the project name, the chosen package\r\n * manager, and the version of every `@warlock.js/*` dependency.\r\n *\r\n * `versions` comes from {@link resolveWarlockVersions} — versions the\r\n * registry has confirmed exist. It is optional because the fluent chain is\r\n * synchronous; without it every sibling gets the caret range floored to the\r\n * scaffolder's major, which is always satisfiable by a published release.\r\n *\r\n * What it must NEVER do again is stamp the scaffolder's own version blind:\r\n * the release tooling bumps that version on every build, published or not,\r\n * so an unverified pin resolves to nothing and the install dies with ETARGET.\r\n */\r\n public updatePackageJson(versions: Record<string, string> = {}) {\r\n const packageManager = getPackageManager();\r\n\r\n const pkg = this.package.replace(\"name\", this.name.replaceAll(\"/\", \"-\"));\r\n\r\n const content: any = pkg.content;\r\n\r\n // Substitute the chosen package manager ONLY into the fields the template\r\n // writes it into literally — the `serve` script and the huskier hooks — by\r\n // path, never with a blanket substring replace over the serialized JSON. A\r\n // raw `replaceAll(\"yarn\", pm)` rewrites every occurrence of the substring\r\n // \"yarn\" anywhere in the document, so a project named `my-yarn-app`, or any\r\n // dependency/path containing \"yarn\", is silently corrupted (`--pm=npm`\r\n // turned `my-yarn-app` into `my-npm-app`). Field-scoped rewriting is also\r\n // order-independent: the name lives in `content.name` and these tokens live\r\n // in `scripts.serve` / `huskier.hooks`, disjoint fields that cannot collide\r\n // with the name substitution above regardless of which runs first.\r\n if (typeof content.scripts?.serve === \"string\") {\r\n content.scripts.serve = content.scripts.serve.replaceAll(\r\n \"yarn\",\r\n packageManager,\r\n );\r\n }\r\n\r\n const hooks = content.huskier?.hooks as\r\n | Record<string, unknown>\r\n | undefined;\r\n\r\n if (hooks) {\r\n for (const hook of Object.keys(hooks)) {\r\n const commands = hooks[hook];\r\n\r\n if (!Array.isArray(commands)) continue;\r\n\r\n hooks[hook] = commands.map(command =>\r\n typeof command === \"string\"\r\n ? command.replaceAll(\"yarn\", packageManager)\r\n : command,\r\n );\r\n }\r\n }\r\n\r\n const warlockVersion: string = (\r\n getJsonFile(packageRoot(\"package.json\")) as { version: string }\r\n ).version;\r\n const defaultRange = fallbackRange(warlockVersion);\r\n\r\n for (const field of [\"dependencies\", \"devDependencies\"] as const) {\r\n const deps = content[field] as Record<string, string> | undefined;\r\n if (!deps) continue;\r\n\r\n for (const dependency of Object.keys(deps)) {\r\n if (dependency.startsWith(\"@warlock.js/\")) {\r\n deps[dependency] = versions[dependency] ?? defaultRange;\r\n }\r\n }\r\n }\r\n\r\n pkg.save();\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Configure the chosen database driver: wire `DB_DRIVER` / `DB_PORT` into\r\n * `.env`, AND pin the driver's npm package (`mongodb` / `pg`) into the\r\n * project's `package.json` dependencies.\r\n *\r\n * The dependency is written HERE — before the base `yarn install` — so the\r\n * driver is pulled deterministically by the very first install. We do NOT\r\n * rely on the post-copy `warlock add <driver> --no-install` + separate\r\n * batched install, which can be skipped or fail and leave the driver\r\n * missing (the \"mongodb package is not installed\" runtime error).\r\n */\r\n public configureDatabaseEnv(driverValue: string) {\r\n const driver = getDatabaseDriver(driverValue);\r\n\r\n if (!driver) return this;\r\n\r\n // Pin the driver package into dependencies (idempotent — never downgrade).\r\n const packageJsonPath = path.resolve(this.path, \"package.json\");\r\n const packageJson = getJsonFile(packageJsonPath) as {\r\n dependencies?: Record<string, string>;\r\n };\r\n packageJson.dependencies = packageJson.dependencies ?? {};\r\n if (!packageJson.dependencies[driver.package]) {\r\n packageJson.dependencies[driver.package] = driver.packageVersion;\r\n putJsonFile(packageJsonPath, packageJson);\r\n }\r\n\r\n let envContent = getFile(this.path + \"/.env\") as string;\r\n\r\n envContent = envContent.replace(/DB_PORT=\\d+/, `DB_PORT=${driver.defaultPort}`);\r\n\r\n if (envContent.includes(\"DB_DRIVER=\")) {\r\n envContent = envContent.replace(/DB_DRIVER=\\w*/, `DB_DRIVER=${driver.value}`);\r\n } else {\r\n envContent = envContent.replace(\r\n /DB_PORT=\\d+/,\r\n `DB_PORT=${driver.defaultPort}\\nDB_DRIVER=${driver.value}`,\r\n );\r\n }\r\n\r\n putFile(this.path + \"/.env\", envContent);\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Remove the database layer for a \"no database\" scaffold.\r\n *\r\n * Deletes `src/config/database.ts` (and a `.tsx` variant if present) from the\r\n * freshly-copied template. The framework's database connector is config-gated\r\n * on that file — with it gone, `config.get(\"database\")` is undefined and the\r\n * connector no-ops, so the app boots with no database wired and no driver\r\n * package pulled. The `DB_*` lines in `.env` are left in place (harmless: no\r\n * config reads them) as a ready template for adding a database back later.\r\n */\r\n public removeDatabaseConfig() {\r\n const configDir = path.resolve(this.path, \"src/config\");\r\n\r\n for (const fileName of [\"database.ts\", \"database.tsx\"]) {\r\n const filePath = path.resolve(configDir, fileName);\r\n\r\n if (fileExists(filePath)) {\r\n unlinkSync(filePath);\r\n }\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Pick the home page implementation based on whether React was selected.\r\n *\r\n * The template ships BOTH a plain JSON controller (`home-page.controller.ts`)\r\n * and a React-rendered page (`home-page.controller.tsx` + `HomePageComponent.tsx`).\r\n * Exactly one survives the scaffold: React projects keep the `.tsx` page (its\r\n * `react`/`react-dom` deps come from the `react` feature), every other project\r\n * keeps the dependency-free JSON controller — so a fresh project never imports\r\n * `react` unless it asked for it.\r\n */\r\n public configureHomePage(useReact: boolean) {\r\n const controllers = this.path + \"/src/app/shared/controllers\";\r\n const components = this.path + \"/src/app/shared/components\";\r\n\r\n const remove = (file: string) => {\r\n if (fileExists(file)) unlinkSync(file);\r\n };\r\n\r\n if (useReact) {\r\n remove(controllers + \"/home-page.controller.ts\");\r\n } else {\r\n remove(controllers + \"/home-page.controller.tsx\");\r\n remove(components + \"/HomePageComponent.tsx\");\r\n }\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Install the selected optional features by delegating to the project's own\r\n * `warlock add`. `--no-install` records every dependency in package.json and\r\n * ejects configs / scripts / setup hooks WITHOUT installing — the caller runs\r\n * one batched install afterwards. Versions come from core's feature map, so\r\n * the scaffolder never duplicates them.\r\n *\r\n * `--no-install` is passed LAST on purpose: the CLI parser treats the\r\n * positional after a bare flag as that flag's value, so it must follow the\r\n * feature list, not precede it.\r\n */\r\n public async installFeatures(features: string[]) {\r\n if (features.length === 0) return true;\r\n\r\n return this.exec(`npx warlock add ${features.join(\" \")} --no-install`);\r\n }\r\n\r\n /**\r\n * Force a single copy of `vite` when the project ends up with BOTH `vite`\r\n * (written by the `web` feature) and `vitest` (always in the template).\r\n *\r\n * `vitest` depends on vite through a wide range of its own\r\n * (`^6 || ^7 || ^8`), which resolves to a DIFFERENT major than the range the\r\n * feature map pins. Yarn 1 then has to nest the second copy under\r\n * `vitest/node_modules` and dies on its own linker invariant:\r\n *\r\n * error Invariant Violation: could not find a copy of vite to link in\r\n * <project>/node_modules/vitest/node_modules\r\n *\r\n * It aborts BEFORE `node_modules/.bin` is written, so `warlock` is never\r\n * linked and the very first `yarn dev` fails with \"'warlock' is not\r\n * recognized\". npm and pnpm nest happily and never hit this — for them the\r\n * `overrides` twin below is one copy instead of two, not a crash fix.\r\n *\r\n * The pin is READ BACK from whatever range the project's own package.json\r\n * declares, never re-stated as a literal here: the feature map in\r\n * `@warlock.js/core` owns the version, and a second copy of it would drift\r\n * the moment that one moves.\r\n *\r\n * Returns whether a pin was written, so the caller (and its spec) can assert\r\n * it instead of assuming it.\r\n */\r\n public pinViteResolution() {\r\n const packageJsonPath = path.resolve(this.path, \"package.json\");\r\n\r\n if (!fileExists(packageJsonPath)) return false;\r\n\r\n const packageJson = getJsonFile(packageJsonPath) as {\r\n dependencies?: Record<string, string>;\r\n devDependencies?: Record<string, string>;\r\n resolutions?: Record<string, string>;\r\n overrides?: Record<string, string>;\r\n };\r\n\r\n const declaredRange = (name: string) =>\r\n packageJson.dependencies?.[name] ?? packageJson.devDependencies?.[name];\r\n\r\n const viteRange = declaredRange(\"vite\");\r\n\r\n // No vite, or no vitest to conflict with it — nothing to pin. Writing a\r\n // resolution for a package the project does not depend on would only\r\n // freeze a transitive tree nobody asked us to freeze.\r\n if (!viteRange || !declaredRange(\"vitest\")) return false;\r\n\r\n // yarn 1 / yarn berry read `resolutions`; npm 8+ and pnpm read `overrides`.\r\n packageJson.resolutions = { ...packageJson.resolutions, vite: viteRange };\r\n packageJson.overrides = { ...packageJson.overrides, vite: viteRange };\r\n\r\n putJsonFile(packageJsonPath, packageJson);\r\n\r\n return true;\r\n }\r\n\r\n /**\r\n * Get package json file\r\n */\r\n public get package() {\r\n return this.json(\"package.json\");\r\n }\r\n\r\n public updateDotEnv() {\r\n this.file(\".env\").replaceAll(\"appName\", this.name).save();\r\n\r\n return this;\r\n }\r\n\r\n /**\r\n * Get env file to update\r\n */\r\n public get env() {\r\n return this.file(\".env\");\r\n }\r\n\r\n public get name() {\r\n return this.app.appName;\r\n }\r\n\r\n public get path() {\r\n return this.app.appPath;\r\n }\r\n\r\n public file(relativePath: string) {\r\n const fullPath = path.resolve(this.path, relativePath);\r\n\r\n if (!this.files[fullPath]) {\r\n this.files[fullPath] = file(fullPath);\r\n }\r\n\r\n return this.files[fullPath];\r\n }\r\n\r\n public json(relativePath: string): JsonFileManager {\r\n const fullPath = path.resolve(this.path, relativePath);\r\n\r\n if (!this.jsonFiles[fullPath]) {\r\n this.jsonFiles[fullPath] = jsonFile(fullPath);\r\n }\r\n\r\n return this.jsonFiles[fullPath];\r\n }\r\n}\r\n\r\nexport function app(app: Application) {\r\n return new App(app);\r\n}\r\n\r\nexport class FileManager {\r\n public content!: string;\r\n public constructor(protected filePath: string) {\r\n this.parseContent();\r\n }\r\n\r\n protected parseContent() {\r\n this.content = getFile(this.filePath) as string;\r\n }\r\n\r\n public replace(search: string, replace: string) {\r\n this.content = this.content.replace(search, replace);\r\n\r\n return this;\r\n }\r\n\r\n public replaceAll(search: string, replace: string) {\r\n this.content = this.content.replaceAll(search, replace);\r\n\r\n return this;\r\n }\r\n\r\n public save() {\r\n putFile(this.filePath, this.content);\r\n }\r\n}\r\n\r\nexport class JsonFileManager extends FileManager {\r\n protected parseContent() {\r\n this.content = getJsonFile(this.filePath);\r\n }\r\n\r\n public save() {\r\n putJsonFile(this.filePath, this.content);\r\n }\r\n\r\n public has(key: string) {\r\n return this.content[key] !== undefined;\r\n }\r\n\r\n public replace(key: string, value: any) {\r\n this.content[key] = value;\r\n\r\n return this;\r\n }\r\n\r\n public replaceAll(key: string, value: any) {\r\n const contentAsString = JSON.stringify(this.content);\r\n\r\n this.content = JSON.parse(contentAsString.replaceAll(key, value));\r\n\r\n return this;\r\n }\r\n}\r\n\r\nexport function file(path: string) {\r\n return new FileManager(path);\r\n}\r\n\r\nexport function jsonFile(path: string) {\r\n return new JsonFileManager(path);\r\n}\r\n"],"mappings":"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"}
package/package.json CHANGED
@@ -12,13 +12,13 @@
12
12
  "dependencies": {
13
13
  "@clack/prompts": "^0.7.0",
14
14
  "@mongez/copper": "^2.1.2",
15
- "@warlock.js/fs": "5.0.1",
15
+ "@warlock.js/fs": "5.1.0",
16
16
  "@mongez/reinforcements": "^4.0.1",
17
17
  "cross-spawn": "^7.0.3",
18
18
  "rimraf": "^6.0.1",
19
19
  "which-pm-runs": "^1.1.0"
20
20
  },
21
- "version": "5.0.1",
21
+ "version": "5.1.0",
22
22
  "type": "module",
23
23
  "main": "./esm/index.mjs",
24
24
  "module": "./esm/index.mjs",
@@ -1,5 +1,5 @@
1
1
  {
2
- "name": "wow2",
2
+ "name": "name-set-by-scaffolder",
3
3
  "version": "1.0.0",
4
4
  "private": true,
5
5
  "type": "module",
@@ -35,14 +35,14 @@
35
35
  "@mongez/reinforcements": "^4.0.1",
36
36
  "@mongez/localization": "^3.4.7",
37
37
  "@mongez/supportive-is": "^2.1.4",
38
- "@warlock.js/auth": "4.15.0",
39
- "@warlock.js/cache": "4.15.0",
40
- "@warlock.js/cascade": "4.15.0",
41
- "@warlock.js/scheduler": "4.15.0",
42
- "@warlock.js/core": "4.15.0",
43
- "@warlock.js/fs": "4.15.0",
44
- "@warlock.js/logger": "4.15.0",
45
- "@warlock.js/seal": "4.15.0",
38
+ "@warlock.js/auth": "0.0.0-set-by-scaffolder",
39
+ "@warlock.js/cache": "0.0.0-set-by-scaffolder",
40
+ "@warlock.js/cascade": "0.0.0-set-by-scaffolder",
41
+ "@warlock.js/scheduler": "0.0.0-set-by-scaffolder",
42
+ "@warlock.js/core": "0.0.0-set-by-scaffolder",
43
+ "@warlock.js/fs": "0.0.0-set-by-scaffolder",
44
+ "@warlock.js/logger": "0.0.0-set-by-scaffolder",
45
+ "@warlock.js/seal": "0.0.0-set-by-scaffolder",
46
46
  "dayjs": "^1.11.19"
47
47
  },
48
48
  "devDependencies": {
@@ -1,11 +1,14 @@
1
- import { t, type RequestHandler } from "@warlock.js/core";
1
+ import { t } from "@warlock.js/core";
2
+ import { type GuardedRequestHandler } from "app/auth/requests/guarded.request";
2
3
  import { logoutAllService } from "../services/auth.service";
3
4
 
4
5
  /**
5
6
  * Logout from all devices controller
6
7
  * POST /auth/logout-all
8
+ *
9
+ * Guarded route — see the note on `logoutController`.
7
10
  */
8
- export const logoutAllController: RequestHandler = async ({ request, response }) => {
11
+ export const logoutAllController: GuardedRequestHandler = async ({ request, response }) => {
9
12
  await logoutAllService(request.user);
10
13
 
11
14
  return response.success({
@@ -1,11 +1,16 @@
1
- import { t, type RequestHandler } from "@warlock.js/core";
1
+ import { t } from "@warlock.js/core";
2
+ import { type GuardedRequestHandler } from "app/auth/requests/guarded.request";
2
3
  import { logoutService } from "../services/auth.service";
3
4
 
4
5
  /**
5
6
  * Logout controller
6
7
  * POST /auth/logout
8
+ *
9
+ * Typed as `GuardedRequestHandler` because the route sits inside `guarded()`
10
+ * (see `../routes.ts`). A plain `RequestHandler` types `request.user` as
11
+ * `RequestUser | undefined`, which is not assignable to `logoutService`.
7
12
  */
8
- export const logoutController: RequestHandler = async ({ request, response }) => {
13
+ export const logoutController: GuardedRequestHandler = async ({ request, response }) => {
9
14
  await logoutService(request.user);
10
15
 
11
16
  return response.success({
@@ -1,10 +1,15 @@
1
- import { type RequestHandler } from "@warlock.js/core";
1
+ import { type GuardedRequestHandler } from "app/auth/requests/guarded.request";
2
2
 
3
3
  /**
4
4
  * Get current user controller
5
5
  * GET /auth/me
6
+ *
7
+ * Guarded route. As a plain `RequestHandler` this compiled without complaint
8
+ * while serializing `RequestUser | undefined` — i.e. the empty interface `{}` —
9
+ * so the response body was typed as nothing at all and no error was raised.
10
+ * `GuardedRequestHandler` types it as the app's `User` model.
6
11
  */
7
- export const meController: RequestHandler = async ({ request, response }) => {
12
+ export const meController: GuardedRequestHandler = async ({ request, response }) => {
8
13
  return response.success({
9
14
  user: request.user,
10
15
  });
@@ -7,3 +7,13 @@ scheduler.newJob("cleanup-expired-otps", cleanupExpiredOtpsService).everyHour();
7
7
 
8
8
  // Cleanup expired refresh tokens every hour
9
9
  scheduler.newJob("cleanup-expired-tokens", () => authService.cleanupExpiredTokens()).everyHour();
10
+
11
+ // Registering jobs does not schedule them — without this, a fresh project's very
12
+ // first `warlock dev` printed "scheduler.start() was never called" and neither
13
+ // cleanup ever ran.
14
+ //
15
+ // Safe to call here: `start()` throws on an empty scheduler, and the two jobs
16
+ // above are registered directly before it. Jobs registered later by other
17
+ // modules still run — `addJob` prepares them when the scheduler is already
18
+ // running.
19
+ scheduler.start();
@@ -1,6 +1,15 @@
1
1
  import { type RequestHandler } from "@warlock.js/core";
2
2
  import { HomePageComponent } from "../components/HomePageComponent";
3
3
 
4
+ /**
5
+ * React-rendered welcome page — kept only when the `react` feature is selected.
6
+ *
7
+ * This file and its plain-JSON sibling (`home-page.controller.ts`) are a
8
+ * mutually exclusive PAIR: both export `homePageController`, and the scaffolder
9
+ * (`configureHomePage`) deletes exactly one at generation time, so a real
10
+ * project never contains both. They coexist here in the template on purpose —
11
+ * this is not resolver-order ambiguity, and neither file is dead code.
12
+ */
4
13
  export const homePageController: RequestHandler = async ({ response }) => {
5
14
  return response.render(<HomePageComponent />);
6
15
  };
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Sample test to verify test infrastructure
3
3
  */
4
- import { testGet } from "@warlock.js/core";
4
+ import { testGet } from "@warlock.js/core/tests";
5
5
  import { describe, expect, it } from "vitest";
6
6
 
7
7
  describe("Test Infrastructure", () => {
@@ -7,5 +7,6 @@ export default Migration.create(User, {
7
7
  password: string(255),
8
8
  image: string(500).nullable(),
9
9
  imageMetadata: json().nullable(),
10
+ lastLogin: timestamp().nullable(),
10
11
  deletedAt: timestamp().nullable(),
11
12
  });
@@ -9,6 +9,11 @@ export const userSchema = v.object({
9
9
  email: v.email().requiredIfEmpty("id"),
10
10
  image: v.string(),
11
11
  password: v.string().min(6).requiredIfEmpty("id").addTransformer(useHashedPassword()),
12
+ // Written by the social-login handler (`app/users/services/login-social.ts`).
13
+ // A field only reachable through `save({ merge })` still has to be declared
14
+ // here — `merge` is typed against this schema, so an undeclared key is a
15
+ // compile error, not a silent write.
16
+ lastLogin: v.date(),
12
17
  });
13
18
 
14
19
  export type UserSchema = Infer<typeof userSchema>;
@@ -13,8 +13,14 @@ const loginSocial: GuardedRequestHandler = async ({ request, response }) => {
13
13
 
14
14
  const auth = await user.generateAccessToken();
15
15
 
16
- user.save({
17
- lastLogin: new Date(),
16
+ // `save()` takes WriterOptions — the fields to write go under `merge`, typed
17
+ // against `userSchema`. Passing `{ lastLogin }` at the top level was not a
18
+ // write at all: it was an unknown option, and the value went nowhere.
19
+ // Awaited, so the write is not a floating promise racing the response.
20
+ await user.save({
21
+ merge: {
22
+ lastLogin: new Date(),
23
+ },
18
24
  });
19
25
 
20
26
  return response.success({
@@ -7,6 +7,22 @@ import {
7
7
  } from "@warlock.js/cache";
8
8
  import { DatabaseCacheDriver, env, useRequestStore } from "@warlock.js/core";
9
9
 
10
+ /**
11
+ * Namespace every cache key by the caller's domain, so two tenants hitting the
12
+ * same app never read each other's cached values.
13
+ *
14
+ * This used to branch on `request.client` first. That property does not exist:
15
+ * it survived from v4, where `Request` carried a `[key: string]: any` index
16
+ * signature that made any property name compile. v5 removed the index signature
17
+ * and the branch became a type error.
18
+ *
19
+ * It was deleted rather than renamed. The obvious "fix" — pointing it at
20
+ * `request.locals.client` — compiles and is worse than the bug: nothing in the
21
+ * framework populates `request.locals`, so the branch would be permanently
22
+ * `undefined` and silently dead. `originDomain` below is real (it is derived
23
+ * from the `Origin` header) and already covers the multi-tenant case, so the
24
+ * scaffold prefixes on that and models nothing the framework does not provide.
25
+ */
10
26
  const globalPrefix = () => {
11
27
  const { request } = useRequestStore();
12
28
 
@@ -14,12 +30,6 @@ const globalPrefix = () => {
14
30
 
15
31
  if (!request) return cachePrefix;
16
32
 
17
- if (request.client) {
18
- cachePrefix = `${cachePrefix}.${request.client.get("username")}`;
19
-
20
- return cachePrefix;
21
- }
22
-
23
33
  const domain = request.originDomain || request.header("domain") || request.input("domain");
24
34
 
25
35
  if (!domain) return cachePrefix;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Project-wide type augmentations.
3
+ *
4
+ * This is where the app teaches the framework about ITS OWN types. Warlock's
5
+ * `Request` class no longer carries a `[key: string]: any` index signature —
6
+ * attaching arbitrary properties used to compile silently and hide real bugs
7
+ * behind `any` — so the sanctioned way to extend a request is to merge extra
8
+ * members into the framework's own interfaces from here.
9
+ *
10
+ * ---------------------------------------------------------------------------
11
+ * WHY THIS FILE HAS `export {}` AT THE BOTTOM
12
+ * ---------------------------------------------------------------------------
13
+ * `declare module "x" { ... }` means two completely different things depending
14
+ * on whether the enclosing file is a module:
15
+ *
16
+ * - In a SCRIPT file (no top-level `import`/`export`), it declares an AMBIENT
17
+ * module — it REPLACES the real typings of `@warlock.js/core` with whatever
18
+ * is inside the braces, and every existing export vanishes.
19
+ * - In a MODULE file (has a top-level `import`/`export`), it is a MODULE
20
+ * AUGMENTATION — it merges into the real typings, which is what we want.
21
+ *
22
+ * The trailing `export {}` is what makes this file a module. Do not delete it,
23
+ * and do not "clean it up" as an unused statement.
24
+ *
25
+ * ---------------------------------------------------------------------------
26
+ * WHY THESE ARE `interface` AND NOT `type`
27
+ * ---------------------------------------------------------------------------
28
+ * This project's standard is "prefer `type` over `interface`". These
29
+ * declarations are a NAMED EXCEPTION to that standard: declaration merging is
30
+ * an interface-only feature. A `type RequestUser = { ... }` here does not merge
31
+ * with the framework's declaration — it is a duplicate-identifier error. Every
32
+ * augmentation in this file must stay an `interface`.
33
+ */
34
+
35
+ declare module "@warlock.js/core" {
36
+ /**
37
+ * Arbitrary per-request data — `request.locals`.
38
+ *
39
+ * Populate it from a middleware and read it downstream in the same request.
40
+ *
41
+ * NOTE: the framework itself never writes to `request.locals`; it only
42
+ * initializes it to `{}` once per request. Anything you declare here is a
43
+ * promise YOU have to keep in a middleware, otherwise the property typechecks
44
+ * and is `undefined` forever at runtime. Declare a field here only once
45
+ * something actually assigns it.
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * interface RequestLocals {
50
+ * // set by a middleware that resolves the tenant from the request host
51
+ * tenant?: Tenant;
52
+ * }
53
+ * ```
54
+ */
55
+ interface RequestLocals {}
56
+
57
+ /**
58
+ * The authenticated user — `request.user`.
59
+ *
60
+ * The auth middleware assigns the resolved user model here. Declaring its
61
+ * shape gives unguarded handlers (ones that may or may not have a user) a
62
+ * real type instead of `{}`.
63
+ *
64
+ * This does NOT make `request.user` non-optional: the framework declares it
65
+ * as `user?: RequestUser`, and augmentation can add members but cannot remove
66
+ * the `?`. For routes that are actually behind the auth guard, type the
67
+ * handler as `GuardedRequestHandler` (see
68
+ * `src/app/auth/requests/guarded.request.ts`) — that narrows `request.user`
69
+ * to the app's `User` model AND drops the `| undefined`.
70
+ *
71
+ * @example
72
+ * ```ts
73
+ * interface RequestUser {
74
+ * id: string | number;
75
+ * email: string;
76
+ * }
77
+ * ```
78
+ */
79
+ interface RequestUser {}
80
+ }
81
+
82
+ export {};
@@ -10,18 +10,27 @@
10
10
  "downlevelIteration": true,
11
11
  "strict": true,
12
12
  "forceConsistentCasingInFileNames": true,
13
- "typeRoots": ["./src/typings.d.ts"],
14
13
  "noFallthroughCasesInSwitch": true,
15
14
  "module": "ESNext",
16
15
  "jsx": "react-jsx",
17
16
  "isolatedModules": true,
18
- "moduleResolution": "node",
17
+ // "bundler", not "node". The framework builds this project with esbuild, and
18
+ // every `@warlock.js/*` package ships an `exports` map with subpath entries
19
+ // (e.g. `@warlock.js/core/tests`). Classic "node" resolution ignores
20
+ // `exports` entirely, so those subpaths resolved to nothing and any import
21
+ // of one failed to typecheck even though it ran fine at runtime.
22
+ "moduleResolution": "bundler",
19
23
  "resolveJsonModule": true,
20
24
  "paths": {
21
25
  "app/*": ["./src/app/*"]
22
26
  },
23
27
  "noEmit": true
24
28
  },
25
- "include": ["src", "warlock.config.ts", ".warlock/typings/*.d.ts"],
29
+ // `src/typings.d.ts` is listed explicitly even though "src" already covers it:
30
+ // it is the project's module-augmentation file, and a dangling reference to it
31
+ // here is exactly the bug this replaced (it used to sit under `typeRoots`,
32
+ // which takes DIRECTORIES of @types packages, not files — so the entry pointed
33
+ // at a path that did not exist and did nothing).
34
+ "include": ["src", "src/typings.d.ts", "warlock.config.ts", ".warlock/typings/*.d.ts"],
26
35
  "exclude": ["node_modules"]
27
36
  }