create-warlock 5.16.0 → 5.17.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 +66 -0
- package/esm/commands/create-new-app/index.mjs +55 -50
- package/esm/commands/create-new-app/index.mjs.map +1 -1
- package/esm/commands/create-new-app/types.d.mts +1 -1
- package/esm/commands/create-warlock-app/index.mjs +2 -2
- package/esm/commands/create-warlock-app/index.mjs.map +1 -1
- package/esm/features/agent-kit-targets.mjs +37 -6
- package/esm/features/agent-kit-targets.mjs.map +1 -1
- package/esm/flags/assert-flag-combinations.mjs +31 -0
- package/esm/flags/assert-flag-combinations.mjs.map +1 -0
- package/esm/flags/assert-package-manager.mjs +44 -0
- package/esm/flags/assert-package-manager.mjs.map +1 -0
- package/esm/flags/definitions.mjs +7 -4
- package/esm/flags/definitions.mjs.map +1 -1
- package/esm/flags/seed-from-flags.mjs +28 -0
- package/esm/flags/seed-from-flags.mjs.map +1 -0
- package/esm/helpers/app.mjs +5 -2
- package/esm/helpers/app.mjs.map +1 -1
- package/esm/helpers/insert-import.mjs.map +1 -1
- package/esm/helpers/package-manager.mjs +29 -19
- package/esm/helpers/package-manager.mjs.map +1 -1
- package/esm/helpers/project-builder-helpers.mjs +1 -1
- package/esm/index.d.mts +1 -1
- package/esm/index.mjs +2 -2
- package/esm/index.mjs.map +1 -1
- package/esm/prompts/ask-agent-targets.mjs +87 -0
- package/esm/prompts/ask-agent-targets.mjs.map +1 -0
- package/esm/prompts/ask-project-name.mjs +25 -0
- package/esm/prompts/ask-project-name.mjs.map +1 -0
- package/esm/prompts/ask-stack.mjs +22 -9
- package/esm/prompts/ask-stack.mjs.map +1 -1
- package/esm/summary.mjs.map +1 -1
- package/llms-full.txt +1 -1
- package/package.json +2 -2
- package/skills/create-a-warlock-project/SKILL.md +1 -1
- package/templates/warlock/AGENTS.md +1 -0
- package/templates/warlock/package.json +3 -3
- package/templates/warlock/skills/web-code-standards/SKILL.md +266 -0
- package/templates/warlock/src/app/uploads/routes.ts +7 -3
- package/templates/warlock/src/config/cache.ts +25 -27
- package/templates/warlock/src/config/http.ts +17 -0
- package/templates/warlock/src/web/404.page.tsx +5 -3
- package/templates/warlock/src/web/home/index.page.tsx +10 -7
- package/templates/warlock/src/web/root.tsx +6 -3
- package/templates/warlock/src/app/uploads/controllers/fetch-uploaded-file.controller.ts +0 -33
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { BUILTIN_AGENT_KIT_TARGETS, DEFAULT_AGENT_KIT_TARGET, getValidAgentKitTargets } from "../features/agent-kit-targets.mjs";
|
|
2
|
+
import { cancel, isCancel, multiselect } from "@clack/prompts";
|
|
3
|
+
|
|
4
|
+
//#region ../create-warlock/src/prompts/ask-agent-targets.ts
|
|
5
|
+
/**
|
|
6
|
+
* Which `agent-kit` targets to derive per-agent docs and skills for.
|
|
7
|
+
*
|
|
8
|
+
* The valid target list is not a literal we own — it comes from the installed
|
|
9
|
+
* `@mongez/agent-kit`, or a cached network fetch, and
|
|
10
|
+
* {@link getValidAgentKitTargets} THROWS when neither is available. That is
|
|
11
|
+
* the right behaviour for `--agents=something-unusual`, where guessing would
|
|
12
|
+
* be worse than failing. It is the wrong behaviour here: this prompt sits
|
|
13
|
+
* five questions into the wizard, and an offline laptop must not lose four
|
|
14
|
+
* already-given answers to a failed HTTP request.
|
|
15
|
+
*
|
|
16
|
+
* So an unreachable list is not fatal — it narrows the menu to the built-in
|
|
17
|
+
* targets and SAYS SO, rather than either crashing or silently pretending
|
|
18
|
+
* that is the whole list.
|
|
19
|
+
*/
|
|
20
|
+
async function askAgentTargets(requested) {
|
|
21
|
+
const { targets: known, complete } = await availableTargets();
|
|
22
|
+
const targets = reconcileRequested(requested, known, complete);
|
|
23
|
+
const answer = await multiselect({
|
|
24
|
+
message: complete ? "Which coding agents should this project be set up for?" : "Which coding agents should this project be set up for? (offline — showing built-in targets only)",
|
|
25
|
+
options: targets.map((target) => ({
|
|
26
|
+
value: target,
|
|
27
|
+
label: target
|
|
28
|
+
})),
|
|
29
|
+
initialValues: initialSelection(requested, targets),
|
|
30
|
+
required: false
|
|
31
|
+
});
|
|
32
|
+
if (isCancel(answer)) {
|
|
33
|
+
cancel("Agent selection cancelled");
|
|
34
|
+
process.exit(0);
|
|
35
|
+
}
|
|
36
|
+
const selected = answer;
|
|
37
|
+
return selected.length > 0 ? selected : [DEFAULT_AGENT_KIT_TARGET];
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The target list, plus whether it is the REAL one. `complete: false` means
|
|
41
|
+
* the lookup failed and these are only the offline built-ins.
|
|
42
|
+
*/
|
|
43
|
+
async function availableTargets() {
|
|
44
|
+
try {
|
|
45
|
+
return {
|
|
46
|
+
targets: await getValidAgentKitTargets(),
|
|
47
|
+
complete: true
|
|
48
|
+
};
|
|
49
|
+
} catch {
|
|
50
|
+
return {
|
|
51
|
+
targets: [...BUILTIN_AGENT_KIT_TARGETS],
|
|
52
|
+
complete: false
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* What `--agents` asked for has to end up among the OPTIONS, or it would be
|
|
58
|
+
* pre-ticked invisibly and then silently dropped — the same defect this
|
|
59
|
+
* wizard's other flags already had.
|
|
60
|
+
*
|
|
61
|
+
* With a complete list, an unrecognised target is refused, naming the valid
|
|
62
|
+
* ones, exactly as `resolveAgentTargets` does for the non-interactive path.
|
|
63
|
+
* With an incomplete one, we have no authority to call anything invalid, so
|
|
64
|
+
* the requested targets are offered alongside the built-ins instead of being
|
|
65
|
+
* judged.
|
|
66
|
+
*/
|
|
67
|
+
function reconcileRequested(requested, known, complete) {
|
|
68
|
+
if (!requested || requested.length === 0) return known;
|
|
69
|
+
const unknown = requested.filter((target) => !known.includes(target));
|
|
70
|
+
if (unknown.length === 0) return known;
|
|
71
|
+
if (complete) {
|
|
72
|
+
cancel(`Unknown agent-kit target(s): ${unknown.join(", ")} — valid targets: ${known.slice().sort().join(", ")}`);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
return [...known, ...unknown];
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Pre-tick what `--agents` asked for. Everything it names is among the
|
|
79
|
+
* options by now, so nothing is filtered away here.
|
|
80
|
+
*/
|
|
81
|
+
function initialSelection(requested, targets) {
|
|
82
|
+
return (requested && requested.length > 0 ? requested : [DEFAULT_AGENT_KIT_TARGET]).filter((target) => targets.includes(target));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
//#endregion
|
|
86
|
+
export { askAgentTargets };
|
|
87
|
+
//# sourceMappingURL=ask-agent-targets.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ask-agent-targets.mjs","names":[],"sources":["../../../../../../create-warlock/src/prompts/ask-agent-targets.ts"],"sourcesContent":["import { cancel, isCancel, multiselect } from \"@clack/prompts\";\nimport {\n BUILTIN_AGENT_KIT_TARGETS,\n DEFAULT_AGENT_KIT_TARGET,\n getValidAgentKitTargets,\n} from \"../features/agent-kit-targets\";\n\n/**\n * Which `agent-kit` targets to derive per-agent docs and skills for.\n *\n * The valid target list is not a literal we own — it comes from the installed\n * `@mongez/agent-kit`, or a cached network fetch, and\n * {@link getValidAgentKitTargets} THROWS when neither is available. That is\n * the right behaviour for `--agents=something-unusual`, where guessing would\n * be worse than failing. It is the wrong behaviour here: this prompt sits\n * five questions into the wizard, and an offline laptop must not lose four\n * already-given answers to a failed HTTP request.\n *\n * So an unreachable list is not fatal — it narrows the menu to the built-in\n * targets and SAYS SO, rather than either crashing or silently pretending\n * that is the whole list.\n */\nexport async function askAgentTargets(\n requested: string[] | undefined,\n): Promise<string[]> {\n const { targets: known, complete } = await availableTargets();\n const targets = reconcileRequested(requested, known, complete);\n\n const answer = await multiselect({\n message: complete\n ? \"Which coding agents should this project be set up for?\"\n : \"Which coding agents should this project be set up for? (offline — showing built-in targets only)\",\n options: targets.map(target => ({ value: target, label: target })),\n initialValues: initialSelection(requested, targets),\n required: false,\n });\n\n if (isCancel(answer)) {\n cancel(\"Agent selection cancelled\");\n process.exit(0);\n }\n\n const selected = answer as string[];\n\n return selected.length > 0 ? selected : [DEFAULT_AGENT_KIT_TARGET];\n}\n\n/**\n * The target list, plus whether it is the REAL one. `complete: false` means\n * the lookup failed and these are only the offline built-ins.\n */\nasync function availableTargets(): Promise<{\n targets: string[];\n complete: boolean;\n}> {\n try {\n return { targets: await getValidAgentKitTargets(), complete: true };\n } catch {\n return { targets: [...BUILTIN_AGENT_KIT_TARGETS], complete: false };\n }\n}\n\n/**\n * What `--agents` asked for has to end up among the OPTIONS, or it would be\n * pre-ticked invisibly and then silently dropped — the same defect this\n * wizard's other flags already had.\n *\n * With a complete list, an unrecognised target is refused, naming the valid\n * ones, exactly as `resolveAgentTargets` does for the non-interactive path.\n * With an incomplete one, we have no authority to call anything invalid, so\n * the requested targets are offered alongside the built-ins instead of being\n * judged.\n */\nfunction reconcileRequested(\n requested: string[] | undefined,\n known: string[],\n complete: boolean,\n): string[] {\n if (!requested || requested.length === 0) return known;\n\n const unknown = requested.filter(target => !known.includes(target));\n\n if (unknown.length === 0) return known;\n\n if (complete) {\n cancel(\n `Unknown agent-kit target(s): ${unknown.join(\", \")} — valid targets: ${known\n .slice()\n .sort()\n .join(\", \")}`,\n );\n process.exit(1);\n }\n\n return [...known, ...unknown];\n}\n\n/**\n * Pre-tick what `--agents` asked for. Everything it names is among the\n * options by now, so nothing is filtered away here.\n */\nfunction initialSelection(\n requested: string[] | undefined,\n targets: string[],\n): string[] {\n const wanted =\n requested && requested.length > 0 ? requested : [DEFAULT_AGENT_KIT_TARGET];\n\n return wanted.filter(target => targets.includes(target));\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAsBA,eAAsB,gBACpB,WACmB;CACnB,MAAM,EAAE,SAAS,OAAO,aAAa,MAAM,iBAAiB;CAC5D,MAAM,UAAU,mBAAmB,WAAW,OAAO,QAAQ;CAE7D,MAAM,SAAS,MAAM,YAAY;EAC/B,SAAS,WACL,2DACA;EACJ,SAAS,QAAQ,KAAI,YAAW;GAAE,OAAO;GAAQ,OAAO;EAAO,EAAE;EACjE,eAAe,iBAAiB,WAAW,OAAO;EAClD,UAAU;CACZ,CAAC;CAED,IAAI,SAAS,MAAM,GAAG;EACpB,OAAO,2BAA2B;EAClC,QAAQ,KAAK,CAAC;CAChB;CAEA,MAAM,WAAW;CAEjB,OAAO,SAAS,SAAS,IAAI,WAAW,CAAC,wBAAwB;AACnE;;;;;AAMA,eAAe,mBAGZ;CACD,IAAI;EACF,OAAO;GAAE,SAAS,MAAM,wBAAwB;GAAG,UAAU;EAAK;CACpE,QAAQ;EACN,OAAO;GAAE,SAAS,CAAC,GAAG,yBAAyB;GAAG,UAAU;EAAM;CACpE;AACF;;;;;;;;;;;;AAaA,SAAS,mBACP,WACA,OACA,UACU;CACV,IAAI,CAAC,aAAa,UAAU,WAAW,GAAG,OAAO;CAEjD,MAAM,UAAU,UAAU,QAAO,WAAU,CAAC,MAAM,SAAS,MAAM,CAAC;CAElE,IAAI,QAAQ,WAAW,GAAG,OAAO;CAEjC,IAAI,UAAU;EACZ,OACE,gCAAgC,QAAQ,KAAK,IAAI,EAAE,oBAAoB,MACpE,MAAM,CAAC,CACP,KAAK,CAAC,CACN,KAAK,IAAI,GACd;EACA,QAAQ,KAAK,CAAC;CAChB;CAEA,OAAO,CAAC,GAAG,OAAO,GAAG,OAAO;AAC9B;;;;;AAMA,SAAS,iBACP,WACA,SACU;CAIV,QAFE,aAAa,UAAU,SAAS,IAAI,YAAY,CAAC,wBAAwB,EAE9D,CAAC,QAAO,WAAU,QAAQ,SAAS,MAAM,CAAC;AACzD"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { cancel, isCancel, text } from "@clack/prompts";
|
|
2
|
+
|
|
3
|
+
//#region ../create-warlock/src/prompts/ask-project-name.ts
|
|
4
|
+
/**
|
|
5
|
+
* The project name is the one answer with no sane default, so every
|
|
6
|
+
* interactive path has to ask it. It lives here so the default path and the
|
|
7
|
+
* full wizard ask it with the SAME wording and the same cancel handling —
|
|
8
|
+
* and so a name already collected by one path can simply be handed to the
|
|
9
|
+
* other instead of being asked a second time.
|
|
10
|
+
*/
|
|
11
|
+
async function askProjectName() {
|
|
12
|
+
const answer = await text({
|
|
13
|
+
message: "What shall we call your project?",
|
|
14
|
+
placeholder: "my-warlock-app"
|
|
15
|
+
});
|
|
16
|
+
if (isCancel(answer) || !answer.trim()) {
|
|
17
|
+
cancel("A project name is required to continue");
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
20
|
+
return answer;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
//#endregion
|
|
24
|
+
export { askProjectName };
|
|
25
|
+
//# sourceMappingURL=ask-project-name.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ask-project-name.mjs","names":[],"sources":["../../../../../../create-warlock/src/prompts/ask-project-name.ts"],"sourcesContent":["import { cancel, isCancel, text } from \"@clack/prompts\";\n\n/**\n * The project name is the one answer with no sane default, so every\n * interactive path has to ask it. It lives here so the default path and the\n * full wizard ask it with the SAME wording and the same cancel handling —\n * and so a name already collected by one path can simply be handed to the\n * other instead of being asked a second time.\n */\nexport async function askProjectName(): Promise<string> {\n const answer = await text({\n message: \"What shall we call your project?\",\n placeholder: \"my-warlock-app\",\n });\n\n if (isCancel(answer) || !answer.trim()) {\n cancel(\"A project name is required to continue\");\n process.exit(0);\n }\n\n return answer;\n}\n"],"mappings":";;;;;;;;;;AASA,eAAsB,iBAAkC;CACtD,MAAM,SAAS,MAAM,KAAK;EACxB,SAAS;EACT,aAAa;CACf,CAAC;CAED,IAAI,SAAS,MAAM,KAAK,CAAC,OAAO,KAAK,GAAG;EACtC,OAAO,wCAAwC;EAC/C,QAAQ,KAAK,CAAC;CAChB;CAEA,OAAO;AACT"}
|
|
@@ -8,19 +8,32 @@ import { cancel, isCancel, select } from "@clack/prompts";
|
|
|
8
8
|
* of the generated project — every other choice (database, features,
|
|
9
9
|
* package manager, agent-kit targets, …) is reversible within minutes and
|
|
10
10
|
* ships as a flag with a default instead.
|
|
11
|
+
*
|
|
12
|
+
* The third entry is not a third stack: `customize` selects the FLOW, handing
|
|
13
|
+
* the run to the full wizard (`--customize`) so every one of those reversible
|
|
14
|
+
* choices gets asked instead of defaulted. It sits in this menu because a flag
|
|
15
|
+
* nobody is told about is a feature nobody has.
|
|
11
16
|
*/
|
|
12
17
|
async function askStack() {
|
|
13
18
|
const answer = await select({
|
|
14
19
|
message: "What are we building?",
|
|
15
|
-
options: [
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
options: [
|
|
21
|
+
{
|
|
22
|
+
value: "api",
|
|
23
|
+
label: "API only",
|
|
24
|
+
hint: "A REST API, no server-rendered pages"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
value: "web",
|
|
28
|
+
label: "Full-stack web",
|
|
29
|
+
hint: "API + server-rendered React pages (SSR)"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
value: "customize",
|
|
33
|
+
label: "Customize",
|
|
34
|
+
hint: "Pick package manager, database, features, AI providers and coding agents one by one"
|
|
35
|
+
}
|
|
36
|
+
],
|
|
24
37
|
initialValue: "api"
|
|
25
38
|
});
|
|
26
39
|
if (isCancel(answer)) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ask-stack.mjs","names":[],"sources":["../../../../../../create-warlock/src/prompts/ask-stack.ts"],"sourcesContent":["import { cancel, isCancel, select } from \"@clack/prompts\";\nimport type {
|
|
1
|
+
{"version":3,"file":"ask-stack.mjs","names":[],"sources":["../../../../../../create-warlock/src/prompts/ask-stack.ts"],"sourcesContent":["import { cancel, isCancel, select } from \"@clack/prompts\";\nimport type { SetupChoice } from \"../commands/create-new-app/types\";\n\n/**\n * The ONE structural question the default (non-`--interactive`) interactive\n * path is allowed to ask: does the generated app need server-rendered web\n * pages, or is it an API only? This is the sole fork that changes the shape\n * of the generated project — every other choice (database, features,\n * package manager, agent-kit targets, …) is reversible within minutes and\n * ships as a flag with a default instead.\n *\n * The third entry is not a third stack: `customize` selects the FLOW, handing\n * the run to the full wizard (`--customize`) so every one of those reversible\n * choices gets asked instead of defaulted. It sits in this menu because a flag\n * nobody is told about is a feature nobody has.\n */\nexport async function askStack(): Promise<SetupChoice> {\n const answer = await select({\n message: \"What are we building?\",\n options: [\n {\n value: \"api\",\n label: \"API only\",\n hint: \"A REST API, no server-rendered pages\",\n },\n {\n value: \"web\",\n label: \"Full-stack web\",\n hint: \"API + server-rendered React pages (SSR)\",\n },\n {\n value: \"customize\",\n label: \"Customize\",\n hint: \"Pick package manager, database, features, AI providers and coding agents one by one\",\n },\n ],\n initialValue: \"api\",\n });\n\n if (isCancel(answer)) {\n cancel(\"Setup cancelled\");\n process.exit(0);\n }\n\n return answer as SetupChoice;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAgBA,eAAsB,WAAiC;CACrD,MAAM,SAAS,MAAM,OAAO;EAC1B,SAAS;EACT,SAAS;GACP;IACE,OAAO;IACP,OAAO;IACP,MAAM;GACR;GACA;IACE,OAAO;IACP,OAAO;IACP,MAAM;GACR;GACA;IACE,OAAO;IACP,OAAO;IACP,MAAM;GACR;EACF;EACA,cAAc;CAChB,CAAC;CAED,IAAI,SAAS,MAAM,GAAG;EACpB,OAAO,iBAAiB;EACxB,QAAQ,KAAK,CAAC;CAChB;CAEA,OAAO;AACT"}
|
package/esm/summary.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"summary.mjs","names":[],"sources":["../../../../../create-warlock/src/summary.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport {
|
|
1
|
+
{"version":3,"file":"summary.mjs","names":[],"sources":["../../../../../create-warlock/src/summary.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\nimport { AppOptions } from \"./commands/create-new-app/types\";\nimport { getDatabaseLabel } from \"./features/database-drivers\";\n\n/** What decisions.spec.ts / the terminal actually needs to render the summary. */\nexport type Decisions = {\n packageManager: string;\n options: AppOptions;\n};\n\n/**\n * Print what the scaffolder decided on the user's behalf, and how to change\n * each decision — required because almost every choice is now a default\n * rather than a prompt, so this is the only place a user sees them.\n */\nexport function printDecisionsSummary({\n packageManager,\n options,\n}: Decisions): void {\n const databaseLabel = getDatabaseLabel(options.databaseDriver);\n\n const headline = [\n `Using ${packageManager}`,\n `database: ${databaseLabel}`,\n `agent-kit targets: ${options.agents.join(\", \")}`,\n ].join(\" · \");\n\n console.log();\n console.log(colors.cyan(headline));\n console.log(\n colors.dim(\n \" change these — package manager: --pm, database: --db/--no-db, agents: --agents, \" +\n \"features: --features, ai: --ai, git: --git/--no-git, jwt: --jwt/--no-jwt — \" +\n \"or re-run with --interactive for the full wizard. Package-manager-level choices \" +\n \"(husky, lint config, etc.) can also be edited directly in the generated project.\",\n ),\n );\n console.log();\n}\n"],"mappings":";;;;;;;;;AAeA,SAAgB,sBAAsB,EACpC,gBACA,WACkB;CAClB,MAAM,gBAAgB,iBAAiB,QAAQ,cAAc;CAE7D,MAAM,WAAW;EACf,SAAS;EACT,aAAa;EACb,sBAAsB,QAAQ,OAAO,KAAK,IAAI;CAChD,CAAC,CAAC,KAAK,KAAK;CAEZ,QAAQ,IAAI;CACZ,QAAQ,IAAI,OAAO,KAAK,QAAQ,CAAC;CACjC,QAAQ,IACN,OAAO,IACL,+TAIF,CACF;CACA,QAAQ,IAAI;AACd"}
|
package/llms-full.txt
CHANGED
|
@@ -136,7 +136,7 @@ my-app/
|
|
|
136
136
|
├─ auth/ # controllers, schema, services, requests, utils, OTP model + migration
|
|
137
137
|
├─ users/ # model, migration, resource, repository, create + list controllers, schema, events, seeds, commands
|
|
138
138
|
├─ posts/ # sample module — model, migration, resource, schema, create + update controllers
|
|
139
|
-
├─ uploads/ #
|
|
139
|
+
├─ uploads/ # routes.ts mounts core's uploadedFileController; named image variants go in src/config/uploads.ts
|
|
140
140
|
└─ shared/ # router/locale utils, home page, scheduler
|
|
141
141
|
```
|
|
142
142
|
|
package/package.json
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@clack/prompts": "^0.7.0",
|
|
14
14
|
"@mongez/copper": "^2.1.2",
|
|
15
|
-
"@warlock.js/fs": "5.
|
|
15
|
+
"@warlock.js/fs": "5.17.0",
|
|
16
16
|
"@mongez/reinforcements": "^4.0.1",
|
|
17
17
|
"cross-spawn": "^7.0.3",
|
|
18
18
|
"rimraf": "^6.0.1",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"engines": {
|
|
22
22
|
"node": ">=22.13"
|
|
23
23
|
},
|
|
24
|
-
"version": "5.
|
|
24
|
+
"version": "5.17.0",
|
|
25
25
|
"type": "module",
|
|
26
26
|
"main": "./esm/index.mjs",
|
|
27
27
|
"module": "./esm/index.mjs",
|
|
@@ -128,7 +128,7 @@ my-app/
|
|
|
128
128
|
├─ auth/ # controllers, schema, services, requests, utils, OTP model + migration
|
|
129
129
|
├─ users/ # model, migration, resource, repository, create + list controllers, schema, events, seeds, commands
|
|
130
130
|
├─ posts/ # sample module — model, migration, resource, schema, create + update controllers
|
|
131
|
-
├─ uploads/ #
|
|
131
|
+
├─ uploads/ # routes.ts mounts core's uploadedFileController; named image variants go in src/config/uploads.ts
|
|
132
132
|
└─ shared/ # router/locale utils, home page, scheduler
|
|
133
133
|
```
|
|
134
134
|
|
|
@@ -59,6 +59,7 @@ automatically on `postinstall`).
|
|
|
59
59
|
- **code-standards** — TypeScript style: `interface` (contracts) vs `type` (data),
|
|
60
60
|
access modifiers on every class member, no `any`, single-responsibility files
|
|
61
61
|
with the documented naming suffixes, JSDoc on the public surface.
|
|
62
|
+
- **web-code-standards** — Thin web pages, service-owned data, feature hooks, and browser-safe rendering.
|
|
62
63
|
- **data-and-persistence** — modeling, storing, and migrating data with
|
|
63
64
|
`@warlock.js/cascade` (money as integer minor units, migrations, indexes).
|
|
64
65
|
- **module-boundaries** — how modules under `src/app/**` relate; one domain noun
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "warlock dev",
|
|
8
|
-
"build": "tsc && warlock build",
|
|
8
|
+
"build": "warlock generate.typings && tsc && warlock build",
|
|
9
9
|
"start": "warlock start",
|
|
10
10
|
"seed": "warlock seed",
|
|
11
11
|
"migrate": "warlock migrate",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"serve": "yarn build && nohup warlock start > /dev/null 2>&1",
|
|
25
25
|
"lint": "npx eslint --fix ./src --max-warnings=0",
|
|
26
26
|
"format": "npx prettier --write ./src/**/*.{js,jsx,ts,tsx,css,md,json} --config ./.prettierrc.json",
|
|
27
|
-
"tsc": "npx tsc --noEmit",
|
|
27
|
+
"tsc": "warlock generate.typings && npx tsc --noEmit",
|
|
28
28
|
"postinstall": "agent-kit init && agent-kit sync || echo skills-sync-skipped",
|
|
29
29
|
"skills:sync": "agent-kit sync",
|
|
30
30
|
"skills:watch": "agent-kit watch",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@mongez/reinforcements": "^4.0.1",
|
|
35
|
-
"@mongez/localization": "^3.
|
|
35
|
+
"@mongez/localization": "^3.5.0",
|
|
36
36
|
"@mongez/supportive-is": "^2.1.4",
|
|
37
37
|
"@warlock.js/auth": "0.0.0-set-by-scaffolder",
|
|
38
38
|
"@warlock.js/cache": "0.0.0-set-by-scaffolder",
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: web-code-standards
|
|
3
|
+
description: >-
|
|
4
|
+
Strict separation-of-concerns standards for Warlock.js src/web pages and
|
|
5
|
+
components: loaders delegate data and business rules to app services;
|
|
6
|
+
components delegate all behavior to separate custom-hook files.
|
|
7
|
+
Use alongside code-standards for web work; skip for backend-only changes.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Web code standards
|
|
11
|
+
|
|
12
|
+
**Applies to:** `src/web/**` in this project. Read
|
|
13
|
+
[`code-standards`](../code-standards/SKILL.md) for the shared TypeScript rules,
|
|
14
|
+
and the relevant `@warlock.js/web` skill before using a framework feature.
|
|
15
|
+
This skill adds the web-specific rules; it does not override security or
|
|
16
|
+
module-boundary requirements. When reviewing existing code, identify debt
|
|
17
|
+
without expanding a requested edit into a wholesale refactor.
|
|
18
|
+
|
|
19
|
+
The bar is a page whose data flow and ownership a new teammate can explain
|
|
20
|
+
without tracing a large component, several implicit casts, and unrelated
|
|
21
|
+
network handlers. Good UI polish is not a substitute for that clarity.
|
|
22
|
+
|
|
23
|
+
## Feature directories
|
|
24
|
+
|
|
25
|
+
Keep files beside the web feature that owns them. For example, `src/web/posts/`
|
|
26
|
+
can own an archive, article route, reader controls, comments, and their hooks
|
|
27
|
+
and copy. `src/web/auth/` and `account/` own their flows; `src/web/admin/`
|
|
28
|
+
can own a guarded dashboard and its domain features. Root document, root
|
|
29
|
+
layout, error pages, home page, and shared site chrome stay at their common
|
|
30
|
+
ancestor.
|
|
31
|
+
|
|
32
|
+
When moving a page, check whether its URL is filesystem-derived. If it is,
|
|
33
|
+
moving the file changes the URL unless you first give `config` an explicit
|
|
34
|
+
`route`. Preserve any explicit `config.route.path` and `config.route.name`,
|
|
35
|
+
and keep protected pages under the layout that owns their server auth guard.
|
|
36
|
+
Place `locales.json` dictionaries with the feature they describe; site copy
|
|
37
|
+
belongs at the common physical source ancestor and admin-only copy belongs
|
|
38
|
+
under the admin layout. Warlock discovers these dictionaries automatically
|
|
39
|
+
and supplies the selected locale through request translation helpers and
|
|
40
|
+
`useTrans()`. An optional nonempty `$group` replaces the inferred static
|
|
41
|
+
directory namespace. Every flattened translation key must have one owner;
|
|
42
|
+
ancestor and child dictionaries cannot redefine the same key. Keep legacy
|
|
43
|
+
`register()` dictionaries only where global translations are still needed.
|
|
44
|
+
|
|
45
|
+
## 1. Keep the page contract, make the implementation thin
|
|
46
|
+
|
|
47
|
+
Warlock's file-routed modules have one policy export: `config`. A page's
|
|
48
|
+
`route`, `cache`, `middleware`, `validation`, `metadata`, and `sitemap` all
|
|
49
|
+
belong in that object; do not use legacy top-level policy exports. `loader`,
|
|
50
|
+
`register`, `ErrorBoundary`, and the default component remain top-level
|
|
51
|
+
exports. Layouts, registration files, `root.tsx`, `404.page.tsx`, and
|
|
52
|
+
`error.page.tsx` also have framework-defined roles. These are intentional
|
|
53
|
+
exceptions to the generic "one primary export per file" rule; do not split
|
|
54
|
+
them merely to satisfy it.
|
|
55
|
+
|
|
56
|
+
Use the config type that matches the module. A page can preserve loader data
|
|
57
|
+
inference for metadata by satisfying `PageConfig<typeof loader>`:
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
import type { PageConfig } from "@warlock.js/web";
|
|
61
|
+
|
|
62
|
+
export async function loader() {
|
|
63
|
+
return { title: "Posts" };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const config = {
|
|
67
|
+
route: { path: "/posts", name: "posts.index" },
|
|
68
|
+
middleware: [requireUser],
|
|
69
|
+
validation: { query: postsQuery },
|
|
70
|
+
metadata: ({ data }) => ({ title: data.title }),
|
|
71
|
+
sitemap: { changefreq: "daily" },
|
|
72
|
+
} satisfies PageConfig<typeof loader>;
|
|
73
|
+
|
|
74
|
+
export default function PostsPage() {
|
|
75
|
+
return <main>Posts</main>;
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
When code navigates by a page name, `href(name, params, query)` validates the
|
|
80
|
+
published route table at runtime. `href("posts.indxe")` is a string that
|
|
81
|
+
TypeScript accepts, then the helper throws with the known route names. Generated
|
|
82
|
+
route-name and parameter types are not available in 5.17.
|
|
83
|
+
|
|
84
|
+
Layouts use one `config satisfies LayoutConfig` object for `prefix`,
|
|
85
|
+
`middleware`, `metadata`, and a sitemap default. Roots use one
|
|
86
|
+
`config satisfies RootConfig` object for `middleware` and the static `strictMode`
|
|
87
|
+
flag: root modules do
|
|
88
|
+
not declare routes, prefixes, metadata, validation, cache, or sitemap policy.
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
import type { LayoutConfig, RootConfig } from "@warlock.js/web";
|
|
92
|
+
|
|
93
|
+
// src/web/account/layout.tsx
|
|
94
|
+
export const config = {
|
|
95
|
+
prefix: "/account",
|
|
96
|
+
middleware: [requireUser],
|
|
97
|
+
metadata: { robots: "noindex" },
|
|
98
|
+
sitemap: false,
|
|
99
|
+
} satisfies LayoutConfig;
|
|
100
|
+
|
|
101
|
+
// src/web/root.tsx
|
|
102
|
+
export const config = {
|
|
103
|
+
middleware: [loadRequestContext],
|
|
104
|
+
} satisfies RootConfig;
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
A page file declares its route contract, coordinates a controller-like loader,
|
|
108
|
+
and composes presentational components. The loader validates input, calls
|
|
109
|
+
named services under `src/app/<domain>/services/`, converts results through
|
|
110
|
+
their resources, and returns the page data. **Every model/repository read and
|
|
111
|
+
every business rule belongs in an app service, even when the loader would need
|
|
112
|
+
only one query.** Do not import models or repositories into page or layout
|
|
113
|
+
loaders. Keep service orchestration in the loader; keep business decisions,
|
|
114
|
+
queries, aggregations, and domain transformations in the service. A service
|
|
115
|
+
must have a meaningful domain name and own the operation, not exist as a
|
|
116
|
+
generic pass-through for a repository method.
|
|
117
|
+
|
|
118
|
+
Extract substantive view sections into cohesive components. A component
|
|
119
|
+
that needs any behavior gets a separate feature-named custom-hook file; a
|
|
120
|
+
component with no behavior stays presentational and needs no hook. Split by
|
|
121
|
+
responsibility, not by a mechanical line-count limit. Framework exports
|
|
122
|
+
remain in the page file even when their work is delegated elsewhere.
|
|
123
|
+
|
|
124
|
+
Preserve the framework's rendering boundary: `root.tsx` owns the document,
|
|
125
|
+
`#vessel` and `<Scripts />`; the loader runs on the server, while the rendered
|
|
126
|
+
component can run on the server **and** the client. Do not move request,
|
|
127
|
+
database, or secret access into a component or hook. If page-only exports
|
|
128
|
+
need server dependencies, use them only in server-side exports and follow the
|
|
129
|
+
web skill's import-graph rules. A new auth gate belongs in server middleware,
|
|
130
|
+
not a hidden link or a client-side redirect.
|
|
131
|
+
|
|
132
|
+
**Example split:** a post page's loader passes validated input to
|
|
133
|
+
`app/posts/services/get-public-post.service.ts` and returns its resource
|
|
134
|
+
output; the page composes `PostArticle`, `PostComments`, and `RelatedPosts`;
|
|
135
|
+
`use-post-comments.ts` owns subscription, state, and handlers for
|
|
136
|
+
`PostComments`. Each piece has one reason to change.
|
|
137
|
+
|
|
138
|
+
## 2. Make the server-to-browser data contract explicit
|
|
139
|
+
|
|
140
|
+
- Validate URL parameters and query input at the page boundary. Use the
|
|
141
|
+
framework's validated request rather than `request.input()` casts where
|
|
142
|
+
validation is available. A `page` parameter must affect a real bounded
|
|
143
|
+
query; accepting and returning it without paging is misleading.
|
|
144
|
+
- Have the app service query only what the view needs. Filter, sort, limit,
|
|
145
|
+
and paginate in the data source **before** serialization or rendering. Use
|
|
146
|
+
a count/aggregate query for totals instead of loading a table to count it.
|
|
147
|
+
Batch related reads; do not do one author lookup or image transformation
|
|
148
|
+
per row on an archive page. Cache does not make an unbounded cold path safe.
|
|
149
|
+
- Treat resources as the wire-format gate. Send the resource's deliberate
|
|
150
|
+
public shape, never a raw model, secret field, or request object to the
|
|
151
|
+
browser. Do not scatter `toJSON() as unknown as SomeRow` through pages:
|
|
152
|
+
fix or explicitly validate the resource contract at a single boundary so
|
|
153
|
+
refactors cannot silently invalidate loader data.
|
|
154
|
+
- Publicly cached pages must not contain viewer-specific state. Keep optional
|
|
155
|
+
identity in an appropriate request-scoped boundary and review cache keys,
|
|
156
|
+
invalidation, and private-page behaviour when the rendered data changes.
|
|
157
|
+
- A deferred key needs a meaningful loading state and an error boundary at
|
|
158
|
+
the consuming section. Keep essential page data non-deferred. Do not wrap
|
|
159
|
+
already-resolved markup in more Suspense boundaries merely to appear
|
|
160
|
+
optimized; measure a real hydration or streaming benefit first.
|
|
161
|
+
- Metadata, canonical links, and sitemap entries should derive from the
|
|
162
|
+
same validated, public data as the page. Keep private routes out of public
|
|
163
|
+
discovery, and do not generate database-backed assets on every GET.
|
|
164
|
+
|
|
165
|
+
## 3. Components render; custom hooks own all behavior
|
|
166
|
+
|
|
167
|
+
**No behavior in React component bodies, even when it is small.** A
|
|
168
|
+
component may declare props, call its dedicated custom hook if needed,
|
|
169
|
+
and render JSX from its props and the hook's returned values. Put `useState`,
|
|
170
|
+
`useEffect`, subscriptions, timers, browser API calls, derived-value
|
|
171
|
+
calculations, event handlers, mutations, and navigation decisions in a
|
|
172
|
+
separate custom-hook file. Keep pure domain calculations in `src/app`
|
|
173
|
+
services; a hook may call a small pure UI helper outside the component.
|
|
174
|
+
Do not put a one-line callback or inline computation in JSX because it
|
|
175
|
+
"doesn't warrant a hook". Pure presentational components need no hook.
|
|
176
|
+
JSX conditionals that only select which markup to show are presentation,
|
|
177
|
+
not a license to implement business rules in render.
|
|
178
|
+
|
|
179
|
+
Render must be deterministic for the same props and request data. Read
|
|
180
|
+
`window`, `document`, `localStorage`, browser clocks, and capabilities in
|
|
181
|
+
hook handlers or effects, not during server rendering or initial hydration.
|
|
182
|
+
Use request-provided data for the first client frame; do not silently render
|
|
183
|
+
another value and hope hydration reconciles it.
|
|
184
|
+
|
|
185
|
+
Identify the source of truth for each value. Loader data is server truth;
|
|
186
|
+
hook state is for an interaction or an intentional optimistic change.
|
|
187
|
+
Avoid copying props into hook state without a plan for refresh and client
|
|
188
|
+
navigation. Avoid setting another state variable inside a state-updater
|
|
189
|
+
callback. Prefer deriving display values from one authoritative state.
|
|
190
|
+
|
|
191
|
+
Effects own their subscriptions and timers: unsubscribe, cancel, or ignore
|
|
192
|
+
stale async work on unmount or entity change. A failed script/socket load
|
|
193
|
+
must not leave a permanently rejected shared promise or silently disable a
|
|
194
|
+
feature for the rest of the session. Realtime events can arrive twice or
|
|
195
|
+
out of order; dedupe by stable identity and reconcile with refreshed data.
|
|
196
|
+
|
|
197
|
+
## 4. Keep mutations and forms cohesive
|
|
198
|
+
|
|
199
|
+
Client mutations belong in the feature's custom hook, **never inside the
|
|
200
|
+
component or its JSX callback**. The hook owns the path from input to API
|
|
201
|
+
request to success/error state to refresh or navigation. App services own
|
|
202
|
+
the operation's business rules; the browser must not reimplement them.
|
|
203
|
+
Never assume an HTTP promise cannot reject. Do not swallow a failed mutation
|
|
204
|
+
or network subscription; show actionable UI feedback and log at the
|
|
205
|
+
appropriate boundary without leaking user data.
|
|
206
|
+
|
|
207
|
+
Share a create/edit form's fields, request encoding, and validation-error
|
|
208
|
+
mapping when they are the same contract. Keep differences explicit (such as
|
|
209
|
+
required-on-create image versus optional-on-edit). Do not build a generic
|
|
210
|
+
form engine for one pair of pages. Use typed request and response shapes;
|
|
211
|
+
`unknown` is for a genuine boundary and must be narrowed before use.
|
|
212
|
+
|
|
213
|
+
Keep secrets and access tokens out of browser storage. Client affordances
|
|
214
|
+
are not authorization; protected page loaders and mutation endpoints must
|
|
215
|
+
enforce the same policy independently. Use a full navigation when an auth or
|
|
216
|
+
document boundary requires it, otherwise prefer the framework's navigation
|
|
217
|
+
and refresh primitives so state and loader data stay in sync.
|
|
218
|
+
|
|
219
|
+
## 5. Ship usable, localizable presentation
|
|
220
|
+
|
|
221
|
+
- Use semantic landmarks, headings, buttons, links, and explicit form labels.
|
|
222
|
+
A placeholder is not a label. Put validation feedback next to its input;
|
|
223
|
+
announce asynchronous success and failure accessibly. Preserve visible
|
|
224
|
+
keyboard focus, reduced-motion preferences, and useful empty/loading/
|
|
225
|
+
failure states.
|
|
226
|
+
- Reuse design tokens and cohesive feature components. Keep utility classes
|
|
227
|
+
readable; extract repeated semantics or interactions, not an abstraction
|
|
228
|
+
for every repeated class string.
|
|
229
|
+
- If the UI offers a language switch, translate first-party visible copy,
|
|
230
|
+
metadata, and accessible names for supported locales. User-generated
|
|
231
|
+
content can remain in its original language. Do not claim a locale is
|
|
232
|
+
complete when only the navigation is translated.
|
|
233
|
+
- Comments explain non-obvious constraints or measured tradeoffs. They do not
|
|
234
|
+
narrate ordinary JSX or promise performance gains without evidence.
|
|
235
|
+
Remove stale starter examples and unexplained debugging writes before
|
|
236
|
+
shipping.
|
|
237
|
+
|
|
238
|
+
## 6. Review and enforcement
|
|
239
|
+
|
|
240
|
+
Apply the shared `code-standards` rules to web TypeScript as well: `type` for
|
|
241
|
+
plain props/data, no `any`, no swallowed catches, no unsafe assertions used
|
|
242
|
+
to hide an unknown data contract. Configure non-mutating ESLint and formatting
|
|
243
|
+
checks to enforce rules that can be automated; do not rely on a lint command
|
|
244
|
+
that rewrites files as the only quality gate.
|
|
245
|
+
|
|
246
|
+
When tests are enabled, cover high-value behaviour through the public page
|
|
247
|
+
and component contracts: server render and hydration, pagination/filtering,
|
|
248
|
+
auth and error boundaries, form success/failure, navigation refresh, and
|
|
249
|
+
realtime cleanup. Co-locate focused tests per the testing-strategy skill.
|
|
250
|
+
Review accessibility and locale behavior in the actual supported modes.
|
|
251
|
+
|
|
252
|
+
Before approving a web change, ask:
|
|
253
|
+
|
|
254
|
+
1. Can a reader identify the page's server data, browser state, and side
|
|
255
|
+
effects without untangling unrelated responsibilities?
|
|
256
|
+
2. Does every loader obtain data and business decisions from a named app
|
|
257
|
+
service, with models/repositories kept out of the web layer?
|
|
258
|
+
3. Does each behavioral component delegate all state, effects, derived
|
|
259
|
+
values, event handlers, and mutations to its own custom hook file?
|
|
260
|
+
4. Is the service query bounded, the resource shape intentional, and the
|
|
261
|
+
cache scope safe?
|
|
262
|
+
5. Do refresh, navigation, errors, retries, and cleanup leave the UI correct?
|
|
263
|
+
6. Are forms and interactions accessible and translated to the degree the
|
|
264
|
+
interface promises?
|
|
265
|
+
7. Are the relevant quality rules enforced or covered by focused tests,
|
|
266
|
+
rather than described only in comments?
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import { router } from "@warlock.js/core";
|
|
2
|
-
import { fetchUploadedFileController } from "./controllers/fetch-uploaded-file.controller";
|
|
1
|
+
import { router, uploadedFileController } from "@warlock.js/core";
|
|
3
2
|
|
|
4
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Serves local uploads. Image variants are opt-in and bounded: only the names
|
|
5
|
+
* declared in `uploads.images.variants` can be requested, e.g.
|
|
6
|
+
* `/uploads/avatars/me.jpg?variant=thumb&format=webp`.
|
|
7
|
+
*/
|
|
8
|
+
router.get("/uploads/*", uploadedFileController);
|
|
@@ -5,39 +5,37 @@ import {
|
|
|
5
5
|
RedisCacheDriver,
|
|
6
6
|
type CacheConfigurations,
|
|
7
7
|
} from "@warlock.js/cache";
|
|
8
|
-
import { DatabaseCacheDriver, env
|
|
8
|
+
import { DatabaseCacheDriver, env } from "@warlock.js/core";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* Namespace every cache key by
|
|
12
|
-
* same app never read each other's cached values.
|
|
11
|
+
* Namespace every cache key by this app, fixed at boot.
|
|
13
12
|
*
|
|
14
|
-
* This used to
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* and
|
|
13
|
+
* This used to derive the prefix from `request.originDomain` / the `domain`
|
|
14
|
+
* header / `?domain=` input. That broke two ways at once: a browser GET has
|
|
15
|
+
* no `Origin` while a CSRF-protected POST does, so the two resolved to
|
|
16
|
+
* different prefixes and a write could never invalidate what a read had
|
|
17
|
+
* cached — repository caches and page-cache tags went silently stale. Worse,
|
|
18
|
+
* none of those three inputs are server-validated, so any visitor could pick
|
|
19
|
+
* `?domain=anything` and grow the store under an arbitrary namespace forever.
|
|
18
20
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
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.
|
|
21
|
+
* A cache prefix has to come from something the app decides, not something
|
|
22
|
+
* a request carries.
|
|
25
23
|
*/
|
|
26
|
-
const globalPrefix = () =>
|
|
27
|
-
const { request } = useRequestStore();
|
|
24
|
+
const globalPrefix = () => env("APP_NAME", "store");
|
|
28
25
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
};
|
|
26
|
+
/**
|
|
27
|
+
* Genuine multi-tenancy (one deployment, many tenants, isolated caches) needs
|
|
28
|
+
* a tenant id the app has already validated server-side — e.g. set on
|
|
29
|
+
* `request.locals.tenant` by the app's own tenant middleware after resolving
|
|
30
|
+
* the host against its tenants table — never the raw `Origin`/`Host`, a
|
|
31
|
+
* header, or query input, none of which the framework trusts.
|
|
32
|
+
*
|
|
33
|
+
* const globalPrefix = () => {
|
|
34
|
+
* const { request } = useRequestStore();
|
|
35
|
+
* const tenant = request?.locals.tenant; // set by app tenant middleware, already validated
|
|
36
|
+
* return tenant ? `${env("APP_NAME", "store")}.${tenant}` : env("APP_NAME", "store");
|
|
37
|
+
* };
|
|
38
|
+
*/
|
|
41
39
|
|
|
42
40
|
const cacheConfigurations: CacheConfigurations<"database"> = {
|
|
43
41
|
// Driven by CACHE_DRIVER so the shipped .env (`memory`) actually wins.
|
|
@@ -18,6 +18,23 @@ const httpConfigurations: HttpConfigurations = {
|
|
|
18
18
|
// allowed methods
|
|
19
19
|
methods: ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"],
|
|
20
20
|
},
|
|
21
|
+
// Safe for local dev: `false` trusts nothing in front of this process, so
|
|
22
|
+
// request.ip/protocol/hostname read the raw socket. Behind a proxy/LB/CDN
|
|
23
|
+
// in production, set this to `true`, a trusted IP/CIDR string, or a list
|
|
24
|
+
// of trusted proxy IPs/CIDRs — otherwise rate limiting, ip-filter
|
|
25
|
+
// allowlists, and absolute URLs (e.g. the sitemap) trust the wrong client.
|
|
26
|
+
trustProxy: false,
|
|
27
|
+
// Opt-in, nonce-based Content-Security-Policy — off by default, zero
|
|
28
|
+
// behaviour change. Flip `enabled: true` to turn it on; the framework
|
|
29
|
+
// appends the request's own `'nonce-<value>'` to `script-src` for you.
|
|
30
|
+
// csp: {
|
|
31
|
+
// enabled: false,
|
|
32
|
+
// directives: {
|
|
33
|
+
// "script-src": ["'self'"],
|
|
34
|
+
// "object-src": ["'none'"],
|
|
35
|
+
// "base-uri": ["'self'"],
|
|
36
|
+
// },
|
|
37
|
+
// },
|
|
21
38
|
};
|
|
22
39
|
|
|
23
40
|
export default httpConfigurations;
|