@tulipes/core 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/dist/acl/acl.d.ts +49 -0
  2. package/dist/acl/acl.js +123 -0
  3. package/dist/acl/acl.js.map +1 -0
  4. package/dist/acl/index.d.ts +2 -0
  5. package/dist/acl/index.js +3 -0
  6. package/dist/acl/index.js.map +1 -0
  7. package/dist/acl/load-acl.d.ts +11 -0
  8. package/dist/acl/load-acl.js +38 -0
  9. package/dist/acl/load-acl.js.map +1 -0
  10. package/dist/boot/app-files.d.ts +17 -0
  11. package/dist/boot/app-files.js +31 -0
  12. package/dist/boot/app-files.js.map +1 -0
  13. package/dist/boot/banner.d.ts +9 -0
  14. package/dist/boot/banner.js +82 -0
  15. package/dist/boot/banner.js.map +1 -0
  16. package/dist/boot/boot.d.ts +32 -0
  17. package/dist/boot/boot.js +212 -0
  18. package/dist/boot/boot.js.map +1 -0
  19. package/dist/boot/contracts.d.ts +47 -0
  20. package/dist/boot/contracts.js +2 -0
  21. package/dist/boot/contracts.js.map +1 -0
  22. package/dist/boot/import-file.d.ts +7 -0
  23. package/dist/boot/import-file.js +23 -0
  24. package/dist/boot/import-file.js.map +1 -0
  25. package/dist/boot/index.d.ts +3 -0
  26. package/dist/boot/index.js +2 -0
  27. package/dist/boot/index.js.map +1 -0
  28. package/dist/cli/bin.d.ts +2 -0
  29. package/dist/cli/bin.js +7 -0
  30. package/dist/cli/bin.js.map +1 -0
  31. package/dist/cli/dev.d.ts +8 -0
  32. package/dist/cli/dev.js +22 -0
  33. package/dist/cli/dev.js.map +1 -0
  34. package/dist/cli/env-check.d.ts +6 -0
  35. package/dist/cli/env-check.js +28 -0
  36. package/dist/cli/env-check.js.map +1 -0
  37. package/dist/cli/main.d.ts +1 -0
  38. package/dist/cli/main.js +45 -0
  39. package/dist/cli/main.js.map +1 -0
  40. package/dist/cli/new-module.d.ts +6 -0
  41. package/dist/cli/new-module.js +84 -0
  42. package/dist/cli/new-module.js.map +1 -0
  43. package/dist/cli/sync.d.ts +12 -0
  44. package/dist/cli/sync.js +117 -0
  45. package/dist/cli/sync.js.map +1 -0
  46. package/dist/config/build-config.d.ts +13 -0
  47. package/dist/config/build-config.js +63 -0
  48. package/dist/config/build-config.js.map +1 -0
  49. package/dist/config/index.d.ts +13 -0
  50. package/dist/config/index.js +2 -0
  51. package/dist/config/index.js.map +1 -0
  52. package/dist/db/database.d.ts +25 -0
  53. package/dist/db/database.js +111 -0
  54. package/dist/db/database.js.map +1 -0
  55. package/dist/db/index.d.ts +2 -0
  56. package/dist/db/index.js +3 -0
  57. package/dist/db/index.js.map +1 -0
  58. package/dist/db/model-store.d.ts +22 -0
  59. package/dist/db/model-store.js +37 -0
  60. package/dist/db/model-store.js.map +1 -0
  61. package/dist/env/coerce.d.ts +17 -0
  62. package/dist/env/coerce.js +53 -0
  63. package/dist/env/coerce.js.map +1 -0
  64. package/dist/env/environment.d.ts +42 -0
  65. package/dist/env/environment.js +117 -0
  66. package/dist/env/environment.js.map +1 -0
  67. package/dist/env/index.d.ts +14 -0
  68. package/dist/env/index.js +5 -0
  69. package/dist/env/index.js.map +1 -0
  70. package/dist/env/meta-discovery.d.ts +17 -0
  71. package/dist/env/meta-discovery.js +65 -0
  72. package/dist/env/meta-discovery.js.map +1 -0
  73. package/dist/env/spec.d.ts +133 -0
  74. package/dist/env/spec.js +63 -0
  75. package/dist/env/spec.js.map +1 -0
  76. package/dist/env/variable-store.d.ts +26 -0
  77. package/dist/env/variable-store.js +49 -0
  78. package/dist/env/variable-store.js.map +1 -0
  79. package/dist/errors/boot-error.d.ts +28 -0
  80. package/dist/errors/boot-error.js +44 -0
  81. package/dist/errors/boot-error.js.map +1 -0
  82. package/dist/errors/index.d.ts +1 -0
  83. package/dist/errors/index.js +2 -0
  84. package/dist/errors/index.js.map +1 -0
  85. package/dist/http/error-handler.d.ts +23 -0
  86. package/dist/http/error-handler.js +48 -0
  87. package/dist/http/error-handler.js.map +1 -0
  88. package/dist/http/index.d.ts +1 -0
  89. package/dist/http/index.js +2 -0
  90. package/dist/http/index.js.map +1 -0
  91. package/dist/index.d.ts +10 -0
  92. package/dist/index.js +11 -0
  93. package/dist/index.js.map +1 -0
  94. package/dist/modules/discovery.d.ts +10 -0
  95. package/dist/modules/discovery.js +91 -0
  96. package/dist/modules/discovery.js.map +1 -0
  97. package/dist/modules/explorer.d.ts +38 -0
  98. package/dist/modules/explorer.js +64 -0
  99. package/dist/modules/explorer.js.map +1 -0
  100. package/dist/modules/graph.d.ts +16 -0
  101. package/dist/modules/graph.js +80 -0
  102. package/dist/modules/graph.js.map +1 -0
  103. package/dist/modules/index.d.ts +4 -0
  104. package/dist/modules/index.js +5 -0
  105. package/dist/modules/index.js.map +1 -0
  106. package/dist/modules/manifest.d.ts +43 -0
  107. package/dist/modules/manifest.js +26 -0
  108. package/dist/modules/manifest.js.map +1 -0
  109. package/dist/queues/index.d.ts +2 -0
  110. package/dist/queues/index.js +3 -0
  111. package/dist/queues/index.js.map +1 -0
  112. package/dist/queues/load-queues.d.ts +12 -0
  113. package/dist/queues/load-queues.js +56 -0
  114. package/dist/queues/load-queues.js.map +1 -0
  115. package/dist/queues/queue-manager.d.ts +44 -0
  116. package/dist/queues/queue-manager.js +134 -0
  117. package/dist/queues/queue-manager.js.map +1 -0
  118. package/dist/sockets/index.d.ts +2 -0
  119. package/dist/sockets/index.js +3 -0
  120. package/dist/sockets/index.js.map +1 -0
  121. package/dist/sockets/load-sockets.d.ts +13 -0
  122. package/dist/sockets/load-sockets.js +42 -0
  123. package/dist/sockets/load-sockets.js.map +1 -0
  124. package/dist/sockets/socket-manager.d.ts +39 -0
  125. package/dist/sockets/socket-manager.js +85 -0
  126. package/dist/sockets/socket-manager.js.map +1 -0
  127. package/package.json +65 -0
@@ -0,0 +1,25 @@
1
+ import { Schema, type Connection } from "mongoose";
2
+ import { BootReport } from "../errors/boot-error.js";
3
+ import type { Ctx } from "../boot/contracts.js";
4
+ import type { ModuleFiles } from "../modules/explorer.js";
5
+ /**
6
+ * Model files are pure definitions — no factory, no ctx. The loader turns
7
+ * them into live models on the shared connection.
8
+ */
9
+ export interface ModelDef {
10
+ name: string;
11
+ schema: Schema;
12
+ /** Override mongoose's automatic collection naming when needed. */
13
+ collection?: string;
14
+ }
15
+ /** Default export of `bootstrap/*.bootstrap.ts` — post-DB, pre-server init. */
16
+ export type BootstrapFn = (ctx: Ctx) => void | Promise<void>;
17
+ /**
18
+ * Boot phase 8: connect, register every model in load order, then run every
19
+ * bootstrap task in load order (indexes, seed roles, default records).
20
+ *
21
+ * A DB-less app is legitimate — no models and no MONGO_URI simply skips the
22
+ * phase. Declaring models *without* a connection string, though, is a
23
+ * contradiction the boot must refuse.
24
+ */
25
+ export declare function initDatabase(explored: readonly ModuleFiles[], ctx: Ctx, report: BootReport): Promise<Connection | undefined>;
@@ -0,0 +1,111 @@
1
+ import { createConnection, Schema } from "mongoose";
2
+ import { BootReport } from "../errors/boot-error.js";
3
+ import { importFile } from "../boot/import-file.js";
4
+ import { ModelStore } from "./model-store.js";
5
+ /** Well-known infra variable, declared by the app's sys core module. */
6
+ const MONGO_URI = "MONGO_URI";
7
+ /**
8
+ * Boot phase 8: connect, register every model in load order, then run every
9
+ * bootstrap task in load order (indexes, seed roles, default records).
10
+ *
11
+ * A DB-less app is legitimate — no models and no MONGO_URI simply skips the
12
+ * phase. Declaring models *without* a connection string, though, is a
13
+ * contradiction the boot must refuse.
14
+ */
15
+ export async function initDatabase(explored, ctx, report) {
16
+ const hasModels = explored.some((files) => files.modelFiles.length > 0);
17
+ const uri = ctx.Environment.store.has(MONGO_URI)
18
+ ? String(ctx.Environment.get(MONGO_URI))
19
+ : undefined;
20
+ if (!uri) {
21
+ if (hasModels) {
22
+ report.add({
23
+ scope: "boot",
24
+ message: `modules declare models but "${MONGO_URI}" is not declared/set — a database is required`,
25
+ });
26
+ }
27
+ return undefined;
28
+ }
29
+ let connection;
30
+ try {
31
+ // Fail fast: default server selection waits 30s before reporting an
32
+ // unreachable database — far too long for a boot-time verdict.
33
+ connection = await createConnection(uri, {
34
+ serverSelectionTimeoutMS: 5_000,
35
+ }).asPromise();
36
+ }
37
+ catch (error) {
38
+ report.add({
39
+ scope: "boot",
40
+ message: `database connection failed (${MONGO_URI}): ${error.message}`,
41
+ });
42
+ return undefined;
43
+ }
44
+ const store = new ModelStore();
45
+ ctx.models = store;
46
+ await registerModels(explored, connection, store, report);
47
+ // Bootstrap tasks only make sense against a fully-populated model store,
48
+ // and only when nothing above them failed.
49
+ if (report.issues.length === 0) {
50
+ await runBootstrapTasks(explored, ctx, report);
51
+ }
52
+ return connection;
53
+ }
54
+ async function registerModels(explored, connection, store, report) {
55
+ for (const { module, modelFiles } of explored) {
56
+ for (const file of modelFiles) {
57
+ const exported = await importFile(file, "boot", module.name, report);
58
+ if (!exported)
59
+ continue;
60
+ const def = exported.default;
61
+ if (typeof def?.name !== "string" ||
62
+ !(def.schema instanceof Schema)) {
63
+ report.add({
64
+ scope: "boot",
65
+ module: module.name,
66
+ message: `${file} must default-export { name, schema } (a ModelDef)`,
67
+ });
68
+ continue;
69
+ }
70
+ const owner = store.ownerOf(def.name);
71
+ if (owner) {
72
+ report.add({
73
+ scope: "boot",
74
+ module: module.name,
75
+ message: `model "${def.name}" is already registered by module "${owner}"`,
76
+ });
77
+ continue;
78
+ }
79
+ const model = connection.model(def.name, def.schema, def.collection);
80
+ store.register_(def.name, model, module.name);
81
+ }
82
+ }
83
+ }
84
+ async function runBootstrapTasks(explored, ctx, report) {
85
+ for (const { module, bootstrapFiles } of explored) {
86
+ for (const file of bootstrapFiles) {
87
+ const exported = await importFile(file, "boot", module.name, report);
88
+ if (!exported)
89
+ continue;
90
+ if (typeof exported.default !== "function") {
91
+ report.add({
92
+ scope: "boot",
93
+ module: module.name,
94
+ message: `${file} must default-export a bootstrap function (ctx) => void`,
95
+ });
96
+ continue;
97
+ }
98
+ try {
99
+ await exported.default(ctx);
100
+ }
101
+ catch (error) {
102
+ report.add({
103
+ scope: "boot",
104
+ module: module.name,
105
+ message: `bootstrap task ${file} threw: ${error.message}`,
106
+ });
107
+ }
108
+ }
109
+ }
110
+ }
111
+ //# sourceMappingURL=database.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"database.js","sourceRoot":"","sources":["../../src/db/database.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export { initDatabase, type BootstrapFn, type ModelDef } from "./database.js";
2
+ export { ModelStore } from "./model-store.js";
@@ -0,0 +1,3 @@
1
+ export { initDatabase } from "./database.js";
2
+ export { ModelStore } from "./model-store.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/db/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAmC,MAAM,eAAe,CAAC;AAC9E,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,22 @@
1
+ import type { Model } from "mongoose";
2
+ /**
3
+ * Global model registry, populated from every module's `models/*.model.ts`
4
+ * during phase 8.
5
+ *
6
+ * Same fail-closed read contract as the variable store: asking for a model
7
+ * nobody registered throws at the call site — a typo'd model name must never
8
+ * travel as an undefined into query code.
9
+ */
10
+ export declare class ModelStore {
11
+ #private;
12
+ /** @internal Database loader only. */
13
+ register_(name: string, model: Model<never>, owner: string): void;
14
+ has(name: string): boolean;
15
+ ownerOf(name: string): string | undefined;
16
+ names(): string[];
17
+ /**
18
+ * The generic is a caller-side assertion — the store can't verify it.
19
+ * Typed accessors per model come with the `tulipes sync` codegen.
20
+ */
21
+ get<T = unknown>(name: string): Model<T>;
22
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Global model registry, populated from every module's `models/*.model.ts`
3
+ * during phase 8.
4
+ *
5
+ * Same fail-closed read contract as the variable store: asking for a model
6
+ * nobody registered throws at the call site — a typo'd model name must never
7
+ * travel as an undefined into query code.
8
+ */
9
+ export class ModelStore {
10
+ /** name → { model, owning module } */
11
+ #models = new Map();
12
+ /** @internal Database loader only. */
13
+ register_(name, model, owner) {
14
+ this.#models.set(name, { model, owner });
15
+ }
16
+ has(name) {
17
+ return this.#models.has(name);
18
+ }
19
+ ownerOf(name) {
20
+ return this.#models.get(name)?.owner;
21
+ }
22
+ names() {
23
+ return [...this.#models.keys()];
24
+ }
25
+ /**
26
+ * The generic is a caller-side assertion — the store can't verify it.
27
+ * Typed accessors per model come with the `tulipes sync` codegen.
28
+ */
29
+ get(name) {
30
+ const stored = this.#models.get(name);
31
+ if (!stored) {
32
+ throw new Error(`Model "${name}" was never registered by any module (known: ${this.names().join(", ") || "none"})`);
33
+ }
34
+ return stored.model;
35
+ }
36
+ }
37
+ //# sourceMappingURL=model-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"model-store.js","sourceRoot":"","sources":["../../src/db/model-store.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,MAAM,OAAO,UAAU;IACrB,sCAAsC;IAC7B,OAAO,GAAG,IAAI,GAAG,EAAkD,CAAC;IAE7E,sCAAsC;IACtC,SAAS,CAAC,IAAY,EAAE,KAAmB,EAAE,KAAa;QACxD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;IAC3C,CAAC;IAED,GAAG,CAAC,IAAY;QACd,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAChC,CAAC;IAED,OAAO,CAAC,IAAY;QAClB,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC;IACvC,CAAC;IAED,KAAK;QACH,OAAO,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC;IAClC,CAAC;IAED;;;OAGG;IACH,GAAG,CAAc,IAAY;QAC3B,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACtC,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,MAAM,IAAI,KAAK,CACb,UAAU,IAAI,gDAAgD,IAAI,CAAC,KAAK,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,MAAM,GAAG,CACnG,CAAC;QACJ,CAAC;QACD,OAAO,MAAM,CAAC,KAA4B,CAAC;IAC7C,CAAC;CACF"}
@@ -0,0 +1,17 @@
1
+ import type { VariableSpec, VariableValue } from "./spec.js";
2
+ /**
3
+ * Turn a raw value (a string from the shell / .env file, or a typed JSON
4
+ * default) into the type its spec declares — or explain why it can't be.
5
+ *
6
+ * Env sources only ever give us strings, while JSON defaults arrive already
7
+ * typed; both funnel through here so a bad *default* is caught with the same
8
+ * rigor as a bad deployment value.
9
+ */
10
+ export type CoerceResult = {
11
+ ok: true;
12
+ value: VariableValue;
13
+ } | {
14
+ ok: false;
15
+ reason: string;
16
+ };
17
+ export declare function coerceValue(spec: VariableSpec, raw: VariableValue): CoerceResult;
@@ -0,0 +1,53 @@
1
+ const TRUTHY = new Set(["true", "1", "yes", "on"]);
2
+ const FALSY = new Set(["false", "0", "no", "off"]);
3
+ export function coerceValue(spec, raw) {
4
+ switch (spec.type) {
5
+ case "string":
6
+ case "secret":
7
+ return typeof raw === "string"
8
+ ? { ok: true, value: raw }
9
+ : { ok: false, reason: `expected a string, got ${typeof raw}` };
10
+ case "number": {
11
+ if (typeof raw === "number")
12
+ return { ok: true, value: raw };
13
+ const parsed = Number(raw);
14
+ return Number.isFinite(parsed)
15
+ ? { ok: true, value: parsed }
16
+ : { ok: false, reason: `"${String(raw)}" is not a number` };
17
+ }
18
+ case "boolean": {
19
+ if (typeof raw === "boolean")
20
+ return { ok: true, value: raw };
21
+ const normalized = String(raw).trim().toLowerCase();
22
+ if (TRUTHY.has(normalized))
23
+ return { ok: true, value: true };
24
+ if (FALSY.has(normalized))
25
+ return { ok: true, value: false };
26
+ return {
27
+ ok: false,
28
+ reason: `"${String(raw)}" is not a boolean (use true/false, 1/0, yes/no, on/off)`,
29
+ };
30
+ }
31
+ case "enum": {
32
+ const value = String(raw);
33
+ // spec.enum presence is guaranteed by schema validation upstream.
34
+ return spec.enum.includes(value)
35
+ ? { ok: true, value }
36
+ : {
37
+ ok: false,
38
+ reason: `"${value}" is not one of: ${spec.enum.join(", ")}`,
39
+ };
40
+ }
41
+ case "url": {
42
+ const value = String(raw);
43
+ try {
44
+ new URL(value);
45
+ return { ok: true, value };
46
+ }
47
+ catch {
48
+ return { ok: false, reason: `"${value}" is not a valid URL` };
49
+ }
50
+ }
51
+ }
52
+ }
53
+ //# sourceMappingURL=coerce.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coerce.js","sourceRoot":"","sources":["../../src/env/coerce.ts"],"names":[],"mappings":"AAeA,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC;AACnD,MAAM,KAAK,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC;AAEnD,MAAM,UAAU,WAAW,CAAC,IAAkB,EAAE,GAAkB;IAChE,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC;QAClB,KAAK,QAAQ,CAAC;QACd,KAAK,QAAQ;YACX,OAAO,OAAO,GAAG,KAAK,QAAQ;gBAC5B,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE;gBAC1B,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,0BAA0B,OAAO,GAAG,EAAE,EAAE,CAAC;QAEpE,KAAK,QAAQ,EAAE,CAAC;YACd,IAAI,OAAO,GAAG,KAAK,QAAQ;gBAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC;YAC7D,MAAM,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;YAC3B,OAAO,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;gBAC5B,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE;gBAC7B,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,MAAM,CAAC,GAAG,CAAC,mBAAmB,EAAE,CAAC;QAChE,CAAC;QAED,KAAK,SAAS,EAAE,CAAC;YACf,IAAI,OAAO,GAAG,KAAK,SAAS;gBAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,CAAC;YAC9D,MAAM,UAAU,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;YACpD,IAAI,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC;gBAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;YAC7D,IAAI,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC;gBAAE,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;YAC7D,OAAO;gBACL,EAAE,EAAE,KAAK;gBACT,MAAM,EAAE,IAAI,MAAM,CAAC,GAAG,CAAC,0DAA0D;aAClF,CAAC;QACJ,CAAC;QAED,KAAK,MAAM,EAAE,CAAC;YACZ,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;YAC1B,kEAAkE;YAClE,OAAO,IAAI,CAAC,IAAK,CAAC,QAAQ,CAAC,KAAK,CAAC;gBAC/B,CAAC,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE;gBACrB,CAAC,CAAC;oBACE,EAAE,EAAE,KAAK;oBACT,MAAM,EAAE,IAAI,KAAK,oBAAoB,IAAI,CAAC,IAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;iBAC7D,CAAC;QACR,CAAC;QAED,KAAK,KAAK,EAAE,CAAC;YACX,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;YAC1B,IAAI,CAAC;gBACH,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;gBACf,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;YAC7B,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,KAAK,sBAAsB,EAAE,CAAC;YAChE,CAAC;QACH,CAAC;IACH,CAAC;AACH,CAAC"}
@@ -0,0 +1,42 @@
1
+ import { VariableStore } from "./variable-store.js";
2
+ import type { VariableValue } from "./spec.js";
3
+ import type { KnownVariables } from "./index.js";
4
+ /** Falls back to the broad VariableValue until codegen narrows the key. */
5
+ type Resolved<K extends string> = K extends keyof KnownVariables ? KnownVariables[K] : VariableValue;
6
+ /**
7
+ * APP_ENV drives which .env file loads. It is deliberately NOT NODE_ENV:
8
+ * bundlers and libraries overload NODE_ENV as an optimization flag, so Tulipes
9
+ * keeps its own axis and mirrors a sane NODE_ENV for third-party code.
10
+ */
11
+ export declare const appEnvs: readonly ["development", "staging", "production", "test"];
12
+ export type AppEnv = (typeof appEnvs)[number];
13
+ export interface EnvironmentOptions {
14
+ /** App root — the directory containing `modules/` and `.envs/`. */
15
+ rootDir: string;
16
+ /** Override the mode; defaults to process.env.APP_ENV, then "development". */
17
+ appEnv?: AppEnv;
18
+ modulesDir?: string;
19
+ envsDir?: string;
20
+ }
21
+ /**
22
+ * Boot phases 1–3: load the mode's .env file, discover every module's
23
+ * meta.variables.json, resolve each declared variable with precedence
24
+ *
25
+ * process.env > .envs/.env.<APP_ENV> > spec default
26
+ *
27
+ * (container-injected values must always win), validate everything, and
28
+ * crash once with the aggregate report if anything is missing or invalid.
29
+ */
30
+ export declare class Environment {
31
+ #private;
32
+ readonly appEnv: AppEnv;
33
+ readonly store: VariableStore;
34
+ private constructor();
35
+ /**
36
+ * Convenience passthrough: `env.get("PORT")` ≡ `env.store.get("PORT")` —
37
+ * with per-variable types once `tulipes sync` has generated config.d.ts.
38
+ */
39
+ get<K extends string>(name: K): Resolved<K>;
40
+ static load(options: EnvironmentOptions): Environment;
41
+ }
42
+ export {};
@@ -0,0 +1,117 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { parse as parseDotenv } from "dotenv";
4
+ import { BootReport } from "../errors/boot-error.js";
5
+ import { coerceValue } from "./coerce.js";
6
+ import { discoverModuleMeta } from "./meta-discovery.js";
7
+ import { VariableStore } from "./variable-store.js";
8
+ /**
9
+ * APP_ENV drives which .env file loads. It is deliberately NOT NODE_ENV:
10
+ * bundlers and libraries overload NODE_ENV as an optimization flag, so Tulipes
11
+ * keeps its own axis and mirrors a sane NODE_ENV for third-party code.
12
+ */
13
+ export const appEnvs = ["development", "staging", "production", "test"];
14
+ /**
15
+ * Boot phases 1–3: load the mode's .env file, discover every module's
16
+ * meta.variables.json, resolve each declared variable with precedence
17
+ *
18
+ * process.env > .envs/.env.<APP_ENV> > spec default
19
+ *
20
+ * (container-injected values must always win), validate everything, and
21
+ * crash once with the aggregate report if anything is missing or invalid.
22
+ */
23
+ export class Environment {
24
+ appEnv;
25
+ store;
26
+ constructor(appEnv, store) {
27
+ this.appEnv = appEnv;
28
+ this.store = store;
29
+ }
30
+ /**
31
+ * Convenience passthrough: `env.get("PORT")` ≡ `env.store.get("PORT")` —
32
+ * with per-variable types once `tulipes sync` has generated config.d.ts.
33
+ */
34
+ get(name) {
35
+ return this.store.get(name);
36
+ }
37
+ static load(options) {
38
+ const report = new BootReport();
39
+ const appEnv = Environment.#resolveAppEnv(options.appEnv, report);
40
+ // Mirror NODE_ENV for third-party libs: anything that isn't clearly
41
+ // dev/test should get production-grade behavior from them.
42
+ process.env.NODE_ENV ??=
43
+ appEnv === "development" || appEnv === "test" ? appEnv : "production";
44
+ const envsDir = options.envsDir ?? join(options.rootDir, ".envs");
45
+ const modulesDir = options.modulesDir ?? join(options.rootDir, "modules");
46
+ const fileValues = Environment.#readEnvFile(join(envsDir, `.env.${appEnv}`));
47
+ const metas = discoverModuleMeta(modulesDir, report);
48
+ const store = new VariableStore();
49
+ Environment.#resolveAll(metas, fileValues, store, report);
50
+ report.throwIfAny("environment");
51
+ return new Environment(appEnv, store);
52
+ }
53
+ static #resolveAppEnv(override, report) {
54
+ const raw = override ?? process.env.APP_ENV ?? "development";
55
+ if (appEnvs.includes(raw))
56
+ return raw;
57
+ report.add({
58
+ scope: "env",
59
+ message: `APP_ENV "${raw}" is invalid — expected one of: ${appEnvs.join(", ")}`,
60
+ });
61
+ return "development"; // placeholder; the report will crash the boot anyway
62
+ }
63
+ /**
64
+ * A missing file is not an error: production containers typically inject
65
+ * everything through real environment variables and ship no .env at all.
66
+ */
67
+ static #readEnvFile(filePath) {
68
+ try {
69
+ return parseDotenv(readFileSync(filePath, "utf8"));
70
+ }
71
+ catch {
72
+ return {};
73
+ }
74
+ }
75
+ static #resolveAll(metas, fileValues, store, report) {
76
+ // Every variable has exactly one owning module. Shared infra variables
77
+ // (MONGO_URI, PORT…) belong to the sys core module; everyone else reads.
78
+ const owners = new Map();
79
+ for (const meta of metas) {
80
+ for (const spec of meta.variables) {
81
+ const existingOwner = owners.get(spec.name);
82
+ if (existingOwner) {
83
+ report.add({
84
+ scope: "env",
85
+ module: meta.module,
86
+ message: `variable "${spec.name}" is already declared by module "${existingOwner}" — one owner per variable`,
87
+ });
88
+ continue;
89
+ }
90
+ owners.set(spec.name, meta.module);
91
+ const raw = process.env[spec.name] ?? fileValues[spec.name] ?? spec.default;
92
+ if (raw === undefined) {
93
+ if (spec.required) {
94
+ report.add({
95
+ scope: "env",
96
+ module: meta.module,
97
+ message: `required variable "${spec.name}" has no value (${spec.description})`,
98
+ });
99
+ }
100
+ // Optional without default: legitimately absent, nothing to store.
101
+ continue;
102
+ }
103
+ const coerced = coerceValue(spec, raw);
104
+ if (!coerced.ok) {
105
+ report.add({
106
+ scope: "env",
107
+ module: meta.module,
108
+ message: `variable "${spec.name}": ${coerced.reason}`,
109
+ });
110
+ continue;
111
+ }
112
+ store.declare_(spec, meta.module, coerced.value);
113
+ }
114
+ }
115
+ }
116
+ }
117
+ //# sourceMappingURL=environment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"environment.js","sourceRoot":"","sources":["../../src/env/environment.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ export { Environment, appEnvs, type AppEnv, type EnvironmentOptions } from "./environment.js";
2
+ /**
3
+ * Augmentation target for the generated `types/config.d.ts` (`tulipes sync`):
4
+ * one property per declared variable, typed from its meta spec. Empty here on
5
+ * purpose — an app without codegen still gets `VariableValue` from get().
6
+ *
7
+ * Declared in THIS module (not re-exported) because TypeScript module
8
+ * augmentation only merges with the module that declares the interface.
9
+ */
10
+ export interface KnownVariables {
11
+ }
12
+ export { VariableStore } from "./variable-store.js";
13
+ export { discoverModuleMeta, META_FILENAME, type ModuleMeta } from "./meta-discovery.js";
14
+ export { metaFileSchema, variableSpecSchema, variableTypes, type MetaFile, type VariableSpec, type VariableType, type VariableValue, } from "./spec.js";
@@ -0,0 +1,5 @@
1
+ export { Environment, appEnvs } from "./environment.js";
2
+ export { VariableStore } from "./variable-store.js";
3
+ export { discoverModuleMeta, META_FILENAME } from "./meta-discovery.js";
4
+ export { metaFileSchema, variableSpecSchema, variableTypes, } from "./spec.js";
5
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/env/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,OAAO,EAAwC,MAAM,kBAAkB,CAAC;AAW9F,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAmB,MAAM,qBAAqB,CAAC;AACzF,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,aAAa,GAKd,MAAM,WAAW,CAAC"}
@@ -0,0 +1,17 @@
1
+ import { BootReport } from "../errors/boot-error.js";
2
+ import { type VariableSpec } from "./spec.js";
3
+ export declare const META_FILENAME = "meta.variables.json";
4
+ export interface ModuleMeta {
5
+ /** Folder name under modules/ — enough for attribution at this phase. */
6
+ module: string;
7
+ filePath: string;
8
+ variables: VariableSpec[];
9
+ }
10
+ /**
11
+ * Walk `modules/*` and load every module's env contract.
12
+ *
13
+ * A module without a meta file simply declares no variables — that's valid,
14
+ * not an error. Malformed files, on the other hand, are collected into the
15
+ * report (never thrown one-by-one) so the developer sees all of them at once.
16
+ */
17
+ export declare function discoverModuleMeta(modulesDir: string, report: BootReport): ModuleMeta[];
@@ -0,0 +1,65 @@
1
+ import { readdirSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { BootReport } from "../errors/boot-error.js";
4
+ import { metaFileSchema } from "./spec.js";
5
+ export const META_FILENAME = "meta.variables.json";
6
+ /**
7
+ * Walk `modules/*` and load every module's env contract.
8
+ *
9
+ * A module without a meta file simply declares no variables — that's valid,
10
+ * not an error. Malformed files, on the other hand, are collected into the
11
+ * report (never thrown one-by-one) so the developer sees all of them at once.
12
+ */
13
+ export function discoverModuleMeta(modulesDir, report) {
14
+ let entries;
15
+ try {
16
+ entries = readdirSync(modulesDir, { withFileTypes: true });
17
+ }
18
+ catch {
19
+ report.add({
20
+ scope: "env",
21
+ message: `modules directory not found: ${modulesDir}`,
22
+ });
23
+ return [];
24
+ }
25
+ const metas = [];
26
+ for (const entry of entries) {
27
+ if (!entry.isDirectory())
28
+ continue;
29
+ const module = entry.name;
30
+ const filePath = join(modulesDir, module, META_FILENAME);
31
+ let text;
32
+ try {
33
+ text = readFileSync(filePath, "utf8");
34
+ }
35
+ catch {
36
+ continue; // no meta file — module needs no env variables
37
+ }
38
+ let json;
39
+ try {
40
+ json = JSON.parse(text);
41
+ }
42
+ catch (error) {
43
+ report.add({
44
+ scope: "env",
45
+ module,
46
+ message: `${META_FILENAME} is not valid JSON: ${error.message}`,
47
+ });
48
+ continue;
49
+ }
50
+ const parsed = metaFileSchema.safeParse(json);
51
+ if (!parsed.success) {
52
+ for (const issue of parsed.error.issues) {
53
+ report.add({
54
+ scope: "env",
55
+ module,
56
+ message: `${META_FILENAME}: ${issue.message}`,
57
+ });
58
+ }
59
+ continue;
60
+ }
61
+ metas.push({ module, filePath, variables: parsed.data.variables });
62
+ }
63
+ return metas;
64
+ }
65
+ //# sourceMappingURL=meta-discovery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"meta-discovery.js","sourceRoot":"","sources":["../../src/env/meta-discovery.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,cAAc,EAAqB,MAAM,WAAW,CAAC;AAE9D,MAAM,CAAC,MAAM,aAAa,GAAG,qBAAqB,CAAC;AASnD;;;;;;GAMG;AACH,MAAM,UAAU,kBAAkB,CAChC,UAAkB,EAClB,MAAkB;IAElB,IAAI,OAAO,CAAC;IACZ,IAAI,CAAC;QACH,OAAO,GAAG,WAAW,CAAC,UAAU,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7D,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,CAAC,GAAG,CAAC;YACT,KAAK,EAAE,KAAK;YACZ,OAAO,EAAE,gCAAgC,UAAU,EAAE;SACtD,CAAC,CAAC;QACH,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,KAAK,GAAiB,EAAE,CAAC;IAE/B,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,IAAI,CAAC,KAAK,CAAC,WAAW,EAAE;YAAE,SAAS;QACnC,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC;QAC1B,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,EAAE,MAAM,EAAE,aAAa,CAAC,CAAC;QAEzD,IAAI,IAAY,CAAC;QACjB,IAAI,CAAC;YACH,IAAI,GAAG,YAAY,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QACxC,CAAC;QAAC,MAAM,CAAC;YACP,SAAS,CAAC,+CAA+C;QAC3D,CAAC;QAED,IAAI,IAAa,CAAC;QAClB,IAAI,CAAC;YACH,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC1B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,GAAG,CAAC;gBACT,KAAK,EAAE,KAAK;gBACZ,MAAM;gBACN,OAAO,EAAE,GAAG,aAAa,uBAAwB,KAAe,CAAC,OAAO,EAAE;aAC3E,CAAC,CAAC;YACH,SAAS;QACX,CAAC;QAED,MAAM,MAAM,GAAG,cAAc,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QAC9C,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YACpB,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC;gBACxC,MAAM,CAAC,GAAG,CAAC;oBACT,KAAK,EAAE,KAAK;oBACZ,MAAM;oBACN,OAAO,EAAE,GAAG,aAAa,KAAK,KAAK,CAAC,OAAO,EAAE;iBAC9C,CAAC,CAAC;YACL,CAAC;YACD,SAAS;QACX,CAAC;QAED,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;IACrE,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC"}