@warlock.js/core 4.10.0 → 4.11.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 +45 -0
- package/esm/application/application.d.mts +19 -0
- package/esm/application/application.d.mts.map +1 -1
- package/esm/application/application.mjs +25 -0
- package/esm/application/application.mjs.map +1 -1
- package/esm/application/boot-signal.d.mts +77 -0
- package/esm/application/boot-signal.d.mts.map +1 -0
- package/esm/application/boot-signal.mjs +64 -0
- package/esm/application/boot-signal.mjs.map +1 -0
- package/esm/application/index.d.mts +1 -0
- package/esm/application/index.mjs +1 -0
- package/esm/bootstrap.mjs +2 -2
- package/esm/bootstrap.mjs.map +1 -1
- package/esm/cli/cli-commands.manager.mjs +3 -3
- package/esm/cli/cli-commands.manager.mjs.map +1 -1
- package/esm/cli/cli-commands.utils.mjs +57 -1
- package/esm/cli/cli-commands.utils.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/module.generator.mjs +1 -1
- package/esm/cli/commands/generate/generators/module.generator.mjs.map +1 -1
- package/esm/cli/commands/start-production.command.mjs +4 -25
- package/esm/cli/commands/start-production.command.mjs.map +1 -1
- package/esm/cli/types.d.mts +6 -1
- package/esm/cli/types.d.mts.map +1 -1
- package/esm/config/config-getter.mjs +5 -5
- package/esm/config/config-getter.mjs.map +1 -1
- package/esm/config/config-loader.mjs +2 -2
- package/esm/config/config-loader.mjs.map +1 -1
- package/esm/config/config-setter.d.mts +27 -0
- package/esm/config/config-setter.d.mts.map +1 -0
- package/esm/config/config-setter.mjs +10 -0
- package/esm/config/config-setter.mjs.map +1 -0
- package/esm/config/index.mjs +1 -0
- package/esm/connectors/access-connector.mjs +2 -2
- package/esm/connectors/access-connector.mjs.map +1 -1
- package/esm/connectors/ai-connector.mjs +2 -2
- package/esm/connectors/ai-connector.mjs.map +1 -1
- package/esm/connectors/cache-connector.mjs +2 -2
- package/esm/connectors/cache-connector.mjs.map +1 -1
- package/esm/connectors/database-connector.mjs +2 -2
- package/esm/connectors/database-connector.mjs.map +1 -1
- package/esm/connectors/herald-connector.mjs +2 -2
- package/esm/connectors/herald-connector.mjs.map +1 -1
- package/esm/connectors/http-connector.d.mts.map +1 -1
- package/esm/connectors/http-connector.mjs +8 -7
- package/esm/connectors/http-connector.mjs.map +1 -1
- package/esm/connectors/logger-connector.mjs +2 -2
- package/esm/connectors/logger-connector.mjs.map +1 -1
- package/esm/connectors/mail-connector.mjs +2 -2
- package/esm/connectors/mail-connector.mjs.map +1 -1
- package/esm/connectors/notifications-connector.mjs +2 -2
- package/esm/connectors/notifications-connector.mjs.map +1 -1
- package/esm/connectors/socket-connector.mjs +3 -3
- package/esm/connectors/socket-connector.mjs.map +1 -1
- package/esm/connectors/storage.connector.d.mts +11 -5
- package/esm/connectors/storage.connector.d.mts.map +1 -1
- package/esm/connectors/storage.connector.mjs +11 -5
- package/esm/connectors/storage.connector.mjs.map +1 -1
- package/esm/dev-server/loader/load-hook.mjs +1 -1
- package/esm/generations/stubs.mjs +1 -1
- package/esm/generations/stubs.mjs.map +1 -1
- package/esm/http/config.mjs +2 -2
- package/esm/http/config.mjs.map +1 -1
- package/esm/http/createHttpApplication.mjs +2 -2
- package/esm/http/createHttpApplication.mjs.map +1 -1
- package/esm/http/index.d.mts +1 -0
- package/esm/http/index.mjs +1 -0
- package/esm/http/middleware/idempotency.middleware.mjs +5 -5
- package/esm/http/middleware/idempotency.middleware.mjs.map +1 -1
- package/esm/http/middleware/inject-request-context.mjs +2 -2
- package/esm/http/middleware/inject-request-context.mjs.map +1 -1
- package/esm/http/middleware/maintenance.middleware.mjs +4 -4
- package/esm/http/middleware/maintenance.middleware.mjs.map +1 -1
- package/esm/http/plugins.mjs +9 -9
- package/esm/http/plugins.mjs.map +1 -1
- package/esm/http/port-preflight.d.mts +34 -0
- package/esm/http/port-preflight.d.mts.map +1 -0
- package/esm/http/port-preflight.mjs +66 -0
- package/esm/http/port-preflight.mjs.map +1 -0
- package/esm/http/response.mjs +5 -5
- package/esm/http/response.mjs.map +1 -1
- package/esm/http/server.mjs +4 -4
- package/esm/http/server.mjs.map +1 -1
- package/esm/index.d.mts +6 -2
- package/esm/index.mjs +5 -1
- package/esm/production/assert-generated-imports.mjs +94 -0
- package/esm/production/assert-generated-imports.mjs.map +1 -0
- package/esm/production/production-builder.mjs +29 -4
- package/esm/production/production-builder.mjs.map +1 -1
- package/esm/production/production-supervisor.mjs +108 -0
- package/esm/production/production-supervisor.mjs.map +1 -0
- package/esm/storage/storage.d.mts +16 -0
- package/esm/storage/storage.d.mts.map +1 -1
- package/esm/storage/storage.mjs +24 -1
- package/esm/storage/storage.mjs.map +1 -1
- package/esm/tests/index.d.mts +2 -1
- package/esm/tests/index.mjs +1 -0
- package/esm/tests/start-http-development-server.d.mts +15 -2
- package/esm/tests/start-http-development-server.d.mts.map +1 -1
- package/esm/tests/start-http-development-server.mjs +43 -16
- package/esm/tests/start-http-development-server.mjs.map +1 -1
- package/esm/tests/test-helpers.d.mts.map +1 -1
- package/esm/tests/test-helpers.mjs +2 -1
- package/esm/tests/test-helpers.mjs.map +1 -1
- package/esm/tests/test-server-port-channel.d.mts +27 -0
- package/esm/tests/test-server-port-channel.d.mts.map +1 -0
- package/esm/tests/test-server-port-channel.mjs +32 -0
- package/esm/tests/test-server-port-channel.mjs.map +1 -0
- package/esm/utils/load-environment.mjs +59 -0
- package/esm/utils/load-environment.mjs.map +1 -0
- package/esm/utils/paths.mjs +2 -2
- package/esm/utils/paths.mjs.map +1 -1
- package/esm/validation/validateAll.mjs +2 -2
- package/esm/validation/validateAll.mjs.map +1 -1
- package/llms-full.txt +90 -3
- package/llms.txt +1 -1
- package/package.json +13 -13
- package/skills/run-app/SKILL.md +63 -0
- package/skills/test-http/SKILL.md +27 -3
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
//#region ../core/src/tests/start-http-development-server.d.ts
|
|
2
|
+
type StartHttpTestServerOptions = {
|
|
3
|
+
/**
|
|
4
|
+
* Port the test server binds to, overriding `http.port`.
|
|
5
|
+
*
|
|
6
|
+
* Needed because the server cannot be moved from outside: this function
|
|
7
|
+
* bootstraps the app itself, and that bootstrap re-reads `.env`.
|
|
8
|
+
*/
|
|
9
|
+
port?: number;
|
|
10
|
+
};
|
|
2
11
|
/**
|
|
3
12
|
* Start the HTTP test server (minimal - no file watching)
|
|
4
13
|
* Call this in Vitest's globalSetup
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* // run this suite on its own port, whatever `.env` says
|
|
17
|
+
* await startHttpTestServer({ port: 3999 });
|
|
5
18
|
*/
|
|
6
|
-
declare function startHttpTestServer(): Promise<void>;
|
|
19
|
+
declare function startHttpTestServer(options?: StartHttpTestServerOptions): Promise<void>;
|
|
7
20
|
/**
|
|
8
21
|
* Stop the HTTP test server
|
|
9
22
|
* Call this in Vitest's globalTeardown
|
|
@@ -14,5 +27,5 @@ declare function stopHttpTestServer(): Promise<void>;
|
|
|
14
27
|
*/
|
|
15
28
|
declare function isTestServerRunning(): boolean;
|
|
16
29
|
//#endregion
|
|
17
|
-
export { isTestServerRunning, startHttpTestServer, stopHttpTestServer };
|
|
30
|
+
export { StartHttpTestServerOptions, isTestServerRunning, startHttpTestServer, stopHttpTestServer };
|
|
18
31
|
//# sourceMappingURL=start-http-development-server.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"start-http-development-server.d.mts","names":[],"sources":["../../../../../../../core/src/tests/start-http-development-server.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"start-http-development-server.d.mts","names":[],"sources":["../../../../../../../core/src/tests/start-http-development-server.ts"],"mappings":";KAwBY,0BAAA;EAAA;;;;AAON;AAuDN;EAvDE,IAAI;AAAA;;;;;;AAyDI;AA8CV;;iBAhDsB,mBAAA,CACpB,OAAA,GAAS,0BAAA,GACR,OAAO;;AA8CyC;AAoBnD;;iBApBsB,kBAAA,IAAsB,OAAO;;AAoBhB;;iBAAnB,mBAAA"}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { Application } from "../application/application.mjs";
|
|
2
2
|
import "../application/index.mjs";
|
|
3
|
+
import { assertPortIsAvailable } from "../http/port-preflight.mjs";
|
|
3
4
|
import { bootstrap } from "../bootstrap.mjs";
|
|
4
5
|
import { ConnectorLifecyclePhase } from "../connectors/types.mjs";
|
|
5
6
|
import { connectorsManager } from "../connectors/connectors-manager.mjs";
|
|
6
7
|
import { warlockConfigManager } from "../warlock-config/warlock-config.manager.mjs";
|
|
7
8
|
import { filesOrchestrator } from "../dev-server/files-orchestrator.mjs";
|
|
8
9
|
import { loadConfigFiles } from "../config/load-config-files.mjs";
|
|
10
|
+
import { publishTestServerPort, withdrawTestServerPort } from "./test-server-port-channel.mjs";
|
|
11
|
+
import config from "@mongez/config";
|
|
9
12
|
|
|
10
13
|
//#region ../core/src/tests/start-http-development-server.ts
|
|
11
14
|
/**
|
|
@@ -21,30 +24,53 @@ import { loadConfigFiles } from "../config/load-config-files.mjs";
|
|
|
21
24
|
*/
|
|
22
25
|
let isServerRunning = false;
|
|
23
26
|
/**
|
|
27
|
+
* Apply the caller's port and preflight the bind.
|
|
28
|
+
*
|
|
29
|
+
* Called at the last moment before the HTTP connector binds, and that timing is
|
|
30
|
+
* the fix: `bootstrap()` re-reads `.env` through `loadEnv()` (dotenv overrides
|
|
31
|
+
* by default), and `src/config/http.ts` then resolves its port from that store,
|
|
32
|
+
* so anything the caller set earlier is gone by the time the port is read.
|
|
33
|
+
* Writing into `config` here is also the only channel that works — assigning
|
|
34
|
+
* `process.env.HTTP_PORT` has no effect, since `env()` reads dotenv's own store
|
|
35
|
+
* and never falls back to `process.env`.
|
|
36
|
+
*/
|
|
37
|
+
async function applyTestServerPort(port) {
|
|
38
|
+
if (!config.get("http")) {
|
|
39
|
+
if (port !== void 0) throw new Error(`startHttpTestServer({ port: ${port} }) was given a port, but this app has no \`http\` config, so no HTTP server is started. Add \`src/config/http.ts\` first.`);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (port !== void 0) config.set("http.port", port);
|
|
43
|
+
const resolvedPort = config.get("http.port");
|
|
44
|
+
if (typeof resolvedPort !== "number" || resolvedPort <= 0) return;
|
|
45
|
+
await assertPortIsAvailable(resolvedPort, config.get("http.host") || "localhost");
|
|
46
|
+
publishTestServerPort(resolvedPort);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
24
49
|
* Start the HTTP test server (minimal - no file watching)
|
|
25
50
|
* Call this in Vitest's globalSetup
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* // run this suite on its own port, whatever `.env` says
|
|
54
|
+
* await startHttpTestServer({ port: 3999 });
|
|
26
55
|
*/
|
|
27
|
-
async function startHttpTestServer() {
|
|
56
|
+
async function startHttpTestServer(options = {}) {
|
|
28
57
|
if (isServerRunning) {
|
|
29
58
|
console.log("[test-server] Server already running, skipping start");
|
|
30
59
|
return;
|
|
31
60
|
}
|
|
32
61
|
console.log("[test-server] Starting HTTP test server...");
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
} catch (error) {
|
|
46
|
-
throw error;
|
|
47
|
-
}
|
|
62
|
+
Application.setRuntimeStrategy("development");
|
|
63
|
+
Application.setEnvironment("test");
|
|
64
|
+
await warlockConfigManager.load();
|
|
65
|
+
await bootstrap();
|
|
66
|
+
await filesOrchestrator.init();
|
|
67
|
+
await filesOrchestrator.initializeAll();
|
|
68
|
+
await loadConfigFiles(true);
|
|
69
|
+
await connectorsManager.startPhase("early");
|
|
70
|
+
await filesOrchestrator.moduleLoader.loadAll();
|
|
71
|
+
await applyTestServerPort(options.port);
|
|
72
|
+
await connectorsManager.startPhase("late");
|
|
73
|
+
isServerRunning = true;
|
|
48
74
|
}
|
|
49
75
|
/**
|
|
50
76
|
* Stop the HTTP test server
|
|
@@ -57,6 +83,7 @@ async function stopHttpTestServer() {
|
|
|
57
83
|
}
|
|
58
84
|
try {
|
|
59
85
|
await connectorsManager.shutdown();
|
|
86
|
+
withdrawTestServerPort();
|
|
60
87
|
isServerRunning = false;
|
|
61
88
|
console.log("[test-server] HTTP test server stopped");
|
|
62
89
|
} catch (error) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"start-http-development-server.mjs","names":[],"sources":["../../../../../../../core/src/tests/start-http-development-server.ts"],"sourcesContent":["/**\r\n * HTTP Test Server\r\n *\r\n * Starts a minimal HTTP server for testing.\r\n * Unlike the full DevelopmentServer, this:\r\n * - Does NOT watch files\r\n * - Does NOT do HMR\r\n * - Only starts connectors needed for HTTP requests\r\n *\r\n * Used in Vitest's globalSetup to start server once for all test workers.\r\n */\r\nimport { Application } from \"../application\";\r\nimport { bootstrap } from \"../bootstrap\";\r\nimport { loadConfigFiles } from \"../config/load-config-files\";\r\nimport { connectorsManager } from \"../connectors/connectors-manager\";\r\nimport { ConnectorLifecyclePhase } from \"../connectors/types\";\r\nimport { filesOrchestrator } from \"../dev-server/files-orchestrator\";\r\nimport { warlockConfigManager } from \"../warlock-config/warlock-config.manager\";\r\n\r\nlet isServerRunning = false;\r\n\r\n/**\r\n * Start the HTTP test server (minimal - no file watching)\r\n * Call this in Vitest's globalSetup\r\n */\r\nexport async function startHttpTestServer(): Promise<void> {\r\n if (isServerRunning) {\r\n console.log(\"[test-server] Server already running, skipping start\");\r\n return;\r\n }\r\n\r\n console.log(\"[test-server] Starting HTTP test server...\");\r\n\r\n
|
|
1
|
+
{"version":3,"file":"start-http-development-server.mjs","names":[],"sources":["../../../../../../../core/src/tests/start-http-development-server.ts"],"sourcesContent":["/**\r\n * HTTP Test Server\r\n *\r\n * Starts a minimal HTTP server for testing.\r\n * Unlike the full DevelopmentServer, this:\r\n * - Does NOT watch files\r\n * - Does NOT do HMR\r\n * - Only starts connectors needed for HTTP requests\r\n *\r\n * Used in Vitest's globalSetup to start server once for all test workers.\r\n */\r\nimport config from \"@mongez/config\";\r\nimport { Application } from \"../application\";\r\nimport { bootstrap } from \"../bootstrap\";\r\nimport { loadConfigFiles } from \"../config/load-config-files\";\r\nimport { connectorsManager } from \"../connectors/connectors-manager\";\r\nimport { ConnectorLifecyclePhase } from \"../connectors/types\";\r\nimport { filesOrchestrator } from \"../dev-server/files-orchestrator\";\r\nimport { assertPortIsAvailable } from \"../http/port-preflight\";\r\nimport { warlockConfigManager } from \"../warlock-config/warlock-config.manager\";\r\nimport { publishTestServerPort, withdrawTestServerPort } from \"./test-server-port-channel\";\r\n\r\nlet isServerRunning = false;\r\n\r\nexport type StartHttpTestServerOptions = {\r\n /**\r\n * Port the test server binds to, overriding `http.port`.\r\n *\r\n * Needed because the server cannot be moved from outside: this function\r\n * bootstraps the app itself, and that bootstrap re-reads `.env`.\r\n */\r\n port?: number;\r\n};\r\n\r\n/**\r\n * Apply the caller's port and preflight the bind.\r\n *\r\n * Called at the last moment before the HTTP connector binds, and that timing is\r\n * the fix: `bootstrap()` re-reads `.env` through `loadEnv()` (dotenv overrides\r\n * by default), and `src/config/http.ts` then resolves its port from that store,\r\n * so anything the caller set earlier is gone by the time the port is read.\r\n * Writing into `config` here is also the only channel that works — assigning\r\n * `process.env.HTTP_PORT` has no effect, since `env()` reads dotenv's own store\r\n * and never falls back to `process.env`.\r\n */\r\nasync function applyTestServerPort(port?: number): Promise<void> {\r\n const httpConfig = config.get(\"http\");\r\n\r\n if (!httpConfig) {\r\n if (port !== undefined) {\r\n throw new Error(\r\n `startHttpTestServer({ port: ${port} }) was given a port, but this app has no \\`http\\` config, so no HTTP server is started. Add \\`src/config/http.ts\\` first.`,\r\n );\r\n }\r\n\r\n return;\r\n }\r\n\r\n if (port !== undefined) {\r\n config.set(\"http.port\", port);\r\n }\r\n\r\n const resolvedPort = config.get(\"http.port\");\r\n\r\n // No configured port — or an explicit `0`, which is the OS's \"pick a free one\r\n // for me\" idiom — means Fastify chooses the port, so there is nothing to\r\n // preflight and nothing to publish: preflighting 0 would bind some unrelated\r\n // ephemeral port and pass without proving anything, and publishing 0 would\r\n // point every worker request at `http://host:0`.\r\n if (typeof resolvedPort !== \"number\" || resolvedPort <= 0) {\r\n return;\r\n }\r\n\r\n await assertPortIsAvailable(resolvedPort, config.get(\"http.host\") || \"localhost\");\r\n\r\n publishTestServerPort(resolvedPort);\r\n}\r\n\r\n/**\r\n * Start the HTTP test server (minimal - no file watching)\r\n * Call this in Vitest's globalSetup\r\n *\r\n * @example\r\n * // run this suite on its own port, whatever `.env` says\r\n * await startHttpTestServer({ port: 3999 });\r\n */\r\nexport async function startHttpTestServer(\r\n options: StartHttpTestServerOptions = {},\r\n): Promise<void> {\r\n if (isServerRunning) {\r\n console.log(\"[test-server] Server already running, skipping start\");\r\n return;\r\n }\r\n\r\n console.log(\"[test-server] Starting HTTP test server...\");\r\n\r\n // Set environment\r\n Application.setRuntimeStrategy(\"development\");\r\n Application.setEnvironment(\"test\");\r\n\r\n // Bootstrap (env, etc.)\r\n await warlockConfigManager.load();\r\n await bootstrap();\r\n\r\n // Initialize file orchestrator (but don't watch)\r\n await filesOrchestrator.init();\r\n await filesOrchestrator.initializeAll();\r\n\r\n // Load config files\r\n await loadConfigFiles(true);\r\n\r\n // Early-phase connectors (database, cache, logger, storage, mailer,\r\n // herald) must start BEFORE app modules load: a module's `main.ts` boot\r\n // side-effect can query the DB at import time, so the data source has to\r\n // be registered first. This mirrors the dev/prod boot order (see\r\n // `cli-commands.manager`, `production-builder`, and `DevelopmentServer`).\r\n await connectorsManager.startPhase(ConnectorLifecyclePhase.Early);\r\n\r\n // Load application modules (their boot side-effects now see a live DB).\r\n await filesOrchestrator.moduleLoader.loadAll();\r\n\r\n await applyTestServerPort(options.port);\r\n\r\n // Late-phase connectors (http, socket) bind after app code has\r\n // registered its routes and listeners.\r\n await connectorsManager.startPhase(ConnectorLifecyclePhase.Late);\r\n\r\n isServerRunning = true;\r\n}\r\n\r\n/**\r\n * Stop the HTTP test server\r\n * Call this in Vitest's globalTeardown\r\n */\r\nexport async function stopHttpTestServer(): Promise<void> {\r\n if (!isServerRunning) {\r\n console.log(\"[test-server] No server to stop\");\r\n return;\r\n }\r\n\r\n try {\r\n await connectorsManager.shutdown();\r\n withdrawTestServerPort();\r\n isServerRunning = false;\r\n console.log(\"[test-server] HTTP test server stopped\");\r\n } catch (error) {\r\n console.error(\"[test-server] Error stopping HTTP server:\", error);\r\n throw error;\r\n }\r\n}\r\n\r\n/**\r\n * Check if test server is running\r\n */\r\nexport function isTestServerRunning(): boolean {\r\n return isServerRunning;\r\n}\r\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAsBA,IAAI,kBAAkB;;;;;;;;;;;;AAuBtB,eAAe,oBAAoB,MAA8B;CAG/D,IAAI,CAFe,OAAO,IAAI,MAEhB,GAAG;EACf,IAAI,SAAS,QACX,MAAM,IAAI,MACR,+BAA+B,KAAK,2HACtC;EAGF;CACF;CAEA,IAAI,SAAS,QACX,OAAO,IAAI,aAAa,IAAI;CAG9B,MAAM,eAAe,OAAO,IAAI,WAAW;CAO3C,IAAI,OAAO,iBAAiB,YAAY,gBAAgB,GACtD;CAGF,MAAM,sBAAsB,cAAc,OAAO,IAAI,WAAW,KAAK,WAAW;CAEhF,sBAAsB,YAAY;AACpC;;;;;;;;;AAUA,eAAsB,oBACpB,UAAsC,CAAC,GACxB;CACf,IAAI,iBAAiB;EACnB,QAAQ,IAAI,sDAAsD;EAClE;CACF;CAEA,QAAQ,IAAI,4CAA4C;CAGxD,YAAY,mBAAmB,aAAa;CAC5C,YAAY,eAAe,MAAM;CAGjC,MAAM,qBAAqB,KAAK;CAChC,MAAM,UAAU;CAGhB,MAAM,kBAAkB,KAAK;CAC7B,MAAM,kBAAkB,cAAc;CAGtC,MAAM,gBAAgB,IAAI;CAO1B,MAAM,kBAAkB,kBAAwC;CAGhE,MAAM,kBAAkB,aAAa,QAAQ;CAE7C,MAAM,oBAAoB,QAAQ,IAAI;CAItC,MAAM,kBAAkB,iBAAuC;CAE/D,kBAAkB;AACpB;;;;;AAMA,eAAsB,qBAAoC;CACxD,IAAI,CAAC,iBAAiB;EACpB,QAAQ,IAAI,iCAAiC;EAC7C;CACF;CAEA,IAAI;EACF,MAAM,kBAAkB,SAAS;EACjC,uBAAuB;EACvB,kBAAkB;EAClB,QAAQ,IAAI,wCAAwC;CACtD,SAAS,OAAO;EACd,QAAQ,MAAM,6CAA6C,KAAK;EAChE,MAAM;CACR;AACF;;;;AAKA,SAAgB,sBAA+B;CAC7C,OAAO;AACT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-helpers.d.mts","names":[],"sources":["../../../../../../../core/src/tests/test-helpers.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"test-helpers.d.mts","names":[],"sources":["../../../../../../../core/src/tests/test-helpers.ts"],"mappings":";;AAYA;;;;AAAgC;AAehC;;AAfgC,iBAAhB,gBAAA;;;;;iBAeM,WAAA,CAAY,IAAA,UAAc,OAAA,GAAS,WAAA,GAAmB,OAAA,CAAQ,QAAA;;;;iBAgB9D,OAAA,CAAQ,IAAA,UAAc,OAAA,GAAS,WAAA,GAAmB,OAAA,CAAQ,QAAA;;;AAhBY;iBAuBtE,QAAA,CACpB,IAAA,UACA,IAAA,YACA,OAAA,GAAS,WAAA,GACR,OAAA,CAAQ,QAAA;;;;iBAWW,OAAA,CACpB,IAAA,UACA,IAAA,YACA,OAAA,GAAS,WAAA,GACR,OAAA,CAAQ,QAAA;;;;iBAWW,UAAA,CAAW,IAAA,UAAc,OAAA,GAAS,WAAA,GAAmB,OAAA,CAAQ,QAAA;;;;iBAO7D,SAAA,CACpB,IAAA,UACA,IAAA,YACA,OAAA,GAAS,WAAA,GACR,OAAA,CAAQ,QAAA;;AAhD6E;AAOxF;iBAoDsB,iBAAA,IAAqB,QAAA,EAAU,QAAA,GAAW,OAAA,CAAQ,CAAA;;;;iBAOlD,UAAA,IAAc,QAAA,EAAU,QAAA,EAAU,cAAA,YAAuB,OAAA,CAAQ,CAAA"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { config } from "../config/config-getter.mjs";
|
|
2
2
|
import "../config/index.mjs";
|
|
3
|
+
import { TEST_SERVER_PORT_ENV_KEY } from "./test-server-port-channel.mjs";
|
|
3
4
|
|
|
4
5
|
//#region ../core/src/tests/test-helpers.ts
|
|
5
6
|
/**
|
|
@@ -11,7 +12,7 @@ import "../config/index.mjs";
|
|
|
11
12
|
* Get the test server base URL
|
|
12
13
|
*/
|
|
13
14
|
function getTestServerUrl() {
|
|
14
|
-
const port = config.key("http.port", 2031);
|
|
15
|
+
const port = process.env["WARLOCK_TEST_SERVER_PORT"] || config.key("http.port", 2031);
|
|
15
16
|
return `http://${config.key("http.host", "localhost")}:${port}`;
|
|
16
17
|
}
|
|
17
18
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-helpers.mjs","names":[],"sources":["../../../../../../../core/src/tests/test-helpers.ts"],"sourcesContent":["/**\n * Warlock.js Test Helpers\n *\n * Utilities for testing Warlock.js applications.\n */\n\nimport { config } from \"../config\";\n\n/**\n * Get the test server base URL\n */\nexport function getTestServerUrl(): string {\n const port = config.key(\"http.port\", 2031);\n const host = config.key(\"http.host\", \"localhost\");\n return `http://${host}:${port}`;\n}\n\n/**\n * Simple HTTP request helper for test server\n * Uses native fetch - lightweight, no extra dependencies\n */\nexport async function testRequest(path: string, options: RequestInit = {}): Promise<Response> {\n const baseUrl = getTestServerUrl();\n const url = path.startsWith(\"/\") ? `${baseUrl}${path}` : `${baseUrl}/${path}`;\n\n return fetch(url, {\n ...options,\n headers: {\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n}\n\n/**\n * GET request helper\n */\nexport async function testGet(path: string, options: RequestInit = {}): Promise<Response> {\n return testRequest(path, { ...options, method: \"GET\" });\n}\n\n/**\n * POST request helper\n */\nexport async function testPost(\n path: string,\n body?: unknown,\n options: RequestInit = {},\n): Promise<Response> {\n return testRequest(path, {\n ...options,\n method: \"POST\",\n body: body ? JSON.stringify(body) : undefined,\n });\n}\n\n/**\n * PUT request helper\n */\nexport async function testPut(\n path: string,\n body?: unknown,\n options: RequestInit = {},\n): Promise<Response> {\n return testRequest(path, {\n ...options,\n method: \"PUT\",\n body: body ? JSON.stringify(body) : undefined,\n });\n}\n\n/**\n * DELETE request helper\n */\nexport async function testDelete(path: string, options: RequestInit = {}): Promise<Response> {\n return testRequest(path, { ...options, method: \"DELETE\" });\n}\n\n/**\n * PATCH request helper\n */\nexport async function testPatch(\n path: string,\n body?: unknown,\n options: RequestInit = {},\n): Promise<Response> {\n return testRequest(path, {\n ...options,\n method: \"PATCH\",\n body: body ? JSON.stringify(body) : undefined,\n });\n}\n\n/**\n * Parse JSON response with type safety\n */\nexport async function parseJsonResponse<T>(response: Response): Promise<T> {\n return response.json() as Promise<T>;\n}\n\n/**\n * Assert response status and return parsed JSON\n */\nexport async function expectJson<T>(response: Response, expectedStatus = 200): Promise<T> {\n if (response.status !== expectedStatus) {\n const text = await response.text();\n throw new Error(`Expected status ${expectedStatus}, got ${response.status}. Body: ${text}`);\n }\n return parseJsonResponse<T>(response);\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"test-helpers.mjs","names":[],"sources":["../../../../../../../core/src/tests/test-helpers.ts"],"sourcesContent":["/**\n * Warlock.js Test Helpers\n *\n * Utilities for testing Warlock.js applications.\n */\n\nimport { config } from \"../config\";\nimport { TEST_SERVER_PORT_ENV_KEY } from \"./test-server-port-channel\";\n\n/**\n * Get the test server base URL\n */\nexport function getTestServerUrl(): string {\n // `startHttpTestServer` publishes the port it actually bound. Test workers are\n // separate processes whose own config resolves `http.port` from `.env`, so\n // without this a suite started on an explicit port would send every request to\n // the `.env` port instead of the one the server is listening on.\n const publishedPort = process.env[TEST_SERVER_PORT_ENV_KEY];\n const port = publishedPort || config.key(\"http.port\", 2031);\n const host = config.key(\"http.host\", \"localhost\");\n return `http://${host}:${port}`;\n}\n\n/**\n * Simple HTTP request helper for test server\n * Uses native fetch - lightweight, no extra dependencies\n */\nexport async function testRequest(path: string, options: RequestInit = {}): Promise<Response> {\n const baseUrl = getTestServerUrl();\n const url = path.startsWith(\"/\") ? `${baseUrl}${path}` : `${baseUrl}/${path}`;\n\n return fetch(url, {\n ...options,\n headers: {\n \"Content-Type\": \"application/json\",\n ...options.headers,\n },\n });\n}\n\n/**\n * GET request helper\n */\nexport async function testGet(path: string, options: RequestInit = {}): Promise<Response> {\n return testRequest(path, { ...options, method: \"GET\" });\n}\n\n/**\n * POST request helper\n */\nexport async function testPost(\n path: string,\n body?: unknown,\n options: RequestInit = {},\n): Promise<Response> {\n return testRequest(path, {\n ...options,\n method: \"POST\",\n body: body ? JSON.stringify(body) : undefined,\n });\n}\n\n/**\n * PUT request helper\n */\nexport async function testPut(\n path: string,\n body?: unknown,\n options: RequestInit = {},\n): Promise<Response> {\n return testRequest(path, {\n ...options,\n method: \"PUT\",\n body: body ? JSON.stringify(body) : undefined,\n });\n}\n\n/**\n * DELETE request helper\n */\nexport async function testDelete(path: string, options: RequestInit = {}): Promise<Response> {\n return testRequest(path, { ...options, method: \"DELETE\" });\n}\n\n/**\n * PATCH request helper\n */\nexport async function testPatch(\n path: string,\n body?: unknown,\n options: RequestInit = {},\n): Promise<Response> {\n return testRequest(path, {\n ...options,\n method: \"PATCH\",\n body: body ? JSON.stringify(body) : undefined,\n });\n}\n\n/**\n * Parse JSON response with type safety\n */\nexport async function parseJsonResponse<T>(response: Response): Promise<T> {\n return response.json() as Promise<T>;\n}\n\n/**\n * Assert response status and return parsed JSON\n */\nexport async function expectJson<T>(response: Response, expectedStatus = 200): Promise<T> {\n if (response.status !== expectedStatus) {\n const text = await response.text();\n throw new Error(`Expected status ${expectedStatus}, got ${response.status}. Body: ${text}`);\n }\n return parseJsonResponse<T>(response);\n}\n"],"mappings":";;;;;;;;;;;;;AAYA,SAAgB,mBAA2B;CAMzC,MAAM,OADgB,QAAQ,mCACA,OAAO,IAAI,aAAa,IAAI;CAE1D,OAAO,UADM,OAAO,IAAI,aAAa,WACjB,EAAE,GAAG;AAC3B;;;;;AAMA,eAAsB,YAAY,MAAc,UAAuB,CAAC,GAAsB;CAC5F,MAAM,UAAU,iBAAiB;CACjC,MAAM,MAAM,KAAK,WAAW,GAAG,IAAI,GAAG,UAAU,SAAS,GAAG,QAAQ,GAAG;CAEvE,OAAO,MAAM,KAAK;EAChB,GAAG;EACH,SAAS;GACP,gBAAgB;GAChB,GAAG,QAAQ;EACb;CACF,CAAC;AACH;;;;AAKA,eAAsB,QAAQ,MAAc,UAAuB,CAAC,GAAsB;CACxF,OAAO,YAAY,MAAM;EAAE,GAAG;EAAS,QAAQ;CAAM,CAAC;AACxD;;;;AAKA,eAAsB,SACpB,MACA,MACA,UAAuB,CAAC,GACL;CACnB,OAAO,YAAY,MAAM;EACvB,GAAG;EACH,QAAQ;EACR,MAAM,OAAO,KAAK,UAAU,IAAI,IAAI;CACtC,CAAC;AACH;;;;AAKA,eAAsB,QACpB,MACA,MACA,UAAuB,CAAC,GACL;CACnB,OAAO,YAAY,MAAM;EACvB,GAAG;EACH,QAAQ;EACR,MAAM,OAAO,KAAK,UAAU,IAAI,IAAI;CACtC,CAAC;AACH;;;;AAKA,eAAsB,WAAW,MAAc,UAAuB,CAAC,GAAsB;CAC3F,OAAO,YAAY,MAAM;EAAE,GAAG;EAAS,QAAQ;CAAS,CAAC;AAC3D;;;;AAKA,eAAsB,UACpB,MACA,MACA,UAAuB,CAAC,GACL;CACnB,OAAO,YAAY,MAAM;EACvB,GAAG;EACH,QAAQ;EACR,MAAM,OAAO,KAAK,UAAU,IAAI,IAAI;CACtC,CAAC;AACH;;;;AAKA,eAAsB,kBAAqB,UAAgC;CACzE,OAAO,SAAS,KAAK;AACvB;;;;AAKA,eAAsB,WAAc,UAAoB,iBAAiB,KAAiB;CACxF,IAAI,SAAS,WAAW,gBAAgB;EACtC,MAAM,OAAO,MAAM,SAAS,KAAK;EACjC,MAAM,IAAI,MAAM,mBAAmB,eAAe,QAAQ,SAAS,OAAO,UAAU,MAAM;CAC5F;CACA,OAAO,kBAAqB,QAAQ;AACtC"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region ../core/src/tests/test-server-port-channel.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The channel `startHttpTestServer` uses to tell test workers which port it
|
|
4
|
+
* bound.
|
|
5
|
+
*
|
|
6
|
+
* Vitest runs `globalSetup` in the main process and each test file in its own
|
|
7
|
+
* worker, so the workers do not share the main process's resolved config — they
|
|
8
|
+
* re-resolve `http.port` from `.env`. An environment variable is the one thing a
|
|
9
|
+
* worker inherits from the process that spawned it, which makes it the only
|
|
10
|
+
* channel an explicit port can travel through.
|
|
11
|
+
*
|
|
12
|
+
* This is process plumbing between two of our own processes, not application
|
|
13
|
+
* configuration — app code still reads its port through `config`.
|
|
14
|
+
*/
|
|
15
|
+
declare const TEST_SERVER_PORT_ENV_KEY = "WARLOCK_TEST_SERVER_PORT";
|
|
16
|
+
/**
|
|
17
|
+
* Publish the port the test server bound, for the workers to read.
|
|
18
|
+
*/
|
|
19
|
+
declare function publishTestServerPort(port: number): void;
|
|
20
|
+
/**
|
|
21
|
+
* Withdraw the published port on shutdown, so a later run in the same process
|
|
22
|
+
* can't inherit a stale one.
|
|
23
|
+
*/
|
|
24
|
+
declare function withdrawTestServerPort(): void;
|
|
25
|
+
//#endregion
|
|
26
|
+
export { TEST_SERVER_PORT_ENV_KEY, publishTestServerPort, withdrawTestServerPort };
|
|
27
|
+
//# sourceMappingURL=test-server-port-channel.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-server-port-channel.d.mts","names":[],"sources":["../../../../../../../core/src/tests/test-server-port-channel.ts"],"mappings":";;AAaA;;;;AAAqC;AAKrC;;;;AAAkD;AAQlD;;cAba,wBAAA;;AAayB;;iBARtB,qBAAA,CAAsB,IAAY;;;;;iBAQlC,sBAAA"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//#region ../core/src/tests/test-server-port-channel.ts
|
|
2
|
+
/**
|
|
3
|
+
* The channel `startHttpTestServer` uses to tell test workers which port it
|
|
4
|
+
* bound.
|
|
5
|
+
*
|
|
6
|
+
* Vitest runs `globalSetup` in the main process and each test file in its own
|
|
7
|
+
* worker, so the workers do not share the main process's resolved config — they
|
|
8
|
+
* re-resolve `http.port` from `.env`. An environment variable is the one thing a
|
|
9
|
+
* worker inherits from the process that spawned it, which makes it the only
|
|
10
|
+
* channel an explicit port can travel through.
|
|
11
|
+
*
|
|
12
|
+
* This is process plumbing between two of our own processes, not application
|
|
13
|
+
* configuration — app code still reads its port through `config`.
|
|
14
|
+
*/
|
|
15
|
+
const TEST_SERVER_PORT_ENV_KEY = "WARLOCK_TEST_SERVER_PORT";
|
|
16
|
+
/**
|
|
17
|
+
* Publish the port the test server bound, for the workers to read.
|
|
18
|
+
*/
|
|
19
|
+
function publishTestServerPort(port) {
|
|
20
|
+
process.env[TEST_SERVER_PORT_ENV_KEY] = String(port);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Withdraw the published port on shutdown, so a later run in the same process
|
|
24
|
+
* can't inherit a stale one.
|
|
25
|
+
*/
|
|
26
|
+
function withdrawTestServerPort() {
|
|
27
|
+
delete process.env[TEST_SERVER_PORT_ENV_KEY];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
//#endregion
|
|
31
|
+
export { TEST_SERVER_PORT_ENV_KEY, publishTestServerPort, withdrawTestServerPort };
|
|
32
|
+
//# sourceMappingURL=test-server-port-channel.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-server-port-channel.mjs","names":[],"sources":["../../../../../../../core/src/tests/test-server-port-channel.ts"],"sourcesContent":["/**\n * The channel `startHttpTestServer` uses to tell test workers which port it\n * bound.\n *\n * Vitest runs `globalSetup` in the main process and each test file in its own\n * worker, so the workers do not share the main process's resolved config — they\n * re-resolve `http.port` from `.env`. An environment variable is the one thing a\n * worker inherits from the process that spawned it, which makes it the only\n * channel an explicit port can travel through.\n *\n * This is process plumbing between two of our own processes, not application\n * configuration — app code still reads its port through `config`.\n */\nexport const TEST_SERVER_PORT_ENV_KEY = \"WARLOCK_TEST_SERVER_PORT\";\n\n/**\n * Publish the port the test server bound, for the workers to read.\n */\nexport function publishTestServerPort(port: number): void {\n process.env[TEST_SERVER_PORT_ENV_KEY] = String(port);\n}\n\n/**\n * Withdraw the published port on shutdown, so a later run in the same process\n * can't inherit a stale one.\n */\nexport function withdrawTestServerPort(): void {\n delete process.env[TEST_SERVER_PORT_ENV_KEY];\n}\n"],"mappings":";;;;;;;;;;;;;;AAaA,MAAa,2BAA2B;;;;AAKxC,SAAgB,sBAAsB,MAAoB;CACxD,QAAQ,IAAI,4BAA4B,OAAO,IAAI;AACrD;;;;;AAMA,SAAgB,yBAA+B;CAC7C,OAAO,QAAQ,IAAI;AACrB"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { loadEnv } from "@mongez/dotenv";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
|
|
5
|
+
//#region ../core/src/utils/load-environment.ts
|
|
6
|
+
/**
|
|
7
|
+
* The files `loadEnv()` will look for, in the order it considers them.
|
|
8
|
+
*
|
|
9
|
+
* Mirrors `@mongez/dotenv`'s own resolution — `.env.shared` first, then
|
|
10
|
+
* `.env.<NODE_ENV>` if it exists, else plain `.env` — because the presence
|
|
11
|
+
* check has to ask the same question the loader is about to ask.
|
|
12
|
+
*/
|
|
13
|
+
function candidateEnvFiles(directory) {
|
|
14
|
+
const files = [path.join(directory, ".env"), path.join(directory, ".env.shared")];
|
|
15
|
+
if (process.env.NODE_ENV) files.push(path.join(directory, `.env.${process.env.NODE_ENV}`));
|
|
16
|
+
return files;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Whether this process has already loaded its env files.
|
|
20
|
+
*
|
|
21
|
+
* Two callers reach here on a bootstrapping command — the CLI preload phase and
|
|
22
|
+
* `bootstrap()` — and `loadEnv()` defaults to `override: true`, so a second pass
|
|
23
|
+
* re-parses the same files and re-writes `process.env`. Anything set in between
|
|
24
|
+
* (a `warlock.config.ts` module body, a config file, an orchestrator) would be
|
|
25
|
+
* silently clobbered by the later load. Same shape as the defect where a
|
|
26
|
+
* caller's test-server port was overwritten by a re-read of `.env`.
|
|
27
|
+
*
|
|
28
|
+
* A deliberate reload is still possible and still used: the dev server calls
|
|
29
|
+
* `loadEnv()` directly when an `.env` file changes on disk, which is a reload
|
|
30
|
+
* the user asked for rather than an accidental second pass.
|
|
31
|
+
*/
|
|
32
|
+
let environmentLoaded = false;
|
|
33
|
+
/**
|
|
34
|
+
* Load `.env` files when there are any, and do nothing when there are not.
|
|
35
|
+
*
|
|
36
|
+
* Loads at most **once per process** — see {@link environmentLoaded}.
|
|
37
|
+
*
|
|
38
|
+
* `@mongez/dotenv`'s `loadEnvFile` **throws** when the file is absent. That was
|
|
39
|
+
* survivable while only `dev` loaded env; now that every command loads it —
|
|
40
|
+
* because `warlock.config.ts` calls `env()` in its module body and had been
|
|
41
|
+
* evaluated against an empty store under every command — an unguarded call
|
|
42
|
+
* would turn "this project has no .env" from a non-event into a hard failure
|
|
43
|
+
* of `warlock build`. A project without a `.env` is legitimate: config files
|
|
44
|
+
* carry defaults, and containers inject their own variables.
|
|
45
|
+
*
|
|
46
|
+
* The guard lives here rather than waiting on a fix in `@mongez/dotenv`: core
|
|
47
|
+
* decides its own env policy and must not depend on another package's release
|
|
48
|
+
* schedule to avoid crashing a working build.
|
|
49
|
+
*/
|
|
50
|
+
async function loadEnvironmentFiles(directory = process.cwd()) {
|
|
51
|
+
if (environmentLoaded) return;
|
|
52
|
+
if (!candidateEnvFiles(directory).some((file) => existsSync(file))) return;
|
|
53
|
+
environmentLoaded = true;
|
|
54
|
+
await loadEnv();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
//#endregion
|
|
58
|
+
export { loadEnvironmentFiles };
|
|
59
|
+
//# sourceMappingURL=load-environment.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-environment.mjs","names":[],"sources":["../../../../../../../core/src/utils/load-environment.ts"],"sourcesContent":["import { loadEnv } from \"@mongez/dotenv\";\nimport { existsSync } from \"node:fs\";\nimport path from \"node:path\";\n\n/**\n * The files `loadEnv()` will look for, in the order it considers them.\n *\n * Mirrors `@mongez/dotenv`'s own resolution — `.env.shared` first, then\n * `.env.<NODE_ENV>` if it exists, else plain `.env` — because the presence\n * check has to ask the same question the loader is about to ask.\n */\nfunction candidateEnvFiles(directory: string): string[] {\n const files = [path.join(directory, \".env\"), path.join(directory, \".env.shared\")];\n\n if (process.env.NODE_ENV) {\n files.push(path.join(directory, `.env.${process.env.NODE_ENV}`));\n }\n\n return files;\n}\n\n/**\n * Whether this process has already loaded its env files.\n *\n * Two callers reach here on a bootstrapping command — the CLI preload phase and\n * `bootstrap()` — and `loadEnv()` defaults to `override: true`, so a second pass\n * re-parses the same files and re-writes `process.env`. Anything set in between\n * (a `warlock.config.ts` module body, a config file, an orchestrator) would be\n * silently clobbered by the later load. Same shape as the defect where a\n * caller's test-server port was overwritten by a re-read of `.env`.\n *\n * A deliberate reload is still possible and still used: the dev server calls\n * `loadEnv()` directly when an `.env` file changes on disk, which is a reload\n * the user asked for rather than an accidental second pass.\n */\nlet environmentLoaded = false;\n\n/**\n * Load `.env` files when there are any, and do nothing when there are not.\n *\n * Loads at most **once per process** — see {@link environmentLoaded}.\n *\n * `@mongez/dotenv`'s `loadEnvFile` **throws** when the file is absent. That was\n * survivable while only `dev` loaded env; now that every command loads it —\n * because `warlock.config.ts` calls `env()` in its module body and had been\n * evaluated against an empty store under every command — an unguarded call\n * would turn \"this project has no .env\" from a non-event into a hard failure\n * of `warlock build`. A project without a `.env` is legitimate: config files\n * carry defaults, and containers inject their own variables.\n *\n * The guard lives here rather than waiting on a fix in `@mongez/dotenv`: core\n * decides its own env policy and must not depend on another package's release\n * schedule to avoid crashing a working build.\n */\nexport async function loadEnvironmentFiles(directory: string = process.cwd()): Promise<void> {\n if (environmentLoaded) {\n return;\n }\n\n const hasEnvFile = candidateEnvFiles(directory).some((file) => existsSync(file));\n\n if (!hasEnvFile) {\n // Not latched: a project may create its `.env` between a command's preload\n // and its bootstrap, and there is nothing to protect from a second override\n // when the first pass loaded nothing.\n return;\n }\n\n environmentLoaded = true;\n\n await loadEnv();\n}\n\n/**\n * Forget that env has been loaded, so the next call loads again.\n *\n * @internal For tests, which run many boots inside one process.\n */\nexport function resetLoadedEnvironment(): void {\n environmentLoaded = false;\n}\n"],"mappings":";;;;;;;;;;;;AAWA,SAAS,kBAAkB,WAA6B;CACtD,MAAM,QAAQ,CAAC,KAAK,KAAK,WAAW,MAAM,GAAG,KAAK,KAAK,WAAW,aAAa,CAAC;CAEhF,IAAI,QAAQ,IAAI,UACd,MAAM,KAAK,KAAK,KAAK,WAAW,QAAQ,QAAQ,IAAI,UAAU,CAAC;CAGjE,OAAO;AACT;;;;;;;;;;;;;;;AAgBA,IAAI,oBAAoB;;;;;;;;;;;;;;;;;;AAmBxB,eAAsB,qBAAqB,YAAoB,QAAQ,IAAI,GAAkB;CAC3F,IAAI,mBACF;CAKF,IAAI,CAFe,kBAAkB,SAAS,CAAC,CAAC,MAAM,SAAS,WAAW,IAAI,CAEhE,GAIZ;CAGF,oBAAoB;CAEpB,MAAM,QAAQ;AAChB"}
|
package/esm/utils/paths.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import config from "@mongez/config";
|
|
2
2
|
import path from "path";
|
|
3
3
|
|
|
4
4
|
//#region ../core/src/utils/paths.ts
|
|
@@ -28,7 +28,7 @@ function storagePath(relativePath = "") {
|
|
|
28
28
|
* If no path is given, it will return the absolute path to the uploads folder
|
|
29
29
|
*/
|
|
30
30
|
function uploadsPath(relativePath = "") {
|
|
31
|
-
const configPath =
|
|
31
|
+
const configPath = config.get("uploads.root");
|
|
32
32
|
if (!configPath) return rootPath("storage", "uploads", relativePath);
|
|
33
33
|
return typeof configPath === "function" ? configPath(relativePath) : path.resolve(configPath, relativePath);
|
|
34
34
|
}
|
package/esm/utils/paths.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"paths.mjs","names":[
|
|
1
|
+
{"version":3,"file":"paths.mjs","names":[],"sources":["../../../../../../../core/src/utils/paths.ts"],"sourcesContent":["import config from \"@mongez/config\";\r\nimport path from \"path\";\r\n\r\n/**\r\n * Get root path or join the given paths to the root path\r\n */\r\nexport function rootPath(...paths: string[]) {\r\n return path.resolve(process.cwd(), ...paths);\r\n}\r\n\r\n/**\r\n * Get src directory path or join the given paths to the src directory path\r\n */\r\nexport function srcPath(...paths: string[]) {\r\n return rootPath(\"src\", ...paths);\r\n}\r\n\r\n/**\r\n * Get the absolute path to the storage folder to the given path\r\n *\r\n * If no path is given, it will return the absolute path to the storage folder\r\n */\r\nexport function storagePath(relativePath = \"\") {\r\n return rootPath(\"storage\", relativePath);\r\n}\r\n\r\n/**\r\n * Get the absolute path to the uploads folder to the given path\r\n *\r\n * If no path is given, it will return the absolute path to the uploads folder\r\n */\r\nexport function uploadsPath(relativePath = \"\") {\r\n const configPath = config.get(\"uploads.root\");\r\n if (!configPath) {\r\n return rootPath(\"storage\", \"uploads\", relativePath);\r\n }\r\n\r\n return typeof configPath === \"function\"\r\n ? configPath(relativePath)\r\n : path.resolve(configPath, relativePath);\r\n}\r\n\r\n/**\r\n * Get the absolute path to the public folder to the given path\r\n *\r\n * If no path is given, it will return the absolute path to the public folder\r\n */\r\nexport function publicPath(relativePath = \"\") {\r\n return rootPath(\"public\", relativePath);\r\n}\r\n\r\n/**\r\n * Get the absolute path to the cache folder to the given path\r\n *\r\n * If no path is given, it will return the absolute path to the cache folder\r\n */\r\nexport function cachePath(relativePath = \"\") {\r\n return rootPath(\"storage\", \"cache\", relativePath);\r\n}\r\n\r\n/**\r\n * App path\r\n */\r\nexport function appPath(relativePath = \"\") {\r\n return rootPath(\"src/app\", relativePath);\r\n}\r\n\r\n/**\r\n * Get logs directory path\r\n */\r\nexport function logsPath(relativePath = \"\") {\r\n return rootPath(\"storage/logs\", relativePath);\r\n}\r\n\r\n/**\r\n * Get a temp path\r\n */\r\nexport function tempPath(relativePath = \"\") {\r\n return rootPath(\"storage/tmp\", relativePath);\r\n}\r\n\r\n/**\r\n * Remove any invalid characters from the file path using regex\r\n * It should accept any language character, numbers, and the following characters: _ - .\r\n */\r\nconst invalidCharsRegex = /[<>:\"/\\\\|?*]/g; // Regex to match invalid characters\r\nexport function sanitizePath(filePath: string) {\r\n return filePath.replace(invalidCharsRegex, \"\"); // Replace invalid characters with an empty string\r\n}\r\n\r\n/**\r\n * Warlock path\r\n * PLEASE DO NOT add any files in this directory as it may be deleted\r\n */\r\nexport function warlockPath(...path: string[]) {\r\n return rootPath(\".warlock\", ...path);\r\n}\r\n\r\n/**\r\n * Get config directory path\r\n */\r\nexport function configPath(...path: string[]) {\r\n return rootPath(\"src/config\", ...path);\r\n}\r\n\r\nexport const paths = {\r\n root: rootPath,\r\n src: srcPath,\r\n storage: storagePath,\r\n logs: logsPath,\r\n uploads: uploadsPath,\r\n public: publicPath,\r\n cache: cachePath,\r\n app: appPath,\r\n temp: tempPath,\r\n warlock: warlockPath,\r\n config: configPath,\r\n sanitize: sanitizePath,\r\n};\r\n"],"mappings":";;;;;;;AAMA,SAAgB,SAAS,GAAG,OAAiB;CAC3C,OAAO,KAAK,QAAQ,QAAQ,IAAI,GAAG,GAAG,KAAK;AAC7C;;;;AAKA,SAAgB,QAAQ,GAAG,OAAiB;CAC1C,OAAO,SAAS,OAAO,GAAG,KAAK;AACjC;;;;;;AAOA,SAAgB,YAAY,eAAe,IAAI;CAC7C,OAAO,SAAS,WAAW,YAAY;AACzC;;;;;;AAOA,SAAgB,YAAY,eAAe,IAAI;CAC7C,MAAM,aAAa,OAAO,IAAI,cAAc;CAC5C,IAAI,CAAC,YACH,OAAO,SAAS,WAAW,WAAW,YAAY;CAGpD,OAAO,OAAO,eAAe,aACzB,WAAW,YAAY,IACvB,KAAK,QAAQ,YAAY,YAAY;AAC3C;;;;;;AAOA,SAAgB,WAAW,eAAe,IAAI;CAC5C,OAAO,SAAS,UAAU,YAAY;AACxC;;;;;;AAOA,SAAgB,UAAU,eAAe,IAAI;CAC3C,OAAO,SAAS,WAAW,SAAS,YAAY;AAClD;;;;AAKA,SAAgB,QAAQ,eAAe,IAAI;CACzC,OAAO,SAAS,WAAW,YAAY;AACzC;;;;AAKA,SAAgB,SAAS,eAAe,IAAI;CAC1C,OAAO,SAAS,gBAAgB,YAAY;AAC9C;;;;AAKA,SAAgB,SAAS,eAAe,IAAI;CAC1C,OAAO,SAAS,eAAe,YAAY;AAC7C;;;;;AAMA,MAAM,oBAAoB;AAC1B,SAAgB,aAAa,UAAkB;CAC7C,OAAO,SAAS,QAAQ,mBAAmB,EAAE;AAC/C;;;;;AAMA,SAAgB,YAAY,GAAG,MAAgB;CAC7C,OAAO,SAAS,YAAY,GAAG,IAAI;AACrC;;;;AAKA,SAAgB,WAAW,GAAG,MAAgB;CAC5C,OAAO,SAAS,cAAc,GAAG,IAAI;AACvC;AAEA,MAAa,QAAQ;CACnB,MAAM;CACN,KAAK;CACL,SAAS;CACT,MAAM;CACN,SAAS;CACT,QAAQ;CACR,OAAO;CACP,KAAK;CACL,MAAM;CACN,SAAS;CACT,QAAQ;CACR,UAAU;AACZ"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { v } from "@warlock.js/seal";
|
|
2
|
-
import
|
|
2
|
+
import config from "@mongez/config";
|
|
3
3
|
import { log } from "@warlock.js/logger";
|
|
4
4
|
import { merge } from "@mongez/reinforcements";
|
|
5
5
|
|
|
@@ -40,7 +40,7 @@ async function validateAll(validation, request, response) {
|
|
|
40
40
|
if (validation.validate) {
|
|
41
41
|
const result = await validation.validate(request, response);
|
|
42
42
|
if (result) {
|
|
43
|
-
if (!response.statusCode) response.setStatusCode(
|
|
43
|
+
if (!response.statusCode) response.setStatusCode(config.get("validation.responseStatus", 400));
|
|
44
44
|
log.info("validation", "failed", "Validation failed");
|
|
45
45
|
return result;
|
|
46
46
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validateAll.mjs","names":[
|
|
1
|
+
{"version":3,"file":"validateAll.mjs","names":[],"sources":["../../../../../../../core/src/validation/validateAll.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport { merge } from \"@mongez/reinforcements\";\nimport { log } from \"@warlock.js/logger\";\nimport { v } from \"@warlock.js/seal\";\nimport type { Request, Response } from \"../http\";\nimport type { RequestHandlerValidation, Route } from \"../router\";\n\nfunction resolveDataToParse(validating: RequestHandlerValidation[\"validating\"], request: Request) {\n if (!validating || validating.length === 0) return request.allExceptParams();\n\n let data: any = {};\n\n for (const validatingType of validating) {\n if (validatingType === \"body\") {\n data = merge(data, request.body);\n }\n\n if (validatingType === \"query\") {\n data = merge(data, request.query);\n }\n\n if (validatingType === \"params\") {\n data = merge(data, request.params);\n }\n\n if (validatingType === \"headers\") {\n data = merge(data, request.headers);\n }\n }\n\n return data;\n}\n\n/**\n * Validate the request route\n */\nexport async function validateAll(\n validation: Route[\"handler\"][\"validation\"],\n request: Request,\n response: Response,\n) {\n if (!validation) return;\n\n log.info(\"validation\", \"started\", \"Start validating the request\");\n\n if (validation.schema) {\n log.info(\"validation\", \"schema\", \"Validating request schema\");\n try {\n const data = resolveDataToParse(validation.validating, request);\n const result = await v.validate(validation.schema, data);\n\n if (result.data && result.isValid) {\n request.setValidatedData(result.data);\n }\n\n if (!result.isValid) {\n log.warn(\"validation\", \"schema\", \"Schema Validation failed\");\n return response.failedSchema(result);\n }\n\n log.success(\"validation\", \"schema\", \"Schema Validation passed\");\n } catch (error) {\n log.warn(\"app.validation\", \"error\", error);\n throw error;\n }\n }\n\n if (validation.validate) {\n const result = await validation.validate(request, response);\n\n // if there is a result, it means it failed\n if (result) {\n // check if there is no response status code, then set it to config value or 400 as default\n if (!response.statusCode) {\n response.setStatusCode(config.get(\"validation.responseStatus\", 400));\n }\n\n log.info(\"validation\", \"failed\", \"Validation failed\");\n\n return result;\n }\n\n log.info(\"validation\", \"passed\", \"Validation passed\");\n }\n}\n"],"mappings":";;;;;;AAOA,SAAS,mBAAmB,YAAoD,SAAkB;CAChG,IAAI,CAAC,cAAc,WAAW,WAAW,GAAG,OAAO,QAAQ,gBAAgB;CAE3E,IAAI,OAAY,CAAC;CAEjB,KAAK,MAAM,kBAAkB,YAAY;EACvC,IAAI,mBAAmB,QACrB,OAAO,MAAM,MAAM,QAAQ,IAAI;EAGjC,IAAI,mBAAmB,SACrB,OAAO,MAAM,MAAM,QAAQ,KAAK;EAGlC,IAAI,mBAAmB,UACrB,OAAO,MAAM,MAAM,QAAQ,MAAM;EAGnC,IAAI,mBAAmB,WACrB,OAAO,MAAM,MAAM,QAAQ,OAAO;CAEtC;CAEA,OAAO;AACT;;;;AAKA,eAAsB,YACpB,YACA,SACA,UACA;CACA,IAAI,CAAC,YAAY;CAEjB,IAAI,KAAK,cAAc,WAAW,8BAA8B;CAEhE,IAAI,WAAW,QAAQ;EACrB,IAAI,KAAK,cAAc,UAAU,2BAA2B;EAC5D,IAAI;GACF,MAAM,OAAO,mBAAmB,WAAW,YAAY,OAAO;GAC9D,MAAM,SAAS,MAAM,EAAE,SAAS,WAAW,QAAQ,IAAI;GAEvD,IAAI,OAAO,QAAQ,OAAO,SACxB,QAAQ,iBAAiB,OAAO,IAAI;GAGtC,IAAI,CAAC,OAAO,SAAS;IACnB,IAAI,KAAK,cAAc,UAAU,0BAA0B;IAC3D,OAAO,SAAS,aAAa,MAAM;GACrC;GAEA,IAAI,QAAQ,cAAc,UAAU,0BAA0B;EAChE,SAAS,OAAO;GACd,IAAI,KAAK,kBAAkB,SAAS,KAAK;GACzC,MAAM;EACR;CACF;CAEA,IAAI,WAAW,UAAU;EACvB,MAAM,SAAS,MAAM,WAAW,SAAS,SAAS,QAAQ;EAG1D,IAAI,QAAQ;GAEV,IAAI,CAAC,SAAS,YACZ,SAAS,cAAc,OAAO,IAAI,6BAA6B,GAAG,CAAC;GAGrE,IAAI,KAAK,cAAc,UAAU,mBAAmB;GAEpD,OAAO;EACT;EAEA,IAAI,KAAK,cAAc,UAAU,mBAAmB;CACtD;AACF"}
|
package/llms-full.txt
CHANGED
|
@@ -3339,6 +3339,57 @@ Just `warlockConfig: true` — same as `build`. The actual app bootstrap happens
|
|
|
3339
3339
|
|
|
3340
3340
|
Means `docker stop` / `kubectl delete pod` works as expected: SIGTERM reaches the bundle, your graceful-shutdown hooks fire, then the parent exits.
|
|
3341
3341
|
|
|
3342
|
+
### pnpm needs esbuild's install script allowed
|
|
3343
|
+
|
|
3344
|
+
pnpm 10+ will not run a dependency's install script unless the app names it. esbuild's script links its platform-native binary, and `warlock build` shells out to that binary — so the app installs cleanly and then cannot build:
|
|
3345
|
+
|
|
3346
|
+
```
|
|
3347
|
+
[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: esbuild@0.27.7
|
|
3348
|
+
```
|
|
3349
|
+
|
|
3350
|
+
Fix it once in the app's `pnpm-workspace.yaml`:
|
|
3351
|
+
|
|
3352
|
+
```yaml
|
|
3353
|
+
allowBuilds:
|
|
3354
|
+
esbuild: true
|
|
3355
|
+
```
|
|
3356
|
+
|
|
3357
|
+
Note pnpm reads this from `pnpm-workspace.yaml`, **not** from `package.json`'s `pnpm` field — pnpm 11 warns that the field is ignored and then carries on, so settings left there fail silently.
|
|
3358
|
+
|
|
3359
|
+
Nothing else is needed for pnpm. Warlock never requires an app to declare a package it does not import: generated code is checked at build time against the app's own `dependencies`, so `warlock build` failing over an unfamiliar package name is a framework bug, not a missing dependency.
|
|
3360
|
+
|
|
3361
|
+
### Output streams — what a supervisor may trust
|
|
3362
|
+
|
|
3363
|
+
**A success line on stdout means the app is serving requests.** That is a contract, not a convention, and you can build a CI gate or a health probe on it.
|
|
3364
|
+
|
|
3365
|
+
| Stream | Carries |
|
|
3366
|
+
| ---------- | -------------------------------------------------------------- |
|
|
3367
|
+
| **stdout** | the started banner, and start failures. Nothing else. |
|
|
3368
|
+
| **stderr** | progress (`🚀 Starting production server...`), diagnostics, the application's own logs |
|
|
3369
|
+
|
|
3370
|
+
The started banner prints **only** when the running application reports a completed boot — not when the command starts, not when the child is spawned. A child that dies before reporting is a failed start: the failure is written to **both** streams (stderr for humans and log collectors, stdout so a supervisor greping for the banner finds a failure rather than silence), and `warlock start` exits non-zero **even when the child itself exited `0`**.
|
|
3371
|
+
|
|
3372
|
+
```bash
|
|
3373
|
+
# a CI gate can be this blunt, and it is now correct
|
|
3374
|
+
yarn warlock start | grep -q "production server started"
|
|
3375
|
+
```
|
|
3376
|
+
|
|
3377
|
+
### How readiness is reported
|
|
3378
|
+
|
|
3379
|
+
`warlock start` spawns the bundle with an IPC channel and sets `WARLOCK_BOOT_SIGNAL=1` on it. `Application.markBooted()` — which the production entry calls after the late-phase connectors (http, socket) are up — sends one versioned message and closes the channel:
|
|
3380
|
+
|
|
3381
|
+
```ts
|
|
3382
|
+
{ type: "warlock:ready", version: 1, pid, at, environment, runtimeStrategy, bootDurationMs?, port? }
|
|
3383
|
+
```
|
|
3384
|
+
|
|
3385
|
+
Three consequences worth knowing:
|
|
3386
|
+
|
|
3387
|
+
1. **A queue worker with no http connector still reports.** Readiness hangs on a completed boot, not on a bound port. `port` is simply absent.
|
|
3388
|
+
2. **Running the bundle any other way changes nothing.** `node dist/app.js`, a Docker `CMD`, or pm2 — the signal is a strict no-op without both the IPC channel and the handshake flag, so Warlock never writes into a channel that belongs to another supervisor.
|
|
3389
|
+
3. **A bundle built before 4.11.0 has no signal.** It starts and runs normally, and after ten seconds prints a note on **stderr only** telling you to re-run `warlock build`. An absent signal is never an error, never fails the run, and never kills a slow boot.
|
|
3390
|
+
|
|
3391
|
+
If you need the same fact inside the app, use `Application.onceBooted()` / `Application.whenBooted()` — the signal and your listeners fire from the same latch.
|
|
3392
|
+
|
|
3342
3393
|
## Picking which mode you're in
|
|
3343
3394
|
|
|
3344
3395
|
`Application.environment` and `Application.runtimeStrategy` are separate axes:
|
|
@@ -3406,6 +3457,18 @@ export default defineConfig({
|
|
|
3406
3457
|
|
|
3407
3458
|
CI sets `BUILD_OUT=build/<sha>` per pipeline. `warlock start` reads the same config and finds the bundle without any hardcoded paths.
|
|
3408
3459
|
|
|
3460
|
+
**This recipe only started working in 4.11.0.** Before that, `warlock.config.ts` was evaluated *before* any `.env` file was read, so every `env()` call in it returned its default — silently, under every command. If you copied this recipe earlier and concluded that `BUILD_OUT` was ignored, it was.
|
|
3461
|
+
|
|
3462
|
+
**Which file the value comes from is decided by `NODE_ENV`, and no Warlock command sets it.** `env()` reads `.env.<NODE_ENV>` when that file exists and falls back to plain `.env`. So:
|
|
3463
|
+
|
|
3464
|
+
```bash
|
|
3465
|
+
warlock build # NODE_ENV unset → reads .env
|
|
3466
|
+
NODE_ENV=production warlock build # → reads .env.production
|
|
3467
|
+
NODE_ENV=staging warlock build # → reads .env.staging
|
|
3468
|
+
```
|
|
3469
|
+
|
|
3470
|
+
That is deliberate: `build` and `start` do **not** force `production`. Forcing it would silently change which file an existing `NODE_ENV=staging` pipeline reads, and would have the framework overriding an operator's explicit choice at the moment of deployment. Set `NODE_ENV` in your Dockerfile, CI job, or process manager — the same place you already set it for `Application.environment`.
|
|
3471
|
+
|
|
3409
3472
|
### Skip type-gen on machines without write access
|
|
3410
3473
|
|
|
3411
3474
|
```bash
|
|
@@ -4493,7 +4556,7 @@ Client `PUT`s the bytes straight to R2 — your server never sees them.
|
|
|
4493
4556
|
|
|
4494
4557
|
---
|
|
4495
4558
|
name: test-http
|
|
4496
|
-
description: 'Integration tests against a real HTTP server — `startHttpTestServer()` boots one shared server in globalSetup, then `testGet` / `testPost` / `expectJson` make typed requests against it. Triggers: `startHttpTestServer`, `stopHttpTestServer`, `testGet`, `testPost`, `testPut`, `testPatch`, `testDelete`, `expectJson`, `getTestServerUrl`, `testRequest`; "integration-test a controller", "end-to-end HTTP test", "globalSetup HTTP server", "assert status and body shape"; typical import `import { testGet, testPost, expectJson } from "@warlock.js/core"`. Skip: pure unit tests — `@warlock.js/core/test-service/SKILL.md`; controller shape — `@warlock.js/core/create-controller/SKILL.md`; competing libs `supertest`, `light-my-request`, `nock`.'
|
|
4559
|
+
description: 'Integration tests against a real HTTP server — `startHttpTestServer()` boots one shared server in globalSetup, then `testGet` / `testPost` / `expectJson` make typed requests against it. Triggers: `startHttpTestServer`, `startHttpTestServer({ port })`, `stopHttpTestServer`, `testGet`, `testPost`, `testPut`, `testPatch`, `testDelete`, `expectJson`, `getTestServerUrl`, `testRequest`, `PortInUseError`, `assertPortIsAvailable`, `isPortAvailable`; "integration-test a controller", "end-to-end HTTP test", "globalSetup HTTP server", "assert status and body shape", "test server port already in use", "EADDRINUSE while running tests", "run tests while the dev server is up"; typical import `import { testGet, testPost, expectJson } from "@warlock.js/core"`. Skip: pure unit tests — `@warlock.js/core/test-service/SKILL.md`; controller shape — `@warlock.js/core/create-controller/SKILL.md`; competing libs `supertest`, `light-my-request`, `nock`.'
|
|
4497
4560
|
---
|
|
4498
4561
|
|
|
4499
4562
|
# Warlock — HTTP integration tests
|
|
@@ -4549,6 +4612,30 @@ Unlike the dev server, it doesn't watch files, doesn't do HMR, doesn't run healt
|
|
|
4549
4612
|
|
|
4550
4613
|
Both are idempotent. A second `start` returns early; `stop` on a non-running server logs and returns.
|
|
4551
4614
|
|
|
4615
|
+
### Choosing the port — `startHttpTestServer({ port })`
|
|
4616
|
+
|
|
4617
|
+
```ts
|
|
4618
|
+
await startHttpTestServer({ port: 3999 });
|
|
4619
|
+
```
|
|
4620
|
+
|
|
4621
|
+
The explicit port wins over `http.port` — including over `HTTP_PORT` in `.env`. Use it to run a suite while the dev server holds the configured port, or to run two suites side by side.
|
|
4622
|
+
|
|
4623
|
+
**Nothing else can move the server.** `startHttpTestServer()` bootstraps the app itself, and that bootstrap re-reads `.env` (dotenv overrides by default), so any value you set before calling it is gone before the port is read. Assigning `process.env.HTTP_PORT` does not work either: `env()` reads dotenv's own store and never falls back to `process.env`. The option is applied after the bootstrap, immediately before the connector binds — that is why it is the only channel that survives.
|
|
4624
|
+
|
|
4625
|
+
The request helpers follow with no extra wiring: the bound port is published as `WARLOCK_TEST_SERVER_PORT`, which the workers inherit, and `getTestServerUrl()` prefers it over their own `.env`-resolved `http.port`. `stopHttpTestServer()` withdraws it.
|
|
4626
|
+
|
|
4627
|
+
### The port is preflighted
|
|
4628
|
+
|
|
4629
|
+
Before binding, the test server checks the resolved port is free and fails with an instruction if it isn't:
|
|
4630
|
+
|
|
4631
|
+
```
|
|
4632
|
+
Port 2031 is already in use on localhost. Stop the dev server (or whatever else is
|
|
4633
|
+
listening on port 2031) and run again, or start on a free port — e.g.
|
|
4634
|
+
startHttpTestServer({ port: 2032 }).
|
|
4635
|
+
```
|
|
4636
|
+
|
|
4637
|
+
This runs whether or not you passed a port, so a collision never reaches you as a bare `EADDRINUSE` from inside Fastify. The failure is a `PortInUseError` carrying `port` and `host`. The same check is available on its own — `assertPortIsAvailable(port, host)` throws it, `isPortAvailable(port, host)` returns a boolean.
|
|
4638
|
+
|
|
4552
4639
|
## Project wiring — `src/test-global-setup.ts` + `vite.config.ts`
|
|
4553
4640
|
|
|
4554
4641
|
```ts title="src/test-global-setup.ts"
|
|
@@ -4598,7 +4685,7 @@ import { getTestServerUrl } from "@warlock.js/core";
|
|
|
4598
4685
|
const url = getTestServerUrl(); // → "http://localhost:2031" (defaults)
|
|
4599
4686
|
```
|
|
4600
4687
|
|
|
4601
|
-
Reads `http.host` (default `"localhost"`) and `http.port` (default `2031`) from config
|
|
4688
|
+
Reads `http.host` (default `"localhost"`) and `http.port` (default `2031`) from config — unless `startHttpTestServer` published a port, which wins, since a worker's own config never sees the `{ port }` option passed in `globalSetup`. If you change the HTTP config, helpers follow automatically.
|
|
4602
4689
|
|
|
4603
4690
|
### Verb helpers
|
|
4604
4691
|
|
|
@@ -4767,7 +4854,7 @@ This is fine for normal test flow. It bites when you're inside a transaction the
|
|
|
4767
4854
|
## Gotchas
|
|
4768
4855
|
|
|
4769
4856
|
- **`globalSetup` must export `setup` and `teardown`.** Vitest reads them by name. A typo in the export gets you a confusing "server not running" error on the first `testGet` call.
|
|
4770
|
-
- **Port conflicts.** If `http.port` matches your running dev server, `
|
|
4857
|
+
- **Port conflicts.** If `http.port` matches your running dev server, the preflight fails with a `PortInUseError` naming the port. Either stop the dev server or pass a free one: `startHttpTestServer({ port: 3999 })`.
|
|
4771
4858
|
- **Auth tokens need a real user.** Generating a JWT with a non-existent `user_id` works — but the auth middleware's user-loading step will reject the request with 401 because it can't find the user in the DB.
|
|
4772
4859
|
- **`expectJson` parses the body once.** If you call it twice on the same response, the second call gets an already-consumed stream error. Capture the result.
|
|
4773
4860
|
- **The HTTP server's connection is NOT torn down between test files.** Data persists across files within a single `vitest` run. Either truncate in `afterEach` / `afterAll`, or design your tests to be order-independent.
|
package/llms.txt
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
- [send-mail](@warlock.js/core/send-mail/SKILL.md): Send transactional email — `Mail` fluent builder, `sendMail()` direct call, React Email components. Test mode auto-captures into an in-memory mailbox; dev mode logs. Triggers: `Mail.to`, `sendMail`, `setMailMode`, `mailEvents`, `assertMailSent`, `getTestMailbox`, `wasMailSentTo`, `closeAllMailers`; "send a transactional email", "build a React Email template", "configure SMTP or SES", "assert an email was sent in tests"; typical import `import { Mail, sendMail } from "@warlock.js/core"`. Skip: per-config wiring — `@warlock.js/core/configure-app/SKILL.md`; layered service patterns — `@warlock.js/core/warlock-conventions/SKILL.md`; competing libs `nodemailer` direct, `@sendgrid/mail`, `resend`, `mailgun.js`.
|
|
27
27
|
- [send-response](@warlock.js/core/send-response/SKILL.md): Send HTTP responses via @warlock.js/core's Response helpers — success/error variants, status helpers, redirects, files, streams, and SSE. Picking the right helper carries the HTTP semantic without manual status codes. Triggers: `response.success`, `response.successCreate`, `response.notFound`, `response.forbidden`, `response.badRequest`, `response.sendFile`, `response.stream`, `response.sse`, `response.replay`, `ResourceNotFoundError`, `ForbiddenError`; "return a 201 from a controller", "send a file", "stream Server-Sent Events", "throw HTTP-shaped errors from services"; typical import `import type { RequestHandler, Response } from "@warlock.js/core"`. Skip: controller shape — `@warlock.js/core/create-controller/SKILL.md`; route registration — `@warlock.js/core/register-route/SKILL.md`; competing patterns: hand-rolled status codes via `reply.code(404).send(...)`, raw Fastify reply.
|
|
28
28
|
- [store-file](@warlock.js/core/store-file/SKILL.md): Read/write/delete files via the `storage` singleton — disks, drivers (local/S3/R2/DO Spaces), `storage.use(name)`, `StorageFile` handles, presigned URLs. Triggers: `storage.put`, `storage.get`, `storage.use`, `StorageFile`, `storageConfigurations`, `getPresignedUrl`, `getPresignedUploadUrl`; "save an uploaded file", "switch between local and S3", "generate a presigned URL", "read file metadata"; typical import `import { storage } from "@warlock.js/core"`. Skip: multipart parsing + image chain — `@warlock.js/core/upload-file/SKILL.md`; image transforms — `@warlock.js/core/process-image/SKILL.md`; storage config shape — `@warlock.js/core/configure-app/SKILL.md`; competing libs `@aws-sdk/client-s3`, `multer`, `formidable`.
|
|
29
|
-
- [test-http](@warlock.js/core/test-http/SKILL.md): Integration tests against a real HTTP server — `startHttpTestServer()` boots one shared server in globalSetup, then `testGet` / `testPost` / `expectJson` make typed requests against it. Triggers: `startHttpTestServer`, `stopHttpTestServer`, `testGet`, `testPost`, `testPut`, `testPatch`, `testDelete`, `expectJson`, `getTestServerUrl`, `testRequest`; "integration-test a controller", "end-to-end HTTP test", "globalSetup HTTP server", "assert status and body shape"; typical import `import { testGet, testPost, expectJson } from "@warlock.js/core"`. Skip: pure unit tests — `@warlock.js/core/test-service/SKILL.md`; controller shape — `@warlock.js/core/create-controller/SKILL.md`; competing libs `supertest`, `light-my-request`, `nock`.
|
|
29
|
+
- [test-http](@warlock.js/core/test-http/SKILL.md): Integration tests against a real HTTP server — `startHttpTestServer()` boots one shared server in globalSetup, then `testGet` / `testPost` / `expectJson` make typed requests against it. Triggers: `startHttpTestServer`, `startHttpTestServer({ port })`, `stopHttpTestServer`, `testGet`, `testPost`, `testPut`, `testPatch`, `testDelete`, `expectJson`, `getTestServerUrl`, `testRequest`, `PortInUseError`, `assertPortIsAvailable`, `isPortAvailable`; "integration-test a controller", "end-to-end HTTP test", "globalSetup HTTP server", "assert status and body shape", "test server port already in use", "EADDRINUSE while running tests", "run tests while the dev server is up"; typical import `import { testGet, testPost, expectJson } from "@warlock.js/core"`. Skip: pure unit tests — `@warlock.js/core/test-service/SKILL.md`; controller shape — `@warlock.js/core/create-controller/SKILL.md`; competing libs `supertest`, `light-my-request`, `nock`.
|
|
30
30
|
- [test-service](@warlock.js/core/test-service/SKILL.md): Pure unit tests against services, repositories, models, and use-cases — `setupTest({ connectors })` bootstraps each Vitest worker with its own DB/cache connections so you can call your code directly. Triggers: `setupTest`, `src/test-setup.ts`, `tests.connectors`, `Application.setEnvironment`; "unit-test a service", "test a repository query", "vitest setupFiles", "skip connectors for pure-logic tests"; typical import `import { setupTest } from "@warlock.js/core"`. Skip: HTTP integration — `@warlock.js/core/test-http/SKILL.md`; warlock add test scaffold — `@warlock.js/core/write-cli-command/SKILL.md`; competing tooling: jest direct, `supertest`, `nock`.
|
|
31
31
|
- [update-packages](@warlock.js/core/update-packages/SKILL.md): Keep a project current with `warlock update` — bump every `@warlock.js/*` dependency in package.json to its latest published version (range operator preserved), then run the lockfile-detected package manager install. Also covers the `warlock dev` update notice, its `u` update-and-restart keyboard shortcut, and the `devServer.checkForUpdates` toggle. Triggers: `warlock update`, `--no-install`, `--dry-run`, `--check`, `checkForUpdates`, `fetchLatestVersion`, `isNewerVersion`; "update warlock packages", "upgrade the framework", "is there a new warlock version", "update notice in the dev server", "press u to update", "dev server keyboard shortcut", "update check offline", "bump @warlock.js/* to latest"; typical CLI `warlock update`. Skip: dev/build/start runtime — `@warlock.js/core/run-app/SKILL.md`; writing a custom command — `@warlock.js/core/write-cli-command/SKILL.md`; installing a NEW feature package (auth, mail, storage) — that is `warlock add`; releasing/publishing the framework — workspace release tooling, not this command.
|
|
32
32
|
- [upload-file](@warlock.js/core/upload-file/SKILL.md): Handle multipart file uploads — read via `request.file()` or `request.validated()`, validate with `v.file()`, save via `UploadedFile.save()` or the storage layer, transform images inline. Triggers: `UploadedFile`, `request.file`, `v.file`, `.save`, `.saveAs`, `.resize`, `.format`, `.quality`, `.image`, `.mimeType`, `.maxSize`; "accept a file upload", "validate file size and mime", "save to S3 or local disk", "resize an uploaded image on save"; typical import `import type { UploadedFile, RequestHandler } from "@warlock.js/core"`. Skip: storage drivers + presigned URLs — `@warlock.js/core/store-file/SKILL.md`; image-only transforms — `@warlock.js/core/process-image/SKILL.md`; schema rules — `@warlock.js/core/validate-input/SKILL.md`; competing libs `multer`, `formidable`, `busboy`.
|