@warlock.js/core 5.14.0 → 5.16.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 +22 -0
- package/esm/application/application-config-types.d.mts +9 -0
- package/esm/application/application-config-types.d.mts.map +1 -1
- package/esm/application/index.d.mts +2 -1
- package/esm/application/index.mjs +1 -0
- package/esm/application/public-url.d.mts +20 -0
- package/esm/application/public-url.d.mts.map +1 -0
- package/esm/application/public-url.mjs +26 -0
- package/esm/application/public-url.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/shared/merge-web-sitemap-config.mjs +118 -0
- package/esm/generations/features/shared/merge-web-sitemap-config.mjs.map +1 -0
- package/esm/generations/features/sitemap.feature.mjs +77 -0
- package/esm/generations/features/sitemap.feature.mjs.map +1 -0
- package/esm/http/errors/errors.d.mts +16 -1
- package/esm/http/errors/errors.d.mts.map +1 -1
- package/esm/http/errors/errors.mjs +19 -1
- package/esm/http/errors/errors.mjs.map +1 -1
- package/esm/http/index.d.mts +2 -1
- package/esm/http/index.mjs +1 -1
- package/esm/http/parse-urlencoded-body.mjs +27 -0
- package/esm/http/parse-urlencoded-body.mjs.map +1 -0
- package/esm/http/plugins.d.mts.map +1 -1
- package/esm/http/plugins.mjs +8 -0
- package/esm/http/plugins.mjs.map +1 -1
- package/esm/http/request.d.mts +8 -0
- package/esm/http/request.d.mts.map +1 -1
- package/esm/http/request.mjs +13 -1
- package/esm/http/request.mjs.map +1 -1
- package/esm/http/response.d.mts +9 -2
- package/esm/http/response.d.mts.map +1 -1
- package/esm/http/response.mjs +12 -4
- package/esm/http/response.mjs.map +1 -1
- package/esm/http/xmlable.d.mts +14 -0
- package/esm/http/xmlable.d.mts.map +1 -0
- package/esm/index.d.mts +4 -2
- package/esm/index.mjs +3 -2
- package/llms-full.txt +74 -3
- package/llms.txt +2 -2
- package/package.json +11 -11
- package/skills/configure-app/SKILL.md +29 -2
- package/skills/create-controller/SKILL.md +14 -0
- package/skills/health-checks/SKILL.md +8 -0
- package/skills/send-response/SKILL.md +17 -1
- package/skills/use-localization/SKILL.md +6 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,28 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
> ⚠ **Versioning: `@warlock.js/*` does not follow SemVer strictly — breaking changes may ship in a minor.** This is a deliberate decision, not an oversight: the framework is pre-adoption and the cost of a major per behaviour fix currently outweighs the benefit. **Pin an exact version or a tilde range (`~4.13.0`) if you need to opt into changes rather than receive them.** Every breaking change is marked **BREAKING** in its entry and summarised in an _Upgrading_ section at the top of the release. **This policy will change once the framework has consumers beyond its author.**
|
|
8
8
|
|
|
9
|
+
## 5.16.0 - 2026-09-18
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Core now parses `application/x-www-form-urlencoded` request bodies, so plain HTML forms and OAuth `form_post` callbacks (Apple) reach their routes instead of failing with `FST_ERR_CTP_INVALID_MEDIA_TYPE`. These bodies have the same `http.bodyLimit` as JSON. A key sent more than once becomes an array.
|
|
14
|
+
- `response.xml()` accepts a raw string or any value with a `toXML(): string` method (such as a `@warlock.js/sitemap` `Sitemap`), and sends `application/xml`.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- `warlock add sitemap` now merges a disabled `sitemap` section into `src/config/web.ts`, and creates that file when it is missing. It no longer writes `src/config/sitemap.ts` or registers `sitemapConnector()`. The merge edits only a `sitemap` key directly on the exported config object, and the result is re-parsed before it is written.
|
|
19
|
+
|
|
20
|
+
## 5.15.0 - 2026-09-18
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
|
|
24
|
+
- `warlock add sitemap` installs `@warlock.js/sitemap`, writes `src/config/sitemap.ts`, and registers `sitemapConnector()` in `warlock.config.ts`. The generated config ships **disabled**: a sitemap needs the application public origin and a generated app cannot know it, so the block explains the two steps to turn it on rather than producing an app that refuses to boot.
|
|
25
|
+
- `app.publicUrl` config key, with a `PUBLIC_APP_URL` environment fallback: the one absolute-URL source for every consumer that needs an origin. It never falls back to a request-derived host — an absolute URL built from the wrong host is worse than a boot that refuses to start, because nothing downstream reports it.
|
|
26
|
+
|
|
27
|
+
### Changed
|
|
28
|
+
|
|
29
|
+
- **BREAKING (fail-loud):** `request.cookie(name)` and `request.hasCookie(name)` now throw `CookieJarUnavailableError` when `@fastify/cookie` is not registered on the Fastify instance, instead of returning `undefined` / `false`. An unregistered plugin is a configuration fault, and it was previously indistinguishable from "the caller sent no such cookie" — under `authMiddleware([], "cookie:token")` it surfaced as a permanent, unexplained 401. Apps built on `createHttpApplication` are unaffected: core registers the plugin before anything mounts. The exposure is a host that mounts a guarded surface on its own Fastify instance. `request.cookies` stays lenient and still returns `{}`, so the framework's own opportunistic reads — locale resolution among them — are unchanged.
|
|
30
|
+
|
|
9
31
|
## 5.14.0 - 2026-09-17
|
|
10
32
|
|
|
11
33
|
### Added
|
|
@@ -16,6 +16,15 @@ type AppConfigurations = {
|
|
|
16
16
|
* @default localhost:
|
|
17
17
|
*/
|
|
18
18
|
baseUrl?: string;
|
|
19
|
+
/**
|
|
20
|
+
* The application's public origin — the absolute URL other consumers (the
|
|
21
|
+
* sitemap route, canonical links, OG tags, absolute URLs in mail) build
|
|
22
|
+
* links against. Optional in general; falls back to the `PUBLIC_APP_URL`
|
|
23
|
+
* env var (see {@link getPublicUrl}). A consumer that requires it (e.g. the
|
|
24
|
+
* sitemap route) fails boot loudly, naming both, when neither is set — it
|
|
25
|
+
* never falls back to a request-derived origin.
|
|
26
|
+
*/
|
|
27
|
+
publicUrl?: string;
|
|
19
28
|
/**
|
|
20
29
|
* Application timezone
|
|
21
30
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"application-config-types.d.mts","names":[],"sources":["../../../../../../../core/src/application/application-config-types.ts"],"mappings":";KAAY,iBAAA;EAAA;;;EAIV,OAAA;EAAA;;;;;EAMA,UAAA;
|
|
1
|
+
{"version":3,"file":"application-config-types.d.mts","names":[],"sources":["../../../../../../../core/src/application/application-config-types.ts"],"mappings":";KAAY,iBAAA;EAAA;;;EAIV,OAAA;EAAA;;;;;EAMA,UAAA;EAuBW;AAAA;;;;EAjBX,OAAA;;;;;;;;;EASA,SAAA;;;;EAIA,QAAA;;;;EAIA,WAAA;AAAA"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { app } from "./app.mjs";
|
|
2
2
|
import { Application, BootContext, BootListener, BootValidator, ShutdownListener } from "./application.mjs";
|
|
3
3
|
import { BOOT_SIGNAL_ENV_KEY, BOOT_SIGNAL_VERSION, BootSignal, BootSignalType, isBootSignal, sendBootSignal } from "./boot-signal.mjs";
|
|
4
|
-
import { AppConfigurations } from "./application-config-types.mjs";
|
|
4
|
+
import { AppConfigurations } from "./application-config-types.mjs";
|
|
5
|
+
import { getPublicUrl } from "./public-url.mjs";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region ../core/src/application/public-url.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The application's public origin — the ONE absolute-URL source every
|
|
4
|
+
* consumer that needs one (the sitemap route, canonical links, OG tags,
|
|
5
|
+
* absolute URLs in mail) reads instead of each keeping its own copy.
|
|
6
|
+
*
|
|
7
|
+
* `app.publicUrl` wins over the `PUBLIC_APP_URL` env fallback. Deliberately
|
|
8
|
+
* does not fall back further to a request-derived origin: a sitemap (or any
|
|
9
|
+
* other absolute URL) served from the wrong host is worse than a boot that
|
|
10
|
+
* refuses to start, because nothing downstream ever tells you it was wrong.
|
|
11
|
+
*
|
|
12
|
+
* Optional in general — most apps have no consumer that needs it yet.
|
|
13
|
+
* Returns `undefined` rather than throwing; a consumer that requires the
|
|
14
|
+
* value (e.g. the sitemap route, at boot) is responsible for failing loudly
|
|
15
|
+
* itself, naming both `app.publicUrl` and `PUBLIC_APP_URL`.
|
|
16
|
+
*/
|
|
17
|
+
declare function getPublicUrl(): string | undefined;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { getPublicUrl };
|
|
20
|
+
//# sourceMappingURL=public-url.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"public-url.d.mts","names":[],"sources":["../../../../../../../core/src/application/public-url.ts"],"mappings":";;AAiBA;;;;AAA4B;;;;;;;;;;iBAAZ,YAAA"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { config } from "../config/config-getter.mjs";
|
|
2
|
+
import "../config/index.mjs";
|
|
3
|
+
|
|
4
|
+
//#region ../core/src/application/public-url.ts
|
|
5
|
+
/**
|
|
6
|
+
* The application's public origin — the ONE absolute-URL source every
|
|
7
|
+
* consumer that needs one (the sitemap route, canonical links, OG tags,
|
|
8
|
+
* absolute URLs in mail) reads instead of each keeping its own copy.
|
|
9
|
+
*
|
|
10
|
+
* `app.publicUrl` wins over the `PUBLIC_APP_URL` env fallback. Deliberately
|
|
11
|
+
* does not fall back further to a request-derived origin: a sitemap (or any
|
|
12
|
+
* other absolute URL) served from the wrong host is worse than a boot that
|
|
13
|
+
* refuses to start, because nothing downstream ever tells you it was wrong.
|
|
14
|
+
*
|
|
15
|
+
* Optional in general — most apps have no consumer that needs it yet.
|
|
16
|
+
* Returns `undefined` rather than throwing; a consumer that requires the
|
|
17
|
+
* value (e.g. the sitemap route, at boot) is responsible for failing loudly
|
|
18
|
+
* itself, naming both `app.publicUrl` and `PUBLIC_APP_URL`.
|
|
19
|
+
*/
|
|
20
|
+
function getPublicUrl() {
|
|
21
|
+
return config.key("app.publicUrl") || process.env.PUBLIC_APP_URL || void 0;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
//#endregion
|
|
25
|
+
export { getPublicUrl };
|
|
26
|
+
//# sourceMappingURL=public-url.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"public-url.mjs","names":[],"sources":["../../../../../../../core/src/application/public-url.ts"],"sourcesContent":["import { config } from \"../config\";\n\n/**\n * The application's public origin — the ONE absolute-URL source every\n * consumer that needs one (the sitemap route, canonical links, OG tags,\n * absolute URLs in mail) reads instead of each keeping its own copy.\n *\n * `app.publicUrl` wins over the `PUBLIC_APP_URL` env fallback. Deliberately\n * does not fall back further to a request-derived origin: a sitemap (or any\n * other absolute URL) served from the wrong host is worse than a boot that\n * refuses to start, because nothing downstream ever tells you it was wrong.\n *\n * Optional in general — most apps have no consumer that needs it yet.\n * Returns `undefined` rather than throwing; a consumer that requires the\n * value (e.g. the sitemap route, at boot) is responsible for failing loudly\n * itself, naming both `app.publicUrl` and `PUBLIC_APP_URL`.\n */\nexport function getPublicUrl(): string | undefined {\n return config.key<string | undefined>(\"app.publicUrl\") || process.env.PUBLIC_APP_URL || undefined;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAiBA,SAAgB,eAAmC;CACjD,OAAO,OAAO,IAAwB,eAAe,KAAK,QAAQ,IAAI,kBAAkB;AAC1F"}
|
|
@@ -26,6 +26,7 @@ import { s3Feature } from "./s3.feature.mjs";
|
|
|
26
26
|
import { schedulerFeature } from "./scheduler.feature.mjs";
|
|
27
27
|
import { sesFeature } from "./ses.feature.mjs";
|
|
28
28
|
import { shadcnFeature } from "./shadcn.feature.mjs";
|
|
29
|
+
import { sitemapFeature } from "./sitemap.feature.mjs";
|
|
29
30
|
import { socketFeature } from "./socket.feature.mjs";
|
|
30
31
|
import { tailwindFeature } from "./tailwind.feature.mjs";
|
|
31
32
|
import { testFeature } from "./test.feature.mjs";
|
|
@@ -56,6 +57,7 @@ const featuresMap = {
|
|
|
56
57
|
web: webFeature,
|
|
57
58
|
tailwind: tailwindFeature,
|
|
58
59
|
shadcn: shadcnFeature,
|
|
60
|
+
sitemap: sitemapFeature,
|
|
59
61
|
herald: heraldFeature,
|
|
60
62
|
queue: queueFeature,
|
|
61
63
|
"bull-board": bullBoardFeature,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/index.ts"],"sourcesContent":["import { accessFeature } from \"./access.feature\";\r\nimport { aiAnthropicFeature } from \"./ai-anthropic.feature\";\r\nimport { aiBedrockFeature } from \"./ai-bedrock.feature\";\r\nimport { aiGoogleFeature } from \"./ai-google.feature\";\r\nimport { aiOllamaFeature } from \"./ai-ollama.feature\";\r\nimport { aiOpenaiFeature } from \"./ai-openai.feature\";\r\nimport { aiPanopticFeature } from \"./ai-panoptic.feature\";\r\nimport { aiToolsFeature } from \"./ai-tools.feature\";\r\nimport { aiWorkspaceFeature } from \"./ai-workspace.feature\";\r\nimport { aiFeature } from \"./ai.feature\";\r\nimport { authGoogleFeature } from \"./auth-google.feature\";\r\nimport { authPasskeysFeature } from \"./auth-passkeys.feature\";\r\nimport { bullBoardFeature } from \"./bull-board.feature\";\r\nimport { heraldFeature } from \"./herald.feature\";\r\nimport { imageFeature } from \"./image.feature\";\r\nimport { mailFeature } from \"./mail.feature\";\r\nimport { mongodbFeature } from \"./mongodb.feature\";\r\nimport { mysqlFeature } from \"./mysql.feature\";\r\nimport { notificationsFeature } from \"./notifications.feature\";\r\nimport { postgresFeature } from \"./postgres.feature\";\r\nimport { queueFeature } from \"./queue.feature\";\r\nimport { reactEmailFeature } from \"./react-email.feature\";\r\nimport { reactFeature } from \"./react.feature\";\r\nimport { redisFeature } from \"./redis.feature\";\r\nimport { s3Feature } from \"./s3.feature\";\r\nimport { schedulerFeature } from \"./scheduler.feature\";\r\nimport { sesFeature } from \"./ses.feature\";\r\nimport { shadcnFeature } from \"./shadcn.feature\";\r\nimport { socketFeature } from \"./socket.feature\";\r\nimport { tailwindFeature } from \"./tailwind.feature\";\r\nimport { testFeature } from \"./test.feature\";\r\nimport type { FeatureDefinition } from \"./types\";\r\nimport { webFeature } from \"./web.feature\";\r\n\r\nexport type { FeatureDefinition } from \"./types\";\r\n\r\n/**\r\n * The feature registry `warlock add` dispatches against.\r\n *\r\n * This file is an INDEX, nothing more: every entry lives in its own module\r\n * alongside the `onExecuting` body it runs. Key order is load-bearing — it is\r\n * the order `--list` prints and the order the \"not allowed\" error lists — so\r\n * add new features in the place they should appear, not alphabetically.\r\n */\r\nexport const featuresMap: Record<string, FeatureDefinition> = {\r\n \"react-email\": reactEmailFeature,\r\n react: reactFeature,\r\n image: imageFeature,\r\n mail: mailFeature,\r\n ses: sesFeature,\r\n mongodb: mongodbFeature,\r\n scheduler: schedulerFeature,\r\n // swagger / postman intentionally omitted — those packages do not exist yet;\r\n // they will ship together in the unified @warlock.js/api-docs package.\r\n postgres: postgresFeature,\r\n mysql: mysqlFeature,\r\n redis: redisFeature,\r\n s3: s3Feature,\r\n test: testFeature,\r\n web: webFeature,\r\n // Directly after `web`, and only there: it `requires` it, it is useless\r\n // without it, and a reader scanning `--list` for the page stack should meet\r\n // the two together rather than find styling filed between queues and sockets.\r\n tailwind: tailwindFeature,\r\n // Immediately after `tailwind`, for the same reason `tailwind` follows `web`:\r\n // it `requires` it, it appends to the stylesheet that feature creates, and the\r\n // three of them are one stack a reader should meet in build order.\r\n shadcn: shadcnFeature,\r\n herald: heraldFeature,\r\n queue: queueFeature,\r\n // Directly after `queue`, for the same reason `tailwind` follows `web`: it\r\n // needs queue already installed and configured, and a reader scanning\r\n // `--list` for job-queue features should meet the two together.\r\n \"bull-board\": bullBoardFeature,\r\n socket: socketFeature,\r\n notifications: notificationsFeature,\r\n access: accessFeature,\r\n // Login methods for @warlock.js/auth — \"<package>-<vendor>\" like the ai-* entries.\r\n \"auth-google\": authGoogleFeature,\r\n \"auth-passkeys\": authPasskeysFeature,\r\n ai: aiFeature,\r\n \"ai-openai\": aiOpenaiFeature,\r\n \"ai-google\": aiGoogleFeature,\r\n \"ai-anthropic\": aiAnthropicFeature,\r\n \"ai-bedrock\": aiBedrockFeature,\r\n \"ai-ollama\": aiOllamaFeature,\r\n \"ai-tools\": aiToolsFeature,\r\n \"ai-panoptic\": aiPanopticFeature,\r\n \"ai-workspace\": aiWorkspaceFeature,\r\n};\r\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/index.ts"],"sourcesContent":["import { accessFeature } from \"./access.feature\";\r\nimport { aiAnthropicFeature } from \"./ai-anthropic.feature\";\r\nimport { aiBedrockFeature } from \"./ai-bedrock.feature\";\r\nimport { aiGoogleFeature } from \"./ai-google.feature\";\r\nimport { aiOllamaFeature } from \"./ai-ollama.feature\";\r\nimport { aiOpenaiFeature } from \"./ai-openai.feature\";\r\nimport { aiPanopticFeature } from \"./ai-panoptic.feature\";\r\nimport { aiToolsFeature } from \"./ai-tools.feature\";\r\nimport { aiWorkspaceFeature } from \"./ai-workspace.feature\";\r\nimport { aiFeature } from \"./ai.feature\";\r\nimport { authGoogleFeature } from \"./auth-google.feature\";\r\nimport { authPasskeysFeature } from \"./auth-passkeys.feature\";\r\nimport { bullBoardFeature } from \"./bull-board.feature\";\r\nimport { heraldFeature } from \"./herald.feature\";\r\nimport { imageFeature } from \"./image.feature\";\r\nimport { mailFeature } from \"./mail.feature\";\r\nimport { mongodbFeature } from \"./mongodb.feature\";\r\nimport { mysqlFeature } from \"./mysql.feature\";\r\nimport { notificationsFeature } from \"./notifications.feature\";\r\nimport { postgresFeature } from \"./postgres.feature\";\r\nimport { queueFeature } from \"./queue.feature\";\r\nimport { reactEmailFeature } from \"./react-email.feature\";\r\nimport { reactFeature } from \"./react.feature\";\r\nimport { redisFeature } from \"./redis.feature\";\r\nimport { s3Feature } from \"./s3.feature\";\r\nimport { schedulerFeature } from \"./scheduler.feature\";\r\nimport { sesFeature } from \"./ses.feature\";\r\nimport { shadcnFeature } from \"./shadcn.feature\";\r\nimport { sitemapFeature } from \"./sitemap.feature\";\r\nimport { socketFeature } from \"./socket.feature\";\r\nimport { tailwindFeature } from \"./tailwind.feature\";\r\nimport { testFeature } from \"./test.feature\";\r\nimport type { FeatureDefinition } from \"./types\";\r\nimport { webFeature } from \"./web.feature\";\r\n\r\nexport type { FeatureDefinition } from \"./types\";\r\n\r\n/**\r\n * The feature registry `warlock add` dispatches against.\r\n *\r\n * This file is an INDEX, nothing more: every entry lives in its own module\r\n * alongside the `onExecuting` body it runs. Key order is load-bearing — it is\r\n * the order `--list` prints and the order the \"not allowed\" error lists — so\r\n * add new features in the place they should appear, not alphabetically.\r\n */\r\nexport const featuresMap: Record<string, FeatureDefinition> = {\r\n \"react-email\": reactEmailFeature,\r\n react: reactFeature,\r\n image: imageFeature,\r\n mail: mailFeature,\r\n ses: sesFeature,\r\n mongodb: mongodbFeature,\r\n scheduler: schedulerFeature,\r\n // swagger / postman intentionally omitted — those packages do not exist yet;\r\n // they will ship together in the unified @warlock.js/api-docs package.\r\n postgres: postgresFeature,\r\n mysql: mysqlFeature,\r\n redis: redisFeature,\r\n s3: s3Feature,\r\n test: testFeature,\r\n web: webFeature,\r\n // Directly after `web`, and only there: it `requires` it, it is useless\r\n // without it, and a reader scanning `--list` for the page stack should meet\r\n // the two together rather than find styling filed between queues and sockets.\r\n tailwind: tailwindFeature,\r\n // Immediately after `tailwind`, for the same reason `tailwind` follows `web`:\r\n // it `requires` it, it appends to the stylesheet that feature creates, and the\r\n // three of them are one stack a reader should meet in build order.\r\n shadcn: shadcnFeature,\r\n // Directly after the web/tailwind/shadcn stack, for the same reason: it\r\n // `requires` web (it reads the page registry `listRoutablePages()`\r\n // exposes) and a reader scanning `--list` for the page stack should meet it\r\n // there rather than filed between queues and sockets.\r\n sitemap: sitemapFeature,\r\n herald: heraldFeature,\r\n queue: queueFeature,\r\n // Directly after `queue`, for the same reason `tailwind` follows `web`: it\r\n // needs queue already installed and configured, and a reader scanning\r\n // `--list` for job-queue features should meet the two together.\r\n \"bull-board\": bullBoardFeature,\r\n socket: socketFeature,\r\n notifications: notificationsFeature,\r\n access: accessFeature,\r\n // Login methods for @warlock.js/auth — \"<package>-<vendor>\" like the ai-* entries.\r\n \"auth-google\": authGoogleFeature,\r\n \"auth-passkeys\": authPasskeysFeature,\r\n ai: aiFeature,\r\n \"ai-openai\": aiOpenaiFeature,\r\n \"ai-google\": aiGoogleFeature,\r\n \"ai-anthropic\": aiAnthropicFeature,\r\n \"ai-bedrock\": aiBedrockFeature,\r\n \"ai-ollama\": aiOllamaFeature,\r\n \"ai-tools\": aiToolsFeature,\r\n \"ai-panoptic\": aiPanopticFeature,\r\n \"ai-workspace\": aiWorkspaceFeature,\r\n};\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6CA,MAAa,cAAiD;CAC5D,eAAe;CACf,OAAO;CACP,OAAO;CACP,MAAM;CACN,KAAK;CACL,SAAS;CACT,WAAW;CAGX,UAAU;CACV,OAAO;CACP,OAAO;CACP,IAAI;CACJ,MAAM;CACN,KAAK;CAIL,UAAU;CAIV,QAAQ;CAKR,SAAS;CACT,QAAQ;CACR,OAAO;CAIP,cAAc;CACd,QAAQ;CACR,eAAe;CACf,QAAQ;CAER,eAAe;CACf,iBAAiB;CACjB,IAAI;CACJ,aAAa;CACb,aAAa;CACb,gBAAgB;CAChB,cAAc;CACd,aAAa;CACb,YAAY;CACZ,eAAe;CACf,gBAAgB;AAClB"}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
|
|
3
|
+
//#region ../core/src/generations/features/shared/merge-web-sitemap-config.ts
|
|
4
|
+
/**
|
|
5
|
+
* Merge a disabled `sitemap` section into `src/config/web.ts` SOURCE TEXT.
|
|
6
|
+
*
|
|
7
|
+
* **String surgery, never parse-and-print** — same rationale as
|
|
8
|
+
* `insertConnectorEntry`: `web.ts` is app-owned and may carry any formatting
|
|
9
|
+
* or comments a parse-and-print would discard. The TypeScript AST is used to
|
|
10
|
+
* *find where to cut*, never to regenerate the file — the splice below still
|
|
11
|
+
* copies the original source's bytes verbatim.
|
|
12
|
+
*
|
|
13
|
+
* Recognises the exported config object two ways: an inline `export default
|
|
14
|
+
* {...}`, or `export default <identifier>;` pointing back at a `const
|
|
15
|
+
* <identifier> = {...}` (or `const <identifier>: SomeType = {...}`) declared
|
|
16
|
+
* earlier in the file — the shape this generator itself scaffolds, and the
|
|
17
|
+
* shape every other generated config file in this project uses. Any other
|
|
18
|
+
* shape (a function call, a re-export, no default export at all) returns
|
|
19
|
+
* `"unrecognised"` rather than guessing at where to cut — clobbering an
|
|
20
|
+
* app-owned file the merge could not actually understand is worse than
|
|
21
|
+
* refusing.
|
|
22
|
+
*
|
|
23
|
+
* A `sitemap` property already present anywhere in the source (by key, so one
|
|
24
|
+
* a human has since hand-edited is still found) is left unchanged — that is
|
|
25
|
+
* what makes a second `warlock add sitemap` a no-op instead of a duplicate
|
|
26
|
+
* block. Detection walks the AST rather than regex-matching `sitemap\s*:`, so
|
|
27
|
+
* a comment like `// sitemap: TODO` — text, not a property — does not count.
|
|
28
|
+
*
|
|
29
|
+
* Before returning a merge, the resulting text is re-parsed and checked for
|
|
30
|
+
* syntax errors; a merge that would not itself parse is refused (as
|
|
31
|
+
* `"unrecognised"`) instead of written, since a matched-but-corrupt splice is
|
|
32
|
+
* worse than doing nothing.
|
|
33
|
+
*
|
|
34
|
+
* @param source The config file's current text.
|
|
35
|
+
* @returns What happened, and the new text when there is any.
|
|
36
|
+
*/
|
|
37
|
+
function mergeWebSitemapConfig(source) {
|
|
38
|
+
const sourceFile = ts.createSourceFile("web.ts", source, ts.ScriptTarget.ESNext, true, ts.ScriptKind.TS);
|
|
39
|
+
const objectLiteral = findExportedConfigObjectLiteral(sourceFile);
|
|
40
|
+
if (!objectLiteral) return { status: "unrecognised" };
|
|
41
|
+
if (hasTopLevelSitemapProperty(objectLiteral)) return { status: "already-present" };
|
|
42
|
+
const bodyStart = objectLiteral.getStart(sourceFile) + 1;
|
|
43
|
+
const closingBraceIndex = objectLiteral.getEnd() - 1;
|
|
44
|
+
const body = source.slice(bodyStart, closingBraceIndex);
|
|
45
|
+
const indentMatch = /\n([ \t]*)\S/.exec(body);
|
|
46
|
+
const indent = indentMatch ? indentMatch[1] : " ";
|
|
47
|
+
const block = `${indent}sitemap: {\n${indent}${indent}enabled: false,\n${indent}${indent}path: "/sitemap.xml",\n${indent}${indent}defaults: { changefreq: "weekly", priority: 0.5 },\n${indent}},\n`;
|
|
48
|
+
const { index: insertionPoint, needsComma } = lastPropertyEnd(source, closingBraceIndex);
|
|
49
|
+
const next = `${source.slice(0, insertionPoint)}${needsComma ? ",\n" : ""}${block}${source.slice(closingBraceIndex)}`;
|
|
50
|
+
if (hasSyntaxErrors(next)) return { status: "unrecognised" };
|
|
51
|
+
return {
|
|
52
|
+
status: "merged",
|
|
53
|
+
next
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** Whether TEXT fails to parse as TypeScript source. */
|
|
57
|
+
function hasSyntaxErrors(text) {
|
|
58
|
+
const { diagnostics } = ts.transpileModule(text, {
|
|
59
|
+
compilerOptions: {
|
|
60
|
+
module: ts.ModuleKind.ESNext,
|
|
61
|
+
target: ts.ScriptTarget.ESNext
|
|
62
|
+
},
|
|
63
|
+
reportDiagnostics: true
|
|
64
|
+
});
|
|
65
|
+
return Boolean(diagnostics && diagnostics.length > 0);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Whether the exported config object literal already has a `sitemap`
|
|
69
|
+
* property directly on it — a `sitemap` key nested under some other
|
|
70
|
+
* property (e.g. `seo: { sitemap: true }`) is a different feature's field
|
|
71
|
+
* and must not block adding the top-level one.
|
|
72
|
+
*/
|
|
73
|
+
function hasTopLevelSitemapProperty(objectLiteral) {
|
|
74
|
+
return objectLiteral.properties.some((property) => (ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) && propertyKeyName(property.name) === "sitemap");
|
|
75
|
+
}
|
|
76
|
+
/** The literal text of a property name node, when it is one of the plain shapes we care about. */
|
|
77
|
+
function propertyKeyName(name) {
|
|
78
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) return name.text;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Find the object literal a source file's default export ultimately points
|
|
82
|
+
* at, whether the export is inline or an identifier declared earlier in the
|
|
83
|
+
* file.
|
|
84
|
+
*/
|
|
85
|
+
function findExportedConfigObjectLiteral(sourceFile) {
|
|
86
|
+
const exportAssignment = sourceFile.statements.find((statement) => ts.isExportAssignment(statement) && !statement.isExportEquals);
|
|
87
|
+
if (!exportAssignment) return void 0;
|
|
88
|
+
const { expression } = exportAssignment;
|
|
89
|
+
if (ts.isObjectLiteralExpression(expression)) return expression;
|
|
90
|
+
if (!ts.isIdentifier(expression)) return void 0;
|
|
91
|
+
const identifier = expression.text;
|
|
92
|
+
for (const statement of sourceFile.statements) {
|
|
93
|
+
if (!ts.isVariableStatement(statement)) continue;
|
|
94
|
+
for (const declaration of statement.declarationList.declarations) if (ts.isIdentifier(declaration.name) && declaration.name.text === identifier && declaration.initializer && ts.isObjectLiteralExpression(declaration.initializer)) return declaration.initializer;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Where to insert a new property right before a closing brace, and whether
|
|
99
|
+
* the property immediately before it (if any) needs a trailing comma added —
|
|
100
|
+
* an object literal with two properties and no comma between them does not
|
|
101
|
+
* parse.
|
|
102
|
+
*/
|
|
103
|
+
function lastPropertyEnd(source, closingBraceIndex) {
|
|
104
|
+
let cursor = closingBraceIndex - 1;
|
|
105
|
+
while (cursor >= 0 && /\s/.test(source.charAt(cursor))) cursor--;
|
|
106
|
+
if (cursor < 0 || source[cursor] === "{" || source[cursor] === ",") return {
|
|
107
|
+
index: closingBraceIndex,
|
|
108
|
+
needsComma: false
|
|
109
|
+
};
|
|
110
|
+
return {
|
|
111
|
+
index: cursor + 1,
|
|
112
|
+
needsComma: true
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
//#endregion
|
|
117
|
+
export { mergeWebSitemapConfig };
|
|
118
|
+
//# sourceMappingURL=merge-web-sitemap-config.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge-web-sitemap-config.mjs","names":[],"sources":["../../../../../../../../../core/src/generations/features/shared/merge-web-sitemap-config.ts"],"sourcesContent":["import ts from \"typescript\";\n\n/** What {@link mergeWebSitemapConfig} did, or could not do, to the source it was given. */\nexport type WebSitemapMergeResult =\n { status: \"merged\"; next: string } | { status: \"already-present\" } | { status: \"unrecognised\" };\n\n/**\n * Merge a disabled `sitemap` section into `src/config/web.ts` SOURCE TEXT.\n *\n * **String surgery, never parse-and-print** — same rationale as\n * `insertConnectorEntry`: `web.ts` is app-owned and may carry any formatting\n * or comments a parse-and-print would discard. The TypeScript AST is used to\n * *find where to cut*, never to regenerate the file — the splice below still\n * copies the original source's bytes verbatim.\n *\n * Recognises the exported config object two ways: an inline `export default\n * {...}`, or `export default <identifier>;` pointing back at a `const\n * <identifier> = {...}` (or `const <identifier>: SomeType = {...}`) declared\n * earlier in the file — the shape this generator itself scaffolds, and the\n * shape every other generated config file in this project uses. Any other\n * shape (a function call, a re-export, no default export at all) returns\n * `\"unrecognised\"` rather than guessing at where to cut — clobbering an\n * app-owned file the merge could not actually understand is worse than\n * refusing.\n *\n * A `sitemap` property already present anywhere in the source (by key, so one\n * a human has since hand-edited is still found) is left unchanged — that is\n * what makes a second `warlock add sitemap` a no-op instead of a duplicate\n * block. Detection walks the AST rather than regex-matching `sitemap\\s*:`, so\n * a comment like `// sitemap: TODO` — text, not a property — does not count.\n *\n * Before returning a merge, the resulting text is re-parsed and checked for\n * syntax errors; a merge that would not itself parse is refused (as\n * `\"unrecognised\"`) instead of written, since a matched-but-corrupt splice is\n * worse than doing nothing.\n *\n * @param source The config file's current text.\n * @returns What happened, and the new text when there is any.\n */\nexport function mergeWebSitemapConfig(source: string): WebSitemapMergeResult {\n const sourceFile = ts.createSourceFile(\n \"web.ts\",\n source,\n ts.ScriptTarget.ESNext,\n /* setParentNodes */ true,\n ts.ScriptKind.TS,\n );\n\n const objectLiteral = findExportedConfigObjectLiteral(sourceFile);\n\n if (!objectLiteral) {\n return { status: \"unrecognised\" };\n }\n\n if (hasTopLevelSitemapProperty(objectLiteral)) {\n return { status: \"already-present\" };\n }\n\n const bodyStart = objectLiteral.getStart(sourceFile) + 1;\n const closingBraceIndex = objectLiteral.getEnd() - 1;\n const body = source.slice(bodyStart, closingBraceIndex);\n\n const indentMatch = /\\n([ \\t]*)\\S/.exec(body);\n const indent = indentMatch ? indentMatch[1] : \" \";\n\n const block =\n `${indent}sitemap: {\\n` +\n `${indent}${indent}enabled: false,\\n` +\n `${indent}${indent}path: \"/sitemap.xml\",\\n` +\n `${indent}${indent}defaults: { changefreq: \"weekly\", priority: 0.5 },\\n` +\n `${indent}},\\n`;\n\n const { index: insertionPoint, needsComma } = lastPropertyEnd(source, closingBraceIndex);\n\n const next =\n `${source.slice(0, insertionPoint)}` +\n `${needsComma ? \",\\n\" : \"\"}` +\n `${block}` +\n `${source.slice(closingBraceIndex)}`;\n\n if (hasSyntaxErrors(next)) {\n return { status: \"unrecognised\" };\n }\n\n return { status: \"merged\", next };\n}\n\n/** Whether TEXT fails to parse as TypeScript source. */\nfunction hasSyntaxErrors(text: string): boolean {\n const { diagnostics } = ts.transpileModule(text, {\n compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ESNext },\n reportDiagnostics: true,\n });\n\n return Boolean(diagnostics && diagnostics.length > 0);\n}\n\n/**\n * Whether the exported config object literal already has a `sitemap`\n * property directly on it — a `sitemap` key nested under some other\n * property (e.g. `seo: { sitemap: true }`) is a different feature's field\n * and must not block adding the top-level one.\n */\nfunction hasTopLevelSitemapProperty(objectLiteral: ts.ObjectLiteralExpression): boolean {\n return objectLiteral.properties.some(\n (property) =>\n (ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) &&\n propertyKeyName(property.name) === \"sitemap\",\n );\n}\n\n/** The literal text of a property name node, when it is one of the plain shapes we care about. */\nfunction propertyKeyName(name: ts.PropertyName): string | undefined {\n if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) {\n return name.text;\n }\n\n return undefined;\n}\n\n/**\n * Find the object literal a source file's default export ultimately points\n * at, whether the export is inline or an identifier declared earlier in the\n * file.\n */\nfunction findExportedConfigObjectLiteral(\n sourceFile: ts.SourceFile,\n): ts.ObjectLiteralExpression | undefined {\n const exportAssignment = sourceFile.statements.find(\n (statement): statement is ts.ExportAssignment =>\n ts.isExportAssignment(statement) && !statement.isExportEquals,\n );\n\n if (!exportAssignment) return undefined;\n\n const { expression } = exportAssignment;\n\n if (ts.isObjectLiteralExpression(expression)) {\n return expression;\n }\n\n if (!ts.isIdentifier(expression)) return undefined;\n\n const identifier = expression.text;\n\n for (const statement of sourceFile.statements) {\n if (!ts.isVariableStatement(statement)) continue;\n\n for (const declaration of statement.declarationList.declarations) {\n if (\n ts.isIdentifier(declaration.name) &&\n declaration.name.text === identifier &&\n declaration.initializer &&\n ts.isObjectLiteralExpression(declaration.initializer)\n ) {\n return declaration.initializer;\n }\n }\n }\n\n return undefined;\n}\n\n/**\n * Where to insert a new property right before a closing brace, and whether\n * the property immediately before it (if any) needs a trailing comma added —\n * an object literal with two properties and no comma between them does not\n * parse.\n */\nfunction lastPropertyEnd(\n source: string,\n closingBraceIndex: number,\n): { index: number; needsComma: boolean } {\n let cursor = closingBraceIndex - 1;\n\n while (cursor >= 0 && /\\s/.test(source.charAt(cursor))) {\n cursor--;\n }\n\n if (cursor < 0 || source[cursor] === \"{\" || source[cursor] === \",\") {\n return { index: closingBraceIndex, needsComma: false };\n }\n\n return { index: cursor + 1, needsComma: true };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAuCA,SAAgB,sBAAsB,QAAuC;CAC3E,MAAM,aAAa,GAAG,iBACpB,UACA,QACA,GAAG,aAAa,QACK,MACrB,GAAG,WAAW,EAChB;CAEA,MAAM,gBAAgB,gCAAgC,UAAU;CAEhE,IAAI,CAAC,eACH,OAAO,EAAE,QAAQ,eAAe;CAGlC,IAAI,2BAA2B,aAAa,GAC1C,OAAO,EAAE,QAAQ,kBAAkB;CAGrC,MAAM,YAAY,cAAc,SAAS,UAAU,IAAI;CACvD,MAAM,oBAAoB,cAAc,OAAO,IAAI;CACnD,MAAM,OAAO,OAAO,MAAM,WAAW,iBAAiB;CAEtD,MAAM,cAAc,eAAe,KAAK,IAAI;CAC5C,MAAM,SAAS,cAAc,YAAY,KAAK;CAE9C,MAAM,QACJ,GAAG,OAAO,cACP,SAAS,OAAO,mBAChB,SAAS,OAAO,yBAChB,SAAS,OAAO,sDAChB,OAAO;CAEZ,MAAM,EAAE,OAAO,gBAAgB,eAAe,gBAAgB,QAAQ,iBAAiB;CAEvF,MAAM,OACJ,GAAG,OAAO,MAAM,GAAG,cAAc,IAC9B,aAAa,QAAQ,KACrB,QACA,OAAO,MAAM,iBAAiB;CAEnC,IAAI,gBAAgB,IAAI,GACtB,OAAO,EAAE,QAAQ,eAAe;CAGlC,OAAO;EAAE,QAAQ;EAAU;CAAK;AAClC;;AAGA,SAAS,gBAAgB,MAAuB;CAC9C,MAAM,EAAE,gBAAgB,GAAG,gBAAgB,MAAM;EAC/C,iBAAiB;GAAE,QAAQ,GAAG,WAAW;GAAQ,QAAQ,GAAG,aAAa;EAAO;EAChF,mBAAmB;CACrB,CAAC;CAED,OAAO,QAAQ,eAAe,YAAY,SAAS,CAAC;AACtD;;;;;;;AAQA,SAAS,2BAA2B,eAAoD;CACtF,OAAO,cAAc,WAAW,MAC7B,cACE,GAAG,qBAAqB,QAAQ,KAAK,GAAG,8BAA8B,QAAQ,MAC/E,gBAAgB,SAAS,IAAI,MAAM,SACvC;AACF;;AAGA,SAAS,gBAAgB,MAA2C;CAClE,IAAI,GAAG,aAAa,IAAI,KAAK,GAAG,gBAAgB,IAAI,KAAK,GAAG,iBAAiB,IAAI,GAC/E,OAAO,KAAK;AAIhB;;;;;;AAOA,SAAS,gCACP,YACwC;CACxC,MAAM,mBAAmB,WAAW,WAAW,MAC5C,cACC,GAAG,mBAAmB,SAAS,KAAK,CAAC,UAAU,cACnD;CAEA,IAAI,CAAC,kBAAkB,OAAO;CAE9B,MAAM,EAAE,eAAe;CAEvB,IAAI,GAAG,0BAA0B,UAAU,GACzC,OAAO;CAGT,IAAI,CAAC,GAAG,aAAa,UAAU,GAAG,OAAO;CAEzC,MAAM,aAAa,WAAW;CAE9B,KAAK,MAAM,aAAa,WAAW,YAAY;EAC7C,IAAI,CAAC,GAAG,oBAAoB,SAAS,GAAG;EAExC,KAAK,MAAM,eAAe,UAAU,gBAAgB,cAClD,IACE,GAAG,aAAa,YAAY,IAAI,KAChC,YAAY,KAAK,SAAS,cAC1B,YAAY,eACZ,GAAG,0BAA0B,YAAY,WAAW,GAEpD,OAAO,YAAY;CAGzB;AAGF;;;;;;;AAQA,SAAS,gBACP,QACA,mBACwC;CACxC,IAAI,SAAS,oBAAoB;CAEjC,OAAO,UAAU,KAAK,KAAK,KAAK,OAAO,OAAO,MAAM,CAAC,GACnD;CAGF,IAAI,SAAS,KAAK,OAAO,YAAY,OAAO,OAAO,YAAY,KAC7D,OAAO;EAAE,OAAO;EAAmB,YAAY;CAAM;CAGvD,OAAO;EAAE,OAAO,SAAS;EAAG,YAAY;CAAK;AAC/C"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { srcPath } from "../../utils/paths.mjs";
|
|
2
|
+
import "../../utils/index.mjs";
|
|
3
|
+
import { INSTALLED_WARLOCK_VERSION } from "./types.mjs";
|
|
4
|
+
import { mergeWebSitemapConfig } from "./shared/merge-web-sitemap-config.mjs";
|
|
5
|
+
import { colors } from "@mongez/copper";
|
|
6
|
+
import { fileExistsAsync, getFileAsync, putFileAsync } from "@warlock.js/fs";
|
|
7
|
+
|
|
8
|
+
//#region ../core/src/generations/features/sitemap.feature.ts
|
|
9
|
+
const NEXT_STEPS = "Next: set `app.publicUrl` (src/config/app.ts) or the PUBLIC_APP_URL environment variable, then flip `sitemap.enabled` to true in src/config/web.ts.";
|
|
10
|
+
/**
|
|
11
|
+
* Ships DISABLED because a sitemap needs this application's public origin and
|
|
12
|
+
* a freshly generated app has no way to know it. Two steps to turn it on:
|
|
13
|
+
*
|
|
14
|
+
* 1. set `app.publicUrl` in src/config/app.ts, or the PUBLIC_APP_URL
|
|
15
|
+
* environment variable;
|
|
16
|
+
* 2. flip `sitemap.enabled` to true here.
|
|
17
|
+
*
|
|
18
|
+
* With it enabled and no origin configured, generation REFUSES rather than
|
|
19
|
+
* serving absolute URLs built from a guessed host — a sitemap pointing at
|
|
20
|
+
* the wrong domain is worse than one that never starts, because nothing
|
|
21
|
+
* downstream reports it.
|
|
22
|
+
*/
|
|
23
|
+
const freshWebConfigStub = `import type { WebSitemapConfig } from "@warlock.js/web/sitemap";
|
|
24
|
+
|
|
25
|
+
const webConfig: { sitemap: WebSitemapConfig } = {
|
|
26
|
+
sitemap: {
|
|
27
|
+
enabled: false,
|
|
28
|
+
path: "/sitemap.xml",
|
|
29
|
+
defaults: { changefreq: "weekly", priority: 0.5 },
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default webConfig;
|
|
34
|
+
`;
|
|
35
|
+
/**
|
|
36
|
+
* `warlock add sitemap` writes its policy into the app's \`src/config/web.ts\`,
|
|
37
|
+
* under the \`sitemap\` key — there is no \`src/config/sitemap.ts\` and no
|
|
38
|
+
* standalone connector to register; \`@warlock.js/web\` (already installed via
|
|
39
|
+
* the \`web\` requirement) reads \`web.sitemap\` itself.
|
|
40
|
+
*
|
|
41
|
+
* Creates \`web.ts\` when the app does not have one yet; otherwise merges a
|
|
42
|
+
* \`sitemap\` section into whatever is already there rather than clobbering it.
|
|
43
|
+
*/
|
|
44
|
+
async function installSitemapConfig() {
|
|
45
|
+
const configPath = srcPath("config/web.ts");
|
|
46
|
+
if (!await fileExistsAsync(configPath)) {
|
|
47
|
+
await putFileAsync(configPath, freshWebConfigStub);
|
|
48
|
+
console.log(`${colors.green("✓")} Created src/config/web.ts with a disabled sitemap section`);
|
|
49
|
+
console.log(NEXT_STEPS);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const merge = mergeWebSitemapConfig(await getFileAsync(configPath));
|
|
53
|
+
if (merge.status === "already-present") {
|
|
54
|
+
console.log(`${colors.yellowBright("sitemap")} already configured in src/config/web.ts, skipping...`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (merge.status === "unrecognised") {
|
|
58
|
+
console.log(`${colors.redBright("✗")} Could not safely add a \`sitemap\` section to ${colors.yellowBright("src/config/web.ts")} — its exported config object was not recognised, so nothing was written. Add it yourself:
|
|
59
|
+
sitemap: { enabled: false, path: "/sitemap.xml", defaults: { changefreq: "weekly", priority: 0.5 } },`);
|
|
60
|
+
process.exitCode = 1;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
await putFileAsync(configPath, merge.next);
|
|
64
|
+
console.log(`${colors.green("✓")} Added a disabled \`sitemap\` section to src/config/web.ts`);
|
|
65
|
+
console.log(NEXT_STEPS);
|
|
66
|
+
}
|
|
67
|
+
/** `warlock add sitemap` — runtime sitemap.xml generation, backed by the page registry. */
|
|
68
|
+
const sitemapFeature = {
|
|
69
|
+
description: "Installs @warlock.js/sitemap — runtime sitemap.xml generation from the page registry. Adds a disabled `sitemap` section to src/config/web.ts, creating the file if it doesn't exist yet. Requires app.publicUrl (or PUBLIC_APP_URL) to be set.",
|
|
70
|
+
requires: ["web"],
|
|
71
|
+
dependencies: { "@warlock.js/sitemap": INSTALLED_WARLOCK_VERSION },
|
|
72
|
+
onExecuting: installSitemapConfig
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
//#endregion
|
|
76
|
+
export { sitemapFeature };
|
|
77
|
+
//# sourceMappingURL=sitemap.feature.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sitemap.feature.mjs","names":[],"sources":["../../../../../../../../core/src/generations/features/sitemap.feature.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { fileExistsAsync, getFileAsync, putFileAsync } from \"@warlock.js/fs\";\nimport { srcPath } from \"../../utils\";\nimport { mergeWebSitemapConfig } from \"./shared/merge-web-sitemap-config\";\nimport { type FeatureDefinition, INSTALLED_WARLOCK_VERSION } from \"./types\";\n\nconst NEXT_STEPS =\n \"Next: set `app.publicUrl` (src/config/app.ts) or the PUBLIC_APP_URL environment \" +\n \"variable, then flip `sitemap.enabled` to true in src/config/web.ts.\";\n\n/**\n * Ships DISABLED because a sitemap needs this application's public origin and\n * a freshly generated app has no way to know it. Two steps to turn it on:\n *\n * 1. set `app.publicUrl` in src/config/app.ts, or the PUBLIC_APP_URL\n * environment variable;\n * 2. flip `sitemap.enabled` to true here.\n *\n * With it enabled and no origin configured, generation REFUSES rather than\n * serving absolute URLs built from a guessed host — a sitemap pointing at\n * the wrong domain is worse than one that never starts, because nothing\n * downstream reports it.\n */\nconst freshWebConfigStub = `import type { WebSitemapConfig } from \"@warlock.js/web/sitemap\";\n\nconst webConfig: { sitemap: WebSitemapConfig } = {\n sitemap: {\n enabled: false,\n path: \"/sitemap.xml\",\n defaults: { changefreq: \"weekly\", priority: 0.5 },\n },\n};\n\nexport default webConfig;\n`;\n\n/**\n * `warlock add sitemap` writes its policy into the app's \\`src/config/web.ts\\`,\n * under the \\`sitemap\\` key — there is no \\`src/config/sitemap.ts\\` and no\n * standalone connector to register; \\`@warlock.js/web\\` (already installed via\n * the \\`web\\` requirement) reads \\`web.sitemap\\` itself.\n *\n * Creates \\`web.ts\\` when the app does not have one yet; otherwise merges a\n * \\`sitemap\\` section into whatever is already there rather than clobbering it.\n */\nasync function installSitemapConfig(): Promise<void> {\n const configPath = srcPath(\"config/web.ts\");\n\n if (!(await fileExistsAsync(configPath))) {\n await putFileAsync(configPath, freshWebConfigStub);\n console.log(`${colors.green(\"✓\")} Created src/config/web.ts with a disabled sitemap section`);\n console.log(NEXT_STEPS);\n\n return;\n }\n\n const current = await getFileAsync(configPath);\n const merge = mergeWebSitemapConfig(current);\n\n if (merge.status === \"already-present\") {\n console.log(\n `${colors.yellowBright(\"sitemap\")} already configured in src/config/web.ts, skipping...`,\n );\n\n return;\n }\n\n if (merge.status === \"unrecognised\") {\n console.log(\n `${colors.redBright(\"✗\")} Could not safely add a \\`sitemap\\` section to ` +\n `${colors.yellowBright(\"src/config/web.ts\")} — its exported config object was not ` +\n \"recognised, so nothing was written. Add it yourself:\\n\" +\n ' sitemap: { enabled: false, path: \"/sitemap.xml\", defaults: { changefreq: \"weekly\", priority: 0.5 } },',\n );\n\n process.exitCode = 1;\n\n return;\n }\n\n await putFileAsync(configPath, merge.next);\n console.log(`${colors.green(\"✓\")} Added a disabled \\`sitemap\\` section to src/config/web.ts`);\n console.log(NEXT_STEPS);\n}\n\n/** `warlock add sitemap` — runtime sitemap.xml generation, backed by the page registry. */\nexport const sitemapFeature: FeatureDefinition = {\n description:\n \"Installs @warlock.js/sitemap — runtime sitemap.xml generation from the page registry. \" +\n \"Adds a disabled `sitemap` section to src/config/web.ts, creating the file if it doesn't \" +\n \"exist yet. Requires app.publicUrl (or PUBLIC_APP_URL) to be set.\",\n requires: [\"web\"],\n dependencies: {\n \"@warlock.js/sitemap\": INSTALLED_WARLOCK_VERSION,\n },\n onExecuting: installSitemapConfig,\n};\n"],"mappings":";;;;;;;;AAMA,MAAM,aACJ;;;;;;;;;;;;;;AAgBF,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;AAsB3B,eAAe,uBAAsC;CACnD,MAAM,aAAa,QAAQ,eAAe;CAE1C,IAAI,CAAE,MAAM,gBAAgB,UAAU,GAAI;EACxC,MAAM,aAAa,YAAY,kBAAkB;EACjD,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,2DAA2D;EAC5F,QAAQ,IAAI,UAAU;EAEtB;CACF;CAGA,MAAM,QAAQ,sBAAsB,MADd,aAAa,UAAU,CACF;CAE3C,IAAI,MAAM,WAAW,mBAAmB;EACtC,QAAQ,IACN,GAAG,OAAO,aAAa,SAAS,EAAE,sDACpC;EAEA;CACF;CAEA,IAAI,MAAM,WAAW,gBAAgB;EACnC,QAAQ,IACN,GAAG,OAAO,UAAU,GAAG,EAAE,iDACpB,OAAO,aAAa,mBAAmB,EAAE;wGAGhD;EAEA,QAAQ,WAAW;EAEnB;CACF;CAEA,MAAM,aAAa,YAAY,MAAM,IAAI;CACzC,QAAQ,IAAI,GAAG,OAAO,MAAM,GAAG,EAAE,2DAA2D;CAC5F,QAAQ,IAAI,UAAU;AACxB;;AAGA,MAAa,iBAAoC;CAC/C,aACE;CAGF,UAAU,CAAC,KAAK;CAChB,cAAc,EACZ,uBAAuB,0BACzB;CACA,aAAa;AACf"}
|
|
@@ -37,6 +37,21 @@ declare class NotAllowedError extends HttpError {
|
|
|
37
37
|
payload?: any | undefined;
|
|
38
38
|
constructor(message: string, payload?: any | undefined);
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* Thrown by `Request.prototype.cookie` / `Request.prototype.hasCookie` when
|
|
42
|
+
* `@fastify/cookie` was never registered on this Fastify instance, so
|
|
43
|
+
* `baseRequest.cookies` is `undefined` rather than an (possibly empty) object.
|
|
44
|
+
*
|
|
45
|
+
* A by-name cookie read is a deliberate assertion by the caller — "this
|
|
46
|
+
* cookie should be readable here" — so an unavailable jar is a configuration
|
|
47
|
+
* fault, not an absent cookie, and must not be swallowed into a default
|
|
48
|
+
* value or a silent `false`. Contrast `Request.prototype.cookies`, which
|
|
49
|
+
* stays lenient because the framework's own opportunistic reads (e.g.
|
|
50
|
+
* locale resolution) must not throw on a request that simply has no jar.
|
|
51
|
+
*/
|
|
52
|
+
declare class CookieJarUnavailableError extends Error {
|
|
53
|
+
constructor(cookieName: string);
|
|
54
|
+
}
|
|
40
55
|
/**
|
|
41
56
|
* Thrown by `Request.prototype.user` in development to catch a call site
|
|
42
57
|
* still reading the removed `request.user` getter/setter after the 5.12.0
|
|
@@ -54,5 +69,5 @@ declare class RequestUserMovedError extends Error {
|
|
|
54
69
|
constructor();
|
|
55
70
|
}
|
|
56
71
|
//#endregion
|
|
57
|
-
export { BadRequestError, ConflictError, ForbiddenError, HttpError, NotAcceptableError, NotAllowedError, RequestUserMovedError, ResourceNotFoundError, ServerError, UnAuthorizedError };
|
|
72
|
+
export { BadRequestError, ConflictError, CookieJarUnavailableError, ForbiddenError, HttpError, NotAcceptableError, NotAllowedError, RequestUserMovedError, ResourceNotFoundError, ServerError, UnAuthorizedError };
|
|
58
73
|
//# sourceMappingURL=errors.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.mts","names":[],"sources":["../../../../../../../../core/src/http/errors/errors.ts"],"mappings":";cAAa,SAAA,SAAkB,KAAK;EAEzB,MAAA;EACA,OAAA;EACA,OAAA;cAFA,MAAA,UACA,OAAA,UACA,OAAA;AAAA;AAAA,cAOE,qBAAA,SAA8B,SAAS;EAGzC,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,iBAAA,SAA0B,SAAS;EAGrC,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,cAAA,SAAuB,SAAS;EAGlC,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,eAAA,SAAwB,SAAS;EAGnC,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,WAAA,SAAoB,SAAS;EAG/B,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,aAAA,SAAsB,SAAS;EAGjC,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,kBAAA,SAA2B,SAAS;EAGtC,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,eAAA,SAAwB,SAAS;EAGnC,OAAA;cADP,OAAA,UACO,OAAA;AAAA;;;;;;;;;;AAlDa;AAOxB
|
|
1
|
+
{"version":3,"file":"errors.d.mts","names":[],"sources":["../../../../../../../../core/src/http/errors/errors.ts"],"mappings":";cAAa,SAAA,SAAkB,KAAK;EAEzB,MAAA;EACA,OAAA;EACA,OAAA;cAFA,MAAA,UACA,OAAA,UACA,OAAA;AAAA;AAAA,cAOE,qBAAA,SAA8B,SAAS;EAGzC,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,iBAAA,SAA0B,SAAS;EAGrC,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,cAAA,SAAuB,SAAS;EAGlC,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,eAAA,SAAwB,SAAS;EAGnC,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,WAAA,SAAoB,SAAS;EAG/B,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,aAAA,SAAsB,SAAS;EAGjC,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,kBAAA,SAA2B,SAAS;EAGtC,OAAA;cADP,OAAA,UACO,OAAA;AAAA;AAAA,cAOE,eAAA,SAAwB,SAAS;EAGnC,OAAA;cADP,OAAA,UACO,OAAA;AAAA;;;;;;;;;;AAlDa;AAOxB;;cA8Da,yBAAA,SAAkC,KAAK;cAC/B,UAAA;AAAA;;;;;;AA5DG;AAOxB;;;;;;;cA6Ea,qBAAA,SAA8B,KAAK;EAAL,WAAA;AAAA"}
|
|
@@ -65,6 +65,24 @@ var NotAllowedError = class extends HttpError {
|
|
|
65
65
|
}
|
|
66
66
|
};
|
|
67
67
|
/**
|
|
68
|
+
* Thrown by `Request.prototype.cookie` / `Request.prototype.hasCookie` when
|
|
69
|
+
* `@fastify/cookie` was never registered on this Fastify instance, so
|
|
70
|
+
* `baseRequest.cookies` is `undefined` rather than an (possibly empty) object.
|
|
71
|
+
*
|
|
72
|
+
* A by-name cookie read is a deliberate assertion by the caller — "this
|
|
73
|
+
* cookie should be readable here" — so an unavailable jar is a configuration
|
|
74
|
+
* fault, not an absent cookie, and must not be swallowed into a default
|
|
75
|
+
* value or a silent `false`. Contrast `Request.prototype.cookies`, which
|
|
76
|
+
* stays lenient because the framework's own opportunistic reads (e.g.
|
|
77
|
+
* locale resolution) must not throw on a request that simply has no jar.
|
|
78
|
+
*/
|
|
79
|
+
var CookieJarUnavailableError = class extends Error {
|
|
80
|
+
constructor(cookieName) {
|
|
81
|
+
super(`Cannot read cookie "${cookieName}": the cookie jar is unavailable because @fastify/cookie is not registered on this Fastify instance. Register the plugin (see core's http/plugins.ts) before reading cookies by name.`);
|
|
82
|
+
this.name = "CookieJarUnavailableError";
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
68
86
|
* Thrown by `Request.prototype.user` in development to catch a call site
|
|
69
87
|
* still reading the removed `request.user` getter/setter after the 5.12.0
|
|
70
88
|
* move: the authenticated user now lives at `request.locals.user`, written
|
|
@@ -85,5 +103,5 @@ var RequestUserMovedError = class extends Error {
|
|
|
85
103
|
};
|
|
86
104
|
|
|
87
105
|
//#endregion
|
|
88
|
-
export { BadRequestError, ConflictError, ForbiddenError, HttpError, NotAcceptableError, NotAllowedError, RequestUserMovedError, ResourceNotFoundError, ServerError, UnAuthorizedError };
|
|
106
|
+
export { BadRequestError, ConflictError, CookieJarUnavailableError, ForbiddenError, HttpError, NotAcceptableError, NotAllowedError, RequestUserMovedError, ResourceNotFoundError, ServerError, UnAuthorizedError };
|
|
89
107
|
//# sourceMappingURL=errors.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.mjs","names":[],"sources":["../../../../../../../../core/src/http/errors/errors.ts"],"sourcesContent":["export class HttpError extends Error {\n public constructor(\n public status: number,\n public message: string,\n public payload?: any,\n ) {\n super(message);\n this.name = \"HttpError\";\n }\n}\n\nexport class ResourceNotFoundError extends HttpError {\n public constructor(\n message: string,\n public payload?: any,\n ) {\n super(404, message, payload);\n this.name = \"ResourceNotFoundError\";\n }\n}\n\nexport class UnAuthorizedError extends HttpError {\n public constructor(\n message: string,\n public payload?: any,\n ) {\n super(401, message, payload);\n this.name = \"UnAuthorizedError\";\n }\n}\n\nexport class ForbiddenError extends HttpError {\n public constructor(\n message: string,\n public payload?: any,\n ) {\n super(403, message, payload);\n this.name = \"ForbiddenError\";\n }\n}\n\nexport class BadRequestError extends HttpError {\n public constructor(\n message: string,\n public payload?: any,\n ) {\n super(400, message, payload);\n this.name = \"BadRequestError\";\n }\n}\n\nexport class ServerError extends HttpError {\n public constructor(\n message: string,\n public payload?: any,\n ) {\n super(500, message, payload);\n this.name = \"ServerError\";\n }\n}\n\nexport class ConflictError extends HttpError {\n public constructor(\n message: string,\n public payload?: any,\n ) {\n super(409, message, payload);\n this.name = \"ConflictError\";\n }\n}\n\nexport class NotAcceptableError extends HttpError {\n public constructor(\n message: string,\n public payload?: any,\n ) {\n super(406, message, payload);\n this.name = \"NotAcceptableError\";\n }\n}\n\nexport class NotAllowedError extends HttpError {\n public constructor(\n message: string,\n public payload?: any,\n ) {\n super(405, message, payload);\n this.name = \"NotAllowedError\";\n }\n}\n\n/**\n * Thrown by `Request.prototype.user` in development to catch a call site\n * still reading the removed `request.user` getter/setter after the 5.12.0\n * move: the authenticated user now lives at `request.locals.user`, written\n * by `@warlock.js/auth`'s middleware.\n *\n * Development-only diagnostic, not a runtime contract other code should\n * catch — the getter itself is typed `never`, so a caller that still\n * compiles against `request.user` only does so via `any`/an outdated type.\n * Kept in core (not auth) because `request.user` is a core `Request`\n * accessor and core cannot depend on `@warlock.js/auth` to react to it.\n * Slated for removal one release after 5.12.0 — see the CHANGELOG.\n */\nexport class RequestUserMovedError extends Error {\n public constructor() {\n super(\n \"request.user has been removed. The authenticated user now lives at \" +\n \"request.locals.user, set by @warlock.js/auth's middleware.\",\n );\n this.name = \"RequestUserMovedError\";\n }\n}\n"],"mappings":";AAAA,IAAa,YAAb,cAA+B,MAAM;CACnC,AAAO,YACL,AAAO,QACP,AAAO,SACP,AAAO,SACP;EACA,MAAM,OAAO;EAJN;EACA;EACA;EAGP,KAAK,OAAO;CACd;AACF;AAEA,IAAa,wBAAb,cAA2C,UAAU;CACnD,AAAO,YACL,SACA,AAAO,SACP;EACA,MAAM,KAAK,SAAS,OAAO;EAFpB;EAGP,KAAK,OAAO;CACd;AACF;AAEA,IAAa,oBAAb,cAAuC,UAAU;CAC/C,AAAO,YACL,SACA,AAAO,SACP;EACA,MAAM,KAAK,SAAS,OAAO;EAFpB;EAGP,KAAK,OAAO;CACd;AACF;AAEA,IAAa,iBAAb,cAAoC,UAAU;CAC5C,AAAO,YACL,SACA,AAAO,SACP;EACA,MAAM,KAAK,SAAS,OAAO;EAFpB;EAGP,KAAK,OAAO;CACd;AACF;AAEA,IAAa,kBAAb,cAAqC,UAAU;CAC7C,AAAO,YACL,SACA,AAAO,SACP;EACA,MAAM,KAAK,SAAS,OAAO;EAFpB;EAGP,KAAK,OAAO;CACd;AACF;AAEA,IAAa,cAAb,cAAiC,UAAU;CACzC,AAAO,YACL,SACA,AAAO,SACP;EACA,MAAM,KAAK,SAAS,OAAO;EAFpB;EAGP,KAAK,OAAO;CACd;AACF;AAEA,IAAa,gBAAb,cAAmC,UAAU;CAC3C,AAAO,YACL,SACA,AAAO,SACP;EACA,MAAM,KAAK,SAAS,OAAO;EAFpB;EAGP,KAAK,OAAO;CACd;AACF;AAEA,IAAa,qBAAb,cAAwC,UAAU;CAChD,AAAO,YACL,SACA,AAAO,SACP;EACA,MAAM,KAAK,SAAS,OAAO;EAFpB;EAGP,KAAK,OAAO;CACd;AACF;AAEA,IAAa,kBAAb,cAAqC,UAAU;CAC7C,AAAO,YACL,SACA,AAAO,SACP;EACA,MAAM,KAAK,SAAS,OAAO;EAFpB;EAGP,KAAK,OAAO;CACd;AACF;;;;;;;;;;;;;;AAeA,IAAa,wBAAb,cAA2C,MAAM;CAC/C,AAAO,cAAc;EACnB,MACE,+HAEF;EACA,KAAK,OAAO;CACd;AACF"}
|
|
1
|
+
{"version":3,"file":"errors.mjs","names":[],"sources":["../../../../../../../../core/src/http/errors/errors.ts"],"sourcesContent":["export class HttpError extends Error {\n public constructor(\n public status: number,\n public message: string,\n public payload?: any,\n ) {\n super(message);\n this.name = \"HttpError\";\n }\n}\n\nexport class ResourceNotFoundError extends HttpError {\n public constructor(\n message: string,\n public payload?: any,\n ) {\n super(404, message, payload);\n this.name = \"ResourceNotFoundError\";\n }\n}\n\nexport class UnAuthorizedError extends HttpError {\n public constructor(\n message: string,\n public payload?: any,\n ) {\n super(401, message, payload);\n this.name = \"UnAuthorizedError\";\n }\n}\n\nexport class ForbiddenError extends HttpError {\n public constructor(\n message: string,\n public payload?: any,\n ) {\n super(403, message, payload);\n this.name = \"ForbiddenError\";\n }\n}\n\nexport class BadRequestError extends HttpError {\n public constructor(\n message: string,\n public payload?: any,\n ) {\n super(400, message, payload);\n this.name = \"BadRequestError\";\n }\n}\n\nexport class ServerError extends HttpError {\n public constructor(\n message: string,\n public payload?: any,\n ) {\n super(500, message, payload);\n this.name = \"ServerError\";\n }\n}\n\nexport class ConflictError extends HttpError {\n public constructor(\n message: string,\n public payload?: any,\n ) {\n super(409, message, payload);\n this.name = \"ConflictError\";\n }\n}\n\nexport class NotAcceptableError extends HttpError {\n public constructor(\n message: string,\n public payload?: any,\n ) {\n super(406, message, payload);\n this.name = \"NotAcceptableError\";\n }\n}\n\nexport class NotAllowedError extends HttpError {\n public constructor(\n message: string,\n public payload?: any,\n ) {\n super(405, message, payload);\n this.name = \"NotAllowedError\";\n }\n}\n\n/**\n * Thrown by `Request.prototype.cookie` / `Request.prototype.hasCookie` when\n * `@fastify/cookie` was never registered on this Fastify instance, so\n * `baseRequest.cookies` is `undefined` rather than an (possibly empty) object.\n *\n * A by-name cookie read is a deliberate assertion by the caller — \"this\n * cookie should be readable here\" — so an unavailable jar is a configuration\n * fault, not an absent cookie, and must not be swallowed into a default\n * value or a silent `false`. Contrast `Request.prototype.cookies`, which\n * stays lenient because the framework's own opportunistic reads (e.g.\n * locale resolution) must not throw on a request that simply has no jar.\n */\nexport class CookieJarUnavailableError extends Error {\n public constructor(cookieName: string) {\n super(\n `Cannot read cookie \"${cookieName}\": the cookie jar is unavailable because ` +\n \"@fastify/cookie is not registered on this Fastify instance. \" +\n \"Register the plugin (see core's http/plugins.ts) before reading cookies by name.\",\n );\n\n this.name = \"CookieJarUnavailableError\";\n }\n}\n\n/**\n * Thrown by `Request.prototype.user` in development to catch a call site\n * still reading the removed `request.user` getter/setter after the 5.12.0\n * move: the authenticated user now lives at `request.locals.user`, written\n * by `@warlock.js/auth`'s middleware.\n *\n * Development-only diagnostic, not a runtime contract other code should\n * catch — the getter itself is typed `never`, so a caller that still\n * compiles against `request.user` only does so via `any`/an outdated type.\n * Kept in core (not auth) because `request.user` is a core `Request`\n * accessor and core cannot depend on `@warlock.js/auth` to react to it.\n * Slated for removal one release after 5.12.0 — see the CHANGELOG.\n */\nexport class RequestUserMovedError extends Error {\n public constructor() {\n super(\n \"request.user has been removed. The authenticated user now lives at \" +\n \"request.locals.user, set by @warlock.js/auth's middleware.\",\n );\n this.name = \"RequestUserMovedError\";\n }\n}\n"],"mappings":";AAAA,IAAa,YAAb,cAA+B,MAAM;CACnC,AAAO,YACL,AAAO,QACP,AAAO,SACP,AAAO,SACP;EACA,MAAM,OAAO;EAJN;EACA;EACA;EAGP,KAAK,OAAO;CACd;AACF;AAEA,IAAa,wBAAb,cAA2C,UAAU;CACnD,AAAO,YACL,SACA,AAAO,SACP;EACA,MAAM,KAAK,SAAS,OAAO;EAFpB;EAGP,KAAK,OAAO;CACd;AACF;AAEA,IAAa,oBAAb,cAAuC,UAAU;CAC/C,AAAO,YACL,SACA,AAAO,SACP;EACA,MAAM,KAAK,SAAS,OAAO;EAFpB;EAGP,KAAK,OAAO;CACd;AACF;AAEA,IAAa,iBAAb,cAAoC,UAAU;CAC5C,AAAO,YACL,SACA,AAAO,SACP;EACA,MAAM,KAAK,SAAS,OAAO;EAFpB;EAGP,KAAK,OAAO;CACd;AACF;AAEA,IAAa,kBAAb,cAAqC,UAAU;CAC7C,AAAO,YACL,SACA,AAAO,SACP;EACA,MAAM,KAAK,SAAS,OAAO;EAFpB;EAGP,KAAK,OAAO;CACd;AACF;AAEA,IAAa,cAAb,cAAiC,UAAU;CACzC,AAAO,YACL,SACA,AAAO,SACP;EACA,MAAM,KAAK,SAAS,OAAO;EAFpB;EAGP,KAAK,OAAO;CACd;AACF;AAEA,IAAa,gBAAb,cAAmC,UAAU;CAC3C,AAAO,YACL,SACA,AAAO,SACP;EACA,MAAM,KAAK,SAAS,OAAO;EAFpB;EAGP,KAAK,OAAO;CACd;AACF;AAEA,IAAa,qBAAb,cAAwC,UAAU;CAChD,AAAO,YACL,SACA,AAAO,SACP;EACA,MAAM,KAAK,SAAS,OAAO;EAFpB;EAGP,KAAK,OAAO;CACd;AACF;AAEA,IAAa,kBAAb,cAAqC,UAAU;CAC7C,AAAO,YACL,SACA,AAAO,SACP;EACA,MAAM,KAAK,SAAS,OAAO;EAFpB;EAGP,KAAK,OAAO;CACd;AACF;;;;;;;;;;;;;AAcA,IAAa,4BAAb,cAA+C,MAAM;CACnD,AAAO,YAAY,YAAoB;EACrC,MACE,uBAAuB,WAAW,sLAGpC;EAEA,KAAK,OAAO;CACd;AACF;;;;;;;;;;;;;;AAeA,IAAa,wBAAb,cAA2C,MAAM;CAC/C,AAAO,cAAc;EACnB,MACE,+HAEF;EACA,KAAK,OAAO;CACd;AACF"}
|
package/esm/http/index.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PipeableReactStream, StreamReactResponseOptions, streamReactResponse } from "./stream-react-response.mjs";
|
|
2
|
+
import { XMLable } from "./xmlable.mjs";
|
|
2
3
|
import { CookieOptions, Response, ResponseStatus, SendBufferOptions, SendFileOptions } from "./response.mjs";
|
|
3
4
|
import { FileNamingStrategy, ImageTransformCallback, ImageTransformConfig, PrefixConfig, PrefixOptions, SaveAsOptions, SaveOptions, UploadedFileImageOptions, UploadsConfigurations } from "./uploads-types.mjs";
|
|
4
5
|
import { FileValidationOptions, UploadedFile, UploadedFileJson } from "./uploaded-file.mjs";
|
|
@@ -14,7 +15,7 @@ import { logResponse, wrapResponseInDataKey } from "./events.mjs";
|
|
|
14
15
|
import { HealthCheck, HealthStatus, health } from "./health.mjs";
|
|
15
16
|
import { RequestController } from "./request-controller.mjs";
|
|
16
17
|
import { UPLOADS_DEFAULTS, uploadsConfig } from "./uploads-config.mjs";
|
|
17
|
-
import { BadRequestError, ConflictError, ForbiddenError, HttpError, NotAcceptableError, NotAllowedError, RequestUserMovedError, ResourceNotFoundError, ServerError, UnAuthorizedError } from "./errors/errors.mjs";
|
|
18
|
+
import { BadRequestError, ConflictError, CookieJarUnavailableError, ForbiddenError, HttpError, NotAcceptableError, NotAllowedError, RequestUserMovedError, ResourceNotFoundError, ServerError, UnAuthorizedError } from "./errors/errors.mjs";
|
|
18
19
|
import { CacheMiddlewareOptions } from "./middleware/cache-response-middleware.mjs";
|
|
19
20
|
import { ConcurrencyLimitOptions } from "./middleware/concurrency-limit.middleware.mjs";
|
|
20
21
|
import { IdempotencyOptions } from "./middleware/idempotency.middleware.mjs";
|
package/esm/http/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { requestContext, useCurrentUser, useRequest, useRequestStore } from "./context/request-context.mjs";
|
|
2
2
|
import { DEFAULT_CSP_DIRECTIVES, InvalidCspDirectiveError, applyCspHeader, buildCspHeaderValue, mergeCspDirectives, resolveCspConfig, serializeCspDirectives, validateCspConfigAtBoot, validateCspDirectives } from "./csp.mjs";
|
|
3
|
-
import { BadRequestError, ConflictError, ForbiddenError, HttpError, NotAcceptableError, NotAllowedError, RequestUserMovedError, ResourceNotFoundError, ServerError, UnAuthorizedError } from "./errors/errors.mjs";
|
|
3
|
+
import { BadRequestError, ConflictError, CookieJarUnavailableError, ForbiddenError, HttpError, NotAcceptableError, NotAllowedError, RequestUserMovedError, ResourceNotFoundError, ServerError, UnAuthorizedError } from "./errors/errors.mjs";
|
|
4
4
|
import { deriveTraceId, parseTraceparentTraceId } from "./tracing/trace-id.mjs";
|
|
5
5
|
import { buildTracingContext, dispatchPhase, dispatchRequestEnd, dispatchRequestStart, isTracingEnabled, resetTracingConfigForTests, resolveTracingConfig } from "./tracing/tracing-dispatcher.mjs";
|
|
6
6
|
import "./tracing/index.mjs";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region ../core/src/http/parse-urlencoded-body.ts
|
|
2
|
+
/**
|
|
3
|
+
* Decode an `application/x-www-form-urlencoded` request body into a plain
|
|
4
|
+
* object, the shape `request.ts`'s `parseBody` already expects from JSON and
|
|
5
|
+
* multipart bodies.
|
|
6
|
+
*
|
|
7
|
+
* Repeated keys become arrays (`code=a&code=b` -> `{ code: ["a", "b"] }`);
|
|
8
|
+
* every other key stays a single string. Bracket-notation keys (`a[b]=1`) are
|
|
9
|
+
* NOT expanded here — they are handed through as literal keys and, like any
|
|
10
|
+
* JSON or query-string key of that shape, are expanded later by the shared
|
|
11
|
+
* `parseBody` nesting logic in `request.ts`. This function's only job is the
|
|
12
|
+
* urlencoded -> flat-object step; it does not duplicate that logic.
|
|
13
|
+
*/
|
|
14
|
+
function parseUrlencodedBody(raw) {
|
|
15
|
+
const params = new URLSearchParams(raw);
|
|
16
|
+
const body = {};
|
|
17
|
+
for (const key of params.keys()) {
|
|
18
|
+
if (Object.prototype.hasOwnProperty.call(body, key)) continue;
|
|
19
|
+
const values = params.getAll(key);
|
|
20
|
+
body[key] = values.length > 1 ? values : values[0] ?? "";
|
|
21
|
+
}
|
|
22
|
+
return body;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
//#endregion
|
|
26
|
+
export { parseUrlencodedBody };
|
|
27
|
+
//# sourceMappingURL=parse-urlencoded-body.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-urlencoded-body.mjs","names":[],"sources":["../../../../../../../core/src/http/parse-urlencoded-body.ts"],"sourcesContent":["/**\n * Decode an `application/x-www-form-urlencoded` request body into a plain\n * object, the shape `request.ts`'s `parseBody` already expects from JSON and\n * multipart bodies.\n *\n * Repeated keys become arrays (`code=a&code=b` -> `{ code: [\"a\", \"b\"] }`);\n * every other key stays a single string. Bracket-notation keys (`a[b]=1`) are\n * NOT expanded here — they are handed through as literal keys and, like any\n * JSON or query-string key of that shape, are expanded later by the shared\n * `parseBody` nesting logic in `request.ts`. This function's only job is the\n * urlencoded -> flat-object step; it does not duplicate that logic.\n */\nexport function parseUrlencodedBody(raw: string): Record<string, string | string[]> {\n const params = new URLSearchParams(raw);\n const body: Record<string, string | string[]> = {};\n\n for (const key of params.keys()) {\n if (Object.prototype.hasOwnProperty.call(body, key)) continue;\n\n const values = params.getAll(key);\n\n body[key] = values.length > 1 ? values : (values[0] ?? \"\");\n }\n\n return body;\n}\n"],"mappings":";;;;;;;;;;;;;AAYA,SAAgB,oBAAoB,KAAgD;CAClF,MAAM,SAAS,IAAI,gBAAgB,GAAG;CACtC,MAAM,OAA0C,CAAC;CAEjD,KAAK,MAAM,OAAO,OAAO,KAAK,GAAG;EAC/B,IAAI,OAAO,UAAU,eAAe,KAAK,MAAM,GAAG,GAAG;EAErD,MAAM,SAAS,OAAO,OAAO,GAAG;EAEhC,KAAK,OAAO,OAAO,SAAS,IAAI,SAAU,OAAO,MAAM;CACzD;CAEA,OAAO;AACT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugins.d.mts","names":[],"sources":["../../../../../../../core/src/http/plugins.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"plugins.d.mts","names":[],"sources":["../../../../../../../core/src/http/plugins.ts"],"mappings":";;;iBAQsB,mBAAA,CAAoB,MAAA,EAAQ,eAAA,GAAe,OAAA"}
|
package/esm/http/plugins.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { rootPath } from "../utils/paths.mjs";
|
|
2
2
|
import "../utils/index.mjs";
|
|
3
3
|
import { buildCorsOptions } from "./build-cors-options.mjs";
|
|
4
|
+
import { parseUrlencodedBody } from "./parse-urlencoded-body.mjs";
|
|
4
5
|
import config from "@mongez/config";
|
|
5
6
|
import fastifyMultipart from "@fastify/multipart";
|
|
6
7
|
|
|
@@ -15,6 +16,13 @@ async function registerHttpPlugins(server) {
|
|
|
15
16
|
attachFieldsToBody: true,
|
|
16
17
|
limits: { fileSize: config.get("http.fileUploadLimit", 10 * 1024 * 1024) }
|
|
17
18
|
});
|
|
19
|
+
server.addContentTypeParser("application/x-www-form-urlencoded", { parseAs: "string" }, (_request, body, done) => {
|
|
20
|
+
try {
|
|
21
|
+
done(null, parseUrlencodedBody(body));
|
|
22
|
+
} catch (error) {
|
|
23
|
+
done(error, void 0);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
18
26
|
server.register(import("@fastify/static"), {
|
|
19
27
|
root: config.get("storage.publicRoot", rootPath("public")),
|
|
20
28
|
prefix: config.get("storage.publicPrefix", "/public/")
|
package/esm/http/plugins.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"plugins.mjs","names":[],"sources":["../../../../../../../core/src/http/plugins.ts"],"sourcesContent":["import fastifyMultipart from \"@fastify/multipart\";\nimport config from \"@mongez/config\";\nimport { rootPath } from \"../utils\";\nimport { buildCorsOptions } from \"./build-cors-options\";\nimport type { FastifyInstance } from \"./server\";\n\nexport async function registerHttpPlugins(server: FastifyInstance) {\n // 👇🏻 register rate-limit plugin\n server.register(import(\"@fastify/rate-limit\"), {\n // max requests per time window\n max: config.get(\"http.rateLimit.max\", 60),\n // maximum time that is will allow max requests\n timeWindow: config.get(\"http.rateLimit.duration\", 60 * 1000),\n });\n\n // 👇🏻 register cors plugin\n server.register(import(\"@fastify/cors\"), buildCorsOptions());\n\n // 👇🏻 import multipart plugin\n server.register(fastifyMultipart, {\n attachFieldsToBody: true,\n limits: {\n // file size could be up to 10MB\n fileSize: config.get(\"http.fileUploadLimit\", 10 * 1024 * 1024),\n },\n });\n\n server.register(import(\"@fastify/static\"), {\n root: config.get(\"storage.publicRoot\", rootPath(\"public\")),\n prefix: config.get(\"storage.publicPrefix\", \"/public/\"),\n });\n\n // 👇🏻 register cookie plugin\n server.register(import(\"@fastify/cookie\"), {\n secret: config.get(\"http.cookies.secret\"), // Optional: allow signed cookies\n parseOptions: config.get(\"http.cookies.options\", {}),\n });\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"plugins.mjs","names":[],"sources":["../../../../../../../core/src/http/plugins.ts"],"sourcesContent":["import fastifyMultipart from \"@fastify/multipart\";\nimport config from \"@mongez/config\";\nimport type { FastifyRequest } from \"fastify\";\nimport { rootPath } from \"../utils\";\nimport { buildCorsOptions } from \"./build-cors-options\";\nimport { parseUrlencodedBody } from \"./parse-urlencoded-body\";\nimport type { FastifyInstance } from \"./server\";\n\nexport async function registerHttpPlugins(server: FastifyInstance) {\n // 👇🏻 register rate-limit plugin\n server.register(import(\"@fastify/rate-limit\"), {\n // max requests per time window\n max: config.get(\"http.rateLimit.max\", 60),\n // maximum time that is will allow max requests\n timeWindow: config.get(\"http.rateLimit.duration\", 60 * 1000),\n });\n\n // 👇🏻 register cors plugin\n server.register(import(\"@fastify/cors\"), buildCorsOptions());\n\n // 👇🏻 import multipart plugin\n server.register(fastifyMultipart, {\n attachFieldsToBody: true,\n limits: {\n // file size could be up to 10MB\n fileSize: config.get(\"http.fileUploadLimit\", 10 * 1024 * 1024),\n },\n });\n\n // 👇🏻 parse application/x-www-form-urlencoded bodies (Apple's OAuth\n // `form_post` callback, plain HTML forms). Fastify only parses JSON and\n // text natively, so without this every urlencoded POST is rejected with\n // FST_ERR_CTP_INVALID_MEDIA_TYPE before it reaches a route.\n //\n // No `bodyLimit` option here: Fastify falls back to the server's own\n // `bodyLimit` (`server.ts`, from `http.bodyLimit`) whenever a content-type\n // parser doesn't set one — the exact limit JSON bodies are already held to.\n // Reading `http.bodyLimit` again here would duplicate that lookup and risk\n // the two silently drifting.\n server.addContentTypeParser(\n \"application/x-www-form-urlencoded\",\n { parseAs: \"string\" },\n (_request: FastifyRequest, body: string, done: (err: Error | null, body?: any) => void) => {\n try {\n done(null, parseUrlencodedBody(body));\n } catch (error) {\n done(error as Error, undefined);\n }\n },\n );\n\n server.register(import(\"@fastify/static\"), {\n root: config.get(\"storage.publicRoot\", rootPath(\"public\")),\n prefix: config.get(\"storage.publicPrefix\", \"/public/\"),\n });\n\n // 👇🏻 register cookie plugin\n server.register(import(\"@fastify/cookie\"), {\n secret: config.get(\"http.cookies.secret\"), // Optional: allow signed cookies\n parseOptions: config.get(\"http.cookies.options\", {}),\n });\n}\n"],"mappings":";;;;;;;;AAQA,eAAsB,oBAAoB,QAAyB;CAEjE,OAAO,SAAS,OAAO,wBAAwB;EAE7C,KAAK,OAAO,IAAI,sBAAsB,EAAE;EAExC,YAAY,OAAO,IAAI,2BAA2B,KAAK,GAAI;CAC7D,CAAC;CAGD,OAAO,SAAS,OAAO,kBAAkB,iBAAiB,CAAC;CAG3D,OAAO,SAAS,kBAAkB;EAChC,oBAAoB;EACpB,QAAQ,EAEN,UAAU,OAAO,IAAI,wBAAwB,KAAK,OAAO,IAAI,EAC/D;CACF,CAAC;CAYD,OAAO,qBACL,qCACA,EAAE,SAAS,SAAS,IACnB,UAA0B,MAAc,SAAkD;EACzF,IAAI;GACF,KAAK,MAAM,oBAAoB,IAAI,CAAC;EACtC,SAAS,OAAO;GACd,KAAK,OAAgB,MAAS;EAChC;CACF,CACF;CAEA,OAAO,SAAS,OAAO,oBAAoB;EACzC,MAAM,OAAO,IAAI,sBAAsB,SAAS,QAAQ,CAAC;EACzD,QAAQ,OAAO,IAAI,wBAAwB,UAAU;CACvD,CAAC;CAGD,OAAO,SAAS,OAAO,oBAAoB;EACzC,QAAQ,OAAO,IAAI,qBAAqB;EACxC,cAAc,OAAO,IAAI,wBAAwB,CAAC,CAAC;CACrD,CAAC;AACH"}
|