@softure-ai/core 0.0.0-stage → 0.1.5

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 (103) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +243 -2
  3. package/dist/cli/index.d.ts +2 -0
  4. package/dist/cli/index.d.ts.map +1 -0
  5. package/dist/cli/index.js +4 -0
  6. package/dist/cli/index.js.map +1 -0
  7. package/dist/cli/load-config.d.ts +49 -0
  8. package/dist/cli/load-config.d.ts.map +1 -0
  9. package/dist/cli/load-config.js +73 -0
  10. package/dist/cli/load-config.js.map +1 -0
  11. package/dist/clock.d.ts +15 -0
  12. package/dist/clock.d.ts.map +1 -0
  13. package/dist/clock.js +26 -0
  14. package/dist/clock.js.map +1 -0
  15. package/dist/config-error.d.ts +12 -0
  16. package/dist/config-error.d.ts.map +1 -0
  17. package/dist/config-error.js +20 -0
  18. package/dist/config-error.js.map +1 -0
  19. package/dist/config.d.ts +39 -0
  20. package/dist/config.d.ts.map +1 -0
  21. package/dist/config.js +161 -0
  22. package/dist/config.js.map +1 -0
  23. package/dist/database-requirement.d.ts +9 -0
  24. package/dist/database-requirement.d.ts.map +1 -0
  25. package/dist/database-requirement.js +26 -0
  26. package/dist/database-requirement.js.map +1 -0
  27. package/dist/i18n.d.ts +37 -0
  28. package/dist/i18n.d.ts.map +1 -0
  29. package/dist/i18n.js +58 -0
  30. package/dist/i18n.js.map +1 -0
  31. package/dist/index.d.ts +14 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +16 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/manifest.d.ts +30 -0
  36. package/dist/manifest.d.ts.map +1 -0
  37. package/dist/manifest.js +48 -0
  38. package/dist/manifest.js.map +1 -0
  39. package/dist/messages/en.d.ts +7 -0
  40. package/dist/messages/en.d.ts.map +1 -0
  41. package/dist/messages/en.js +7 -0
  42. package/dist/messages/en.js.map +1 -0
  43. package/dist/messages/index.d.ts +6 -0
  44. package/dist/messages/index.d.ts.map +1 -0
  45. package/dist/messages/index.js +5 -0
  46. package/dist/messages/index.js.map +1 -0
  47. package/dist/messages/pl.d.ts +3 -0
  48. package/dist/messages/pl.d.ts.map +1 -0
  49. package/dist/messages/pl.js +7 -0
  50. package/dist/messages/pl.js.map +1 -0
  51. package/dist/module.d.ts +99 -0
  52. package/dist/module.d.ts.map +1 -0
  53. package/dist/module.js +137 -0
  54. package/dist/module.js.map +1 -0
  55. package/dist/next/index.d.ts +2 -0
  56. package/dist/next/index.d.ts.map +1 -0
  57. package/dist/next/index.js +4 -0
  58. package/dist/next/index.js.map +1 -0
  59. package/dist/next/registry.d.ts +8 -0
  60. package/dist/next/registry.d.ts.map +1 -0
  61. package/dist/next/registry.js +20 -0
  62. package/dist/next/registry.js.map +1 -0
  63. package/dist/result.d.ts +17 -0
  64. package/dist/result.d.ts.map +1 -0
  65. package/dist/result.js +10 -0
  66. package/dist/result.js.map +1 -0
  67. package/dist/safe-error.d.ts +14 -0
  68. package/dist/safe-error.d.ts.map +1 -0
  69. package/dist/safe-error.js +43 -0
  70. package/dist/safe-error.js.map +1 -0
  71. package/dist/site-urls.d.ts +14 -0
  72. package/dist/site-urls.d.ts.map +1 -0
  73. package/dist/site-urls.js +26 -0
  74. package/dist/site-urls.js.map +1 -0
  75. package/dist/switches.d.ts +22 -0
  76. package/dist/switches.d.ts.map +1 -0
  77. package/dist/switches.js +19 -0
  78. package/dist/switches.js.map +1 -0
  79. package/dist/version-range.d.ts +14 -0
  80. package/dist/version-range.d.ts.map +1 -0
  81. package/dist/version-range.js +70 -0
  82. package/dist/version-range.js.map +1 -0
  83. package/package.json +47 -4
  84. package/src/cli/index.ts +15 -0
  85. package/src/cli/load-config.ts +106 -0
  86. package/src/clock.ts +42 -0
  87. package/src/config-error.ts +26 -0
  88. package/src/config.ts +209 -0
  89. package/src/database-requirement.ts +28 -0
  90. package/src/i18n.ts +93 -0
  91. package/src/index.ts +47 -0
  92. package/src/manifest.ts +56 -0
  93. package/src/messages/en.ts +6 -0
  94. package/src/messages/index.ts +8 -0
  95. package/src/messages/pl.ts +8 -0
  96. package/src/module.ts +240 -0
  97. package/src/next/index.ts +3 -0
  98. package/src/next/registry.ts +38 -0
  99. package/src/result.ts +29 -0
  100. package/src/safe-error.ts +50 -0
  101. package/src/site-urls.ts +40 -0
  102. package/src/switches.ts +35 -0
  103. package/src/version-range.ts +90 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SOFTURE
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,244 @@
1
- # Temporary Holding Version
1
+ # @softure-ai/core
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **Status:** wave 0 · implemented in FD-3 (`core-contract`), 0.1.5, prepared for its first release (the owner's tag, `scripts/release/README.md`).
4
+
5
+ The contract every SOFTURE module stands on. Standard:
6
+ [docs/02-module-standard.md](../../docs/02-module-standard.md). Sources in FIRE_TRACKER: the
7
+ `{ ok, error }` action results, `src/lib/safe-error.ts` and `src/lib/plural.ts`.
8
+
9
+ ## 1. What it provides
10
+
11
+ The app configuration (`defineSoftureConfig`), the module contract (`defineModule`), `Result`
12
+ with namespaced error codes, an injectable `Clock`, `pl`/`en` messages with partial overrides,
13
+ and `safeError`. `@softure-ai/core/cli` loads the app's config for the modules' command-line tools.
14
+
15
+ ## 2. Installation
16
+
17
+ ```bash
18
+ npm install @softure-ai/core
19
+ ```
20
+
21
+ Node ≥ 22, ESM only. The only runtime dependency is `zod`.
22
+
23
+ ## 3. Configuration
24
+
25
+ ```ts
26
+ interface SoftureConfigInput {
27
+ database?: { url: string } | null; // required once any module has a dbSchema
28
+ locale: "en" | "pl";
29
+ timezone: string; // IANA zone, e.g. "Europe/Warsaw"
30
+ appOrigin: string; // http(s) origin without a path
31
+ modules: SoftureModule[]; // a module is enabled by being listed
32
+ }
33
+ ```
34
+
35
+ ```ts
36
+ // softure.config.ts
37
+ import { defineSoftureConfig } from "@softure-ai/core";
38
+ import { registerSoftureConfig } from "@softure-ai/core/next";
39
+ import { notes } from "@softure-ai/notes";
40
+
41
+ const config = defineSoftureConfig({
42
+ database: { url: process.env.DATABASE_URL ?? "" },
43
+ locale: "pl",
44
+ timezone: "Europe/Warsaw",
45
+ appOrigin: process.env.APP_ORIGIN ?? "http://localhost:3000",
46
+ modules: [notes({ limit: 10, routes: { list: "/my-notes" }, messages: { en: { list: { title: "My notes" } } } })],
47
+ });
48
+
49
+ registerSoftureConfig(config);
50
+ export default config;
51
+ ```
52
+
53
+ `defineSoftureConfig` throws `SoftureConfigError` with every problem listed (`issues`): an
54
+ unknown locale or zone, an origin with a path, a module listed twice, two modules on one database
55
+ schema, a required dependency missing, a dependency outside its version range (also an optional
56
+ one, when listed), a dependency cycle, or a module with a `dbSchema` and no `database`.
57
+
58
+ A command that never connects loads the config inside `withDatabaseOptional(() => import(...))`:
59
+ there a missing, `null` or empty `database.url` gives `database: null` instead of a refusal, also when
60
+ a module has a `dbSchema`; a real URL is kept and every other check still runs. The flag is scoped to
61
+ that one load, and Node keeps a module's first evaluation, so a process loads the config one way.
62
+
63
+ `getModule(config, id)` finds an enabled module; `sortModulesByDependencies(modules)` returns the
64
+ migration order (dependencies first, listed order otherwise).
65
+
66
+ **Defining a module** (in a module package):
67
+
68
+ ```ts
69
+ import { defineModule, resolveMigrationsDir } from "@softure-ai/core";
70
+ import { z } from "zod";
71
+ import { en } from "./messages/en.js";
72
+ import { pl } from "./messages/pl.js";
73
+
74
+ export const notes = defineModule({
75
+ manifest: {
76
+ id: "notes", version: "0.1.0", dependsOn: { auth: "^0.1.0" }, dbSchema: "notes",
77
+ tables: ["notes"], env: [], switches: ["notes.read_only"], routes: { list: "/notes" },
78
+ mount: [], privacy: { exports: true, deletes: true },
79
+ },
80
+ messages: { en, pl },
81
+ options: z.object({ limit: z.number().int().positive().default(100) }),
82
+ migrations: { dir: resolveMigrationsDir(import.meta.url, "../migrations/") },
83
+ privacy: { exportUserData, deleteUserData },
84
+ health: checkNotesReady, // optional: (context) => Promise<Result<undefined>>
85
+ // switchReader: optional, only for the app's switch provider (section 7)
86
+ // siteUrls: optional, only for the app's site URL provider (section 7)
87
+ });
88
+ ```
89
+
90
+ - `resolveMigrationsDir(import.meta.url, "../migrations/")` gives the folder's `file:` URL. Do not
91
+ write `new URL("../migrations/", import.meta.url)` in a module: Next.js (Turbopack) treats that
92
+ literal form as an asset import and fails the app's `next build` on a folder, also from
93
+ `node_modules` (measured in identity ID-1).
94
+ - The manifest is checked against `moduleManifestSchema` when the package is imported; a
95
+ `dbSchema` needs `migrations`, and each privacy flag needs its function (and only then).
96
+ - The returned factory takes the module's options plus two reserved keys: `routes` (new paths
97
+ for known route names) and `messages` (partial copy per locale). It throws
98
+ `SoftureConfigError` naming the module and every invalid field.
99
+ - **`module.json`** is the JSON projection of the TS manifest, which is the source of truth at
100
+ runtime. A module's test keeps them equal:
101
+ `expect(JSON.parse(readFileSync("module.json", "utf8"))).toEqual(toModuleJson(notes))`.
102
+ - Version ranges in `dependsOn`: `x.y.z`, `^x.y.z`, `~x.y.z`, `*`, each optionally ending in `?`
103
+ (optional dependency). Caret follows npm on `0.x`: `^0.1.0` is `>=0.1.0 <0.2.0`.
104
+
105
+ ## 4. Mounting
106
+
107
+ `@softure-ai/core/next` holds the config registry for server actions and route handlers shipped
108
+ in module packages, which cannot import the app's `softure.config.ts`:
109
+
110
+ - `registerSoftureConfig(config)`: call it in `softure.config.ts`, and import that file from
111
+ `instrumentation.ts` (it runs at server start) and from the root layout (`next build` prerenders
112
+ static pages without running instrumentation);
113
+ - `getSoftureConfig()`: read it inside package code; it throws when nothing was registered;
114
+ - `clearSoftureConfig()`: for tests.
115
+
116
+ Confirmed by identity ID-1 (`next-actions-spike`): a server action, a route handler and a server
117
+ component page shipped in a package all read the registered config, in `next dev` and in
118
+ `next build && next start`, installed as a packed copy or linked from the workspace. The rules for
119
+ package-shipped Next code are in docs/02 §8. `createSoftureHandlers` and `softureMiddleware` come
120
+ with the first module that mounts routes.
121
+
122
+ `@softure-ai/core/cli` is how a module's command-line tool (`softure migrate`, `softure-mail`,
123
+ `softure-blog`) finds the app's config, so the lookup and its messages live once:
124
+
125
+ - `takeConfigOption(argv)`: takes `--config <file>` or `--config=<file>` out of the arguments;
126
+ - `loadAppConfig({ cwd, configPath, appScript })`: imports the `--config` file, or the first of
127
+ `DEFAULT_CONFIG_FILES` (`softure.config.{ts,mts,js,mjs}`) in `cwd`; when Node cannot import it, the
128
+ problem names `appScript.runner` and the `appScript.packageName` README;
129
+ - `findDefaultConfig(cwd)` and `loadConfig(path, appScript, { database })`: the two halves on their own;
130
+ - `database: "optional"` (default `"required"`) on both loaders: for a command that never connects
131
+ (`softure-blog check`), the import runs inside `withDatabaseOptional`.
132
+
133
+ Each returns `{ ok: true, … }` or `{ ok: false, problem }`; the tool prefixes the problem with its name.
134
+ Node only: the root entry has no `node:` imports.
135
+
136
+ ## 5. Migrations and tables
137
+
138
+ None. Core owns no database schema. A module points at its SQL folder with
139
+ `migrations: { dir: resolveMigrationsDir(import.meta.url, "../migrations/") }`; `@softure-ai/db`
140
+ (FD-4) applies the files.
141
+
142
+ ## 6. Environment variables
143
+
144
+ None. Core reads no environment variables; the app passes values into `defineSoftureConfig`.
145
+
146
+ ## 7. Switches
147
+
148
+ None of its own. Modules declare theirs in `manifest.switches`, each prefixed with the module id
149
+ (`notes.read_only`); the `feature-switches` module manages them.
150
+
151
+ Core holds the **switch-reader contract**, so a module can read a switch without importing the
152
+ module that stores them (feature-switches depends on auth, so auth could not import it back):
153
+
154
+ ```ts
155
+ import { readSwitch } from "@softure-ai/core";
156
+
157
+ const reading = await readSwitch(ctx, "notes.read_only");
158
+ const isReadOnly = reading.kind === "value" ? reading.isEnabled : getNotesOptions(ctx.config).readOnly;
159
+ ```
160
+
161
+ - `readSwitch(ctx, name)` asks the enabled module that passed `switchReader` to `defineModule`
162
+ and returns `SwitchReading`: `{ kind: "value", isEnabled }`, or `{ kind: "undeclared" }` when no
163
+ module provides a reader or the app did not define that switch there. On `undeclared` the module
164
+ uses its own default.
165
+ - A `SwitchReader` is `(context, name) => Promise<SwitchReading>`. It resolves read failures itself
166
+ (a fail mode) and never throws for an unknown name. `@softure-ai/feature-switches` is the provider.
167
+ - `defineSoftureConfig` refuses two enabled modules that provide a reader; `findSwitchReader(config)`
168
+ returns the one there is, or `null`.
169
+ - `readSwitch` caches nothing: each call is one read by the provider. Ask once per request.
170
+
171
+ Core also holds the **site-URL contract**, so a module can declare the absolute URLs of its pages
172
+ (canonical, Open Graph, JSON-LD, feeds) by the app's canonical rule without importing the module that
173
+ owns it (`@softure-ai/seo` is optional for the blog, and a bundler resolves every `import()`):
174
+
175
+ ```ts
176
+ import { getSiteUrls } from "@softure-ai/core";
177
+
178
+ const urls = getSiteUrls(config);
179
+ urls.getCanonicalUrl("/notes/first"); // a page: the provider's host and trailing-slash rule
180
+ `${urls.origin}/notes/feed.xml`; // a file: on the site's origin as it is
181
+ ```
182
+
183
+ - `getSiteUrls(config)` asks the enabled module that passed `siteUrls` to `defineModule` (a
184
+ `SiteUrlProvider`, `(config) => SiteUrls`); `@softure-ai/seo` is the provider. Without one, `origin`
185
+ is `appOrigin` and `getCanonicalUrl(path)` appends the path to it unchanged.
186
+ - `getCanonicalUrl` takes a path starting with a single `/` and throws for anything else.
187
+ - `defineSoftureConfig` refuses two enabled modules that provide site URLs; `findSiteUrlProvider(config)`
188
+ returns the one there is, or `null`.
189
+
190
+ ## 8. Appearance
191
+
192
+ None. Tokens, slots and styles live in `@softure-ai/ui`.
193
+
194
+ ## 9. Copy
195
+
196
+ - `LOCALES` is `["en", "pl"]`; every module ships both dictionaries complete (`pl.ts` is typed
197
+ `typeof en`).
198
+ - `mergeMessages(defaults, overrides)` applies partial overrides per locale; unknown keys and a
199
+ string in place of a group are ignored.
200
+ - `formatMessage("{count} of {total}", { count, total })` fills placeholders; a missing value
201
+ stays visible as `{name}`.
202
+ - `selectPlural(locale, count, { one, few, many, other })` uses `Intl.PluralRules`.
203
+ - `getMessage(dictionary, "errors.unexpected")` reads a dotted path.
204
+ - Core's own keys (`coreMessages`): `errors.database_failed`, `errors.unexpected`, the copy for
205
+ the `core.database_failed` and `core.unexpected` codes.
206
+
207
+ **Errors as values.** `Result<T, E>` is `{ ok: true, value } | { ok: false, error }`, built with
208
+ `ok(value)` / `ok()` and `err("module.code")`. Codes are namespaced by module id
209
+ (`` `${string}.${string}` ``); the UI translates them through messages.
210
+ `safeError(error)` turns a caught error into `core.database_failed` (a Drizzle `Failed query:` or a
211
+ bare SQL statement) or `core.unexpected`, so no SQL or parameter reaches a caller.
212
+ `errorLogLabel(error)` gives a log line with the error class and SQLSTATE only, never the text.
213
+
214
+ **Time.** Server code receives a `Clock` (`ModuleContext.clock`) instead of calling `new Date()`.
215
+ `systemClock` is the real one; `createTestClock(start)` has `advance(ms)` and `set(date)`.
216
+ A whole test run can also move to another day without code changes: `TEST_TODAY=YYYY-MM-DD` with the
217
+ `@softure-ai/testing/vitest-setup` setup file shifts the global `Date` (and so `systemClock`) to that
218
+ day while time keeps running ([`@softure-ai/testing`](../testing/README.md)).
219
+
220
+ ## 10. Hooks
221
+
222
+ `ModuleContext` is what every server function of a module receives: `{ db, clock, config }`
223
+ (`db` is typed by `@softure-ai/db`). Request scope (cookies, headers) stays in `next/`.
224
+
225
+ `health` (optional) is the module's readiness probe, a `HealthCheck`:
226
+ `(context: ModuleContext) => Promise<Result<undefined>>`. `GET /api/health` of `@softure-ai/ops`
227
+ runs it for every enabled module that has one; `ok()` is healthy, an `Err` or a throw is not.
228
+ Keep it to one cheap query.
229
+
230
+ ## 11. GDPR
231
+
232
+ Core collects nothing. It defines the contributor contract a module implements:
233
+ `exportUserData(context, userId)` returning `Result<unknown>` and
234
+ `deleteUserData(context, userId)` returning `Result<undefined>`, present exactly when the
235
+ manifest's `privacy.exports` / `privacy.deletes` flag is true. The `privacy` module runs them.
236
+
237
+ ## 12. Limitations
238
+
239
+ - The registry in `next/` is provisional until ID-1.
240
+ - Versions are plain `x.y.z`; pre-release versions and range forms beyond `^`, `~`, exact and `*`
241
+ are rejected.
242
+ - Route maps mix mounted paths and redirect targets, so two modules may share a path; mount
243
+ collisions are left to `softure doctor`.
244
+ - No date or number formatting helpers yet; apps use `Intl` with `config.locale` and `config.timezone`.
@@ -0,0 +1,2 @@
1
+ export { DEFAULT_CONFIG_FILES, findDefaultConfig, loadAppConfig, loadConfig, takeConfigOption, type AppScriptHint, type ConfigLoadResult, type ConfigOptionResult, type DatabaseRequirement, type LoadAppConfigOptions, type LoadConfigOptions, } from "./load-config.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,aAAa,EACb,UAAU,EACV,gBAAgB,EAChB,KAAK,aAAa,EAClB,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,EACxB,KAAK,oBAAoB,EACzB,KAAK,iBAAiB,GACvB,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,4 @@
1
+ // `@softure-ai/core/cli`: the config loading shared by the modules' command-line tools. Node only;
2
+ // the root entry stays free of `node:` imports.
3
+ export { DEFAULT_CONFIG_FILES, findDefaultConfig, loadAppConfig, loadConfig, takeConfigOption, } from "./load-config.js";
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":"AAAA,mGAAmG;AACnG,gDAAgD;AAChD,OAAO,EACL,oBAAoB,EACpB,iBAAiB,EACjB,aAAa,EACb,UAAU,EACV,gBAAgB,GAOjB,MAAM,kBAAkB,CAAC"}
@@ -0,0 +1,49 @@
1
+ import type { SoftureConfig } from "../config.js";
2
+ /** The file names looked up in the working directory when no `--config` is given, in this order. */
3
+ export declare const DEFAULT_CONFIG_FILES: readonly string[];
4
+ /** The function and package an app script calls when Node cannot import the config file. */
5
+ export interface AppScriptHint {
6
+ /** e.g. `runMigrateCli`. */
7
+ readonly runner: string;
8
+ /** e.g. `@softure-ai/db`, whose README shows the app script. */
9
+ readonly packageName: string;
10
+ }
11
+ export type ConfigOptionResult = {
12
+ readonly ok: true;
13
+ readonly configPath: string | undefined;
14
+ readonly argv: string[];
15
+ } | {
16
+ readonly ok: false;
17
+ readonly problem: string;
18
+ };
19
+ export type ConfigLoadResult = {
20
+ readonly ok: true;
21
+ readonly config: SoftureConfig;
22
+ } | {
23
+ readonly ok: false;
24
+ readonly problem: string;
25
+ };
26
+ /**
27
+ * Whether the command needs the config's database. `"optional"` is for a command that never connects
28
+ * (`softure-blog check`): a missing or empty URL then loads as `database: null` (see `withDatabaseOptional`).
29
+ */
30
+ export type DatabaseRequirement = "required" | "optional";
31
+ export interface LoadConfigOptions {
32
+ /** Default `"required"`. */
33
+ readonly database?: DatabaseRequirement;
34
+ }
35
+ export interface LoadAppConfigOptions extends LoadConfigOptions {
36
+ readonly cwd: string;
37
+ /** The `--config` value, relative to `cwd`; `undefined` looks up `DEFAULT_CONFIG_FILES`. */
38
+ readonly configPath: string | undefined;
39
+ readonly appScript: AppScriptHint;
40
+ }
41
+ /** Takes `--config <file>` or `--config=<file>` out of the arguments and keeps the rest in order. */
42
+ export declare function takeConfigOption(argv: readonly string[]): ConfigOptionResult;
43
+ /** The first of `DEFAULT_CONFIG_FILES` that exists in `cwd`, as an absolute path. */
44
+ export declare function findDefaultConfig(cwd: string): string | undefined;
45
+ /** The config the file exports (default or `config`), or why it could not be used. */
46
+ export declare function loadConfig(path: string, appScript: AppScriptHint, options?: LoadConfigOptions): Promise<ConfigLoadResult>;
47
+ /** Loads the `--config` file, or the default one found in `cwd`. */
48
+ export declare function loadAppConfig(options: LoadAppConfigOptions): Promise<ConfigLoadResult>;
49
+ //# sourceMappingURL=load-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load-config.d.ts","sourceRoot":"","sources":["../../src/cli/load-config.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAGlD,oGAAoG;AACpG,eAAO,MAAM,oBAAoB,EAAE,SAAS,MAAM,EAA2F,CAAC;AAE9I,4FAA4F;AAC5F,MAAM,WAAW,aAAa;IAC5B,4BAA4B;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,gEAAgE;IAChE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,MAAM,kBAAkB,GAC1B;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,GACvF;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAErD,MAAM,MAAM,gBAAgB,GAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAExI;;;GAGG;AACH,MAAM,MAAM,mBAAmB,GAAG,UAAU,GAAG,UAAU,CAAC;AAE1D,MAAM,WAAW,iBAAiB;IAChC,4BAA4B;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,mBAAmB,CAAC;CACzC;AAED,MAAM,WAAW,oBAAqB,SAAQ,iBAAiB;IAC7D,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,4FAA4F;IAC5F,QAAQ,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAAC;IACxC,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC;CACnC;AAED,qGAAqG;AACrG,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,kBAAkB,CAiB5E;AAED,qFAAqF;AACrF,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAEjE;AAED,sFAAsF;AACtF,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,aAAa,EAAE,OAAO,GAAE,iBAAsB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAiBnI;AAED,oEAAoE;AACpE,wBAAsB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAS5F"}
@@ -0,0 +1,73 @@
1
+ // How a module's command-line tool (`softure migrate`, `softure-mail`, `softure-blog`) finds and loads
2
+ // the app's `softure.config.*`. The file is loaded with a dynamic import, so it must be one Node can
3
+ // run itself: `.js`/`.mjs`, or `.ts` where Node strips types (Node >= 22.18; relative imports then
4
+ // need `.ts` extensions). Anything else uses an app script that passes the config to the command.
5
+ // Every problem is returned without the tool's name; each tool prefixes its own.
6
+ import { existsSync } from "node:fs";
7
+ import { resolve } from "node:path";
8
+ import { pathToFileURL } from "node:url";
9
+ import { withDatabaseOptional } from "../database-requirement.js";
10
+ /** The file names looked up in the working directory when no `--config` is given, in this order. */
11
+ export const DEFAULT_CONFIG_FILES = ["softure.config.ts", "softure.config.mts", "softure.config.js", "softure.config.mjs"];
12
+ /** Takes `--config <file>` or `--config=<file>` out of the arguments and keeps the rest in order. */
13
+ export function takeConfigOption(argv) {
14
+ const rest = [];
15
+ let configPath;
16
+ for (let index = 0; index < argv.length; index += 1) {
17
+ const arg = argv[index] ?? "";
18
+ if (arg === "--config") {
19
+ const value = argv[index + 1];
20
+ if (value === undefined || value.startsWith("--"))
21
+ return { ok: false, problem: "--config needs a file path" };
22
+ configPath = value;
23
+ index += 1;
24
+ }
25
+ else if (arg.startsWith("--config=")) {
26
+ configPath = arg.slice("--config=".length);
27
+ }
28
+ else {
29
+ rest.push(arg);
30
+ }
31
+ }
32
+ return { ok: true, configPath, argv: rest };
33
+ }
34
+ /** The first of `DEFAULT_CONFIG_FILES` that exists in `cwd`, as an absolute path. */
35
+ export function findDefaultConfig(cwd) {
36
+ return DEFAULT_CONFIG_FILES.map((name) => resolve(cwd, name)).find((path) => existsSync(path));
37
+ }
38
+ /** The config the file exports (default or `config`), or why it could not be used. */
39
+ export async function loadConfig(path, appScript, options = {}) {
40
+ if (!existsSync(path))
41
+ return { ok: false, problem: `config file ${path} does not exist` };
42
+ const importConfig = () => import(pathToFileURL(path).href);
43
+ let exported;
44
+ try {
45
+ exported = await (options.database === "optional" ? withDatabaseOptional(importConfig) : importConfig());
46
+ }
47
+ catch (error) {
48
+ const reason = error instanceof Error ? error.message : String(error);
49
+ return {
50
+ ok: false,
51
+ problem: `cannot load ${path}: ${reason}. If Node cannot run this file, call ${appScript.runner} from an app script (see the ${appScript.packageName} README).`,
52
+ };
53
+ }
54
+ const config = exported.default ?? exported.config;
55
+ return isConfigLike(config)
56
+ ? { ok: true, config }
57
+ : { ok: false, problem: `${path} must export (default or as "config") the result of defineSoftureConfig` };
58
+ }
59
+ /** Loads the `--config` file, or the default one found in `cwd`. */
60
+ export async function loadAppConfig(options) {
61
+ const path = options.configPath === undefined ? findDefaultConfig(options.cwd) : resolve(options.cwd, options.configPath);
62
+ if (path === undefined) {
63
+ return {
64
+ ok: false,
65
+ problem: `no config found; looked for ${DEFAULT_CONFIG_FILES.join(", ")} in ${options.cwd}; pass --config <file>`,
66
+ };
67
+ }
68
+ return loadConfig(path, options.appScript, { database: options.database ?? "required" });
69
+ }
70
+ function isConfigLike(value) {
71
+ return typeof value === "object" && value !== null && "modules" in value && Array.isArray(value.modules) && "database" in value;
72
+ }
73
+ //# sourceMappingURL=load-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"load-config.js","sourceRoot":"","sources":["../../src/cli/load-config.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ export interface Clock {
2
+ /** The current instant. Every call returns a new `Date`, so callers may mutate it. */
3
+ now(): Date;
4
+ }
5
+ /** A clock a test moves by hand. */
6
+ export interface TestClock extends Clock {
7
+ /** Moves the clock forward (or back, with a negative value) by `milliseconds`. */
8
+ advance(milliseconds: number): void;
9
+ /** Moves the clock to `instant`. */
10
+ set(instant: Date): void;
11
+ }
12
+ /** The real wall clock. */
13
+ export declare const systemClock: Clock;
14
+ export declare function createTestClock(start: Date): TestClock;
15
+ //# sourceMappingURL=clock.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clock.d.ts","sourceRoot":"","sources":["../src/clock.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,KAAK;IACpB,sFAAsF;IACtF,GAAG,IAAI,IAAI,CAAC;CACb;AAED,oCAAoC;AACpC,MAAM,WAAW,SAAU,SAAQ,KAAK;IACtC,kFAAkF;IAClF,OAAO,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,oCAAoC;IACpC,GAAG,CAAC,OAAO,EAAE,IAAI,GAAG,IAAI,CAAC;CAC1B;AAED,2BAA2B;AAC3B,eAAO,MAAM,WAAW,EAAE,KAEzB,CAAC;AAEF,wBAAgB,eAAe,CAAC,KAAK,EAAE,IAAI,GAAG,SAAS,CAYtD"}
package/dist/clock.js ADDED
@@ -0,0 +1,26 @@
1
+ // Time is injected, never read from the global `Date` inside module logic, so tests control it
2
+ // (docs/02-module-standard.md §9, FIRE_TRACKER's `now` convention).
3
+ /** The real wall clock. */
4
+ export const systemClock = {
5
+ now: () => new Date(),
6
+ };
7
+ export function createTestClock(start) {
8
+ let current = readTime(start);
9
+ return {
10
+ now: () => new Date(current),
11
+ advance: (milliseconds) => {
12
+ current += milliseconds;
13
+ },
14
+ set: (instant) => {
15
+ current = readTime(instant);
16
+ },
17
+ };
18
+ }
19
+ function readTime(instant) {
20
+ const time = instant.getTime();
21
+ if (Number.isNaN(time)) {
22
+ throw new RangeError("createTestClock: invalid start date");
23
+ }
24
+ return time;
25
+ }
26
+ //# sourceMappingURL=clock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"clock.js","sourceRoot":"","sources":["../src/clock.ts"],"names":[],"mappings":"AAAA,+FAA+F;AAC/F,oEAAoE;AAepE,2BAA2B;AAC3B,MAAM,CAAC,MAAM,WAAW,GAAU;IAChC,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE;CACtB,CAAC;AAEF,MAAM,UAAU,eAAe,CAAC,KAAW;IACzC,IAAI,OAAO,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAE9B,OAAO;QACL,GAAG,EAAE,GAAG,EAAE,CAAC,IAAI,IAAI,CAAC,OAAO,CAAC;QAC5B,OAAO,EAAE,CAAC,YAAY,EAAE,EAAE;YACxB,OAAO,IAAI,YAAY,CAAC;QAC1B,CAAC;QACD,GAAG,EAAE,CAAC,OAAO,EAAE,EAAE;YACf,OAAO,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC;QAC9B,CAAC;KACF,CAAC;AACJ,CAAC;AAED,SAAS,QAAQ,CAAC,OAAa;IAC7B,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IAC/B,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,UAAU,CAAC,qCAAqC,CAAC,CAAC;IAC9D,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,12 @@
1
+ export declare class SoftureConfigError extends Error {
2
+ readonly name = "SoftureConfigError";
3
+ /** One line per problem: `<path>: <what is wrong>`. */
4
+ readonly issues: readonly string[];
5
+ constructor(subject: string, issues: readonly string[]);
6
+ }
7
+ /** Formats zod issues as `<path>: <message>` lines, with an optional path prefix. */
8
+ export declare function formatIssues(issues: readonly {
9
+ readonly path: readonly PropertyKey[];
10
+ readonly message: string;
11
+ }[], prefix?: string): string[];
12
+ //# sourceMappingURL=config-error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-error.d.ts","sourceRoot":"","sources":["../src/config-error.ts"],"names":[],"mappings":"AAIA,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,SAAkB,IAAI,wBAAwB;IAE9C,uDAAuD;IACvD,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;gBAEvB,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,MAAM,EAAE;CAIvD;AAED,qFAAqF;AACrF,wBAAgB,YAAY,CAC1B,MAAM,EAAE,SAAS;IAAE,QAAQ,CAAC,IAAI,EAAE,SAAS,WAAW,EAAE,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,EAAE,EACtF,MAAM,CAAC,EAAE,MAAM,GACd,MAAM,EAAE,CAKV"}
@@ -0,0 +1,20 @@
1
+ // A configuration that cannot work: an invalid module manifest, invalid module options or an
2
+ // invalid app config. It is thrown at startup (docs/02-module-standard.md §7), because the app
3
+ // cannot run with it; every issue is listed at once so one restart shows all of them.
4
+ export class SoftureConfigError extends Error {
5
+ name = "SoftureConfigError";
6
+ /** One line per problem: `<path>: <what is wrong>`. */
7
+ issues;
8
+ constructor(subject, issues) {
9
+ super(`Invalid SOFTURE configuration in ${subject}:\n${issues.map((issue) => `- ${issue}`).join("\n")}`);
10
+ this.issues = issues;
11
+ }
12
+ }
13
+ /** Formats zod issues as `<path>: <message>` lines, with an optional path prefix. */
14
+ export function formatIssues(issues, prefix) {
15
+ return issues.map((issue) => {
16
+ const path = [prefix, ...issue.path.map(String)].filter((part) => part !== undefined && part !== "").join(".");
17
+ return path === "" ? issue.message : `${path}: ${issue.message}`;
18
+ });
19
+ }
20
+ //# sourceMappingURL=config-error.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-error.js","sourceRoot":"","sources":["../src/config-error.ts"],"names":[],"mappings":"AAAA,6FAA6F;AAC7F,+FAA+F;AAC/F,sFAAsF;AAEtF,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IACzB,IAAI,GAAG,oBAAoB,CAAC;IAE9C,uDAAuD;IAC9C,MAAM,CAAoB;IAEnC,YAAY,OAAe,EAAE,MAAyB;QACpD,KAAK,CAAC,oCAAoC,OAAO,MAAM,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACzG,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACvB,CAAC;CACF;AAED,qFAAqF;AACrF,MAAM,UAAU,YAAY,CAC1B,MAAsF,EACtF,MAAe;IAEf,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE;QAC1B,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,GAAG,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC/G,OAAO,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI,KAAK,KAAK,CAAC,OAAO,EAAE,CAAC;IACnE,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,39 @@
1
+ import { type Locale } from "./i18n.js";
2
+ import type { AnySoftureModule } from "./module.js";
3
+ import { type Result } from "./result.js";
4
+ export interface SoftureConfig {
5
+ /** `null` when the app has no database; required as soon as a module has a `dbSchema`. */
6
+ readonly database: {
7
+ readonly url: string;
8
+ } | null;
9
+ readonly locale: Locale;
10
+ /** IANA time zone used for every date shown or computed per calendar day. */
11
+ readonly timezone: string;
12
+ /** Scheme, host and port of the app, without a path: `https://app.example.com`. */
13
+ readonly appOrigin: string;
14
+ /** Enabled modules, in the order the app listed them. */
15
+ readonly modules: readonly AnySoftureModule[];
16
+ }
17
+ /** What the app writes in `softure.config.ts`. */
18
+ export interface SoftureConfigInput {
19
+ readonly database?: {
20
+ readonly url: string;
21
+ } | null;
22
+ readonly locale: Locale;
23
+ readonly timezone: string;
24
+ readonly appOrigin: string;
25
+ readonly modules: readonly AnySoftureModule[];
26
+ }
27
+ /**
28
+ * Validates the app configuration and returns it frozen. Throws `SoftureConfigError` listing
29
+ * every problem: an app with an invalid configuration must not start.
30
+ */
31
+ export declare function defineSoftureConfig(input: SoftureConfigInput): SoftureConfig;
32
+ /** The enabled module with this id, or `undefined` when the app did not list it. */
33
+ export declare function getModule(config: SoftureConfig, id: string): AnySoftureModule | undefined;
34
+ /**
35
+ * The modules with every listed dependency before its dependents (the migration order); ties
36
+ * keep the listed order. Dependencies that are not listed are ignored here.
37
+ */
38
+ export declare function sortModulesByDependencies(modules: readonly AnySoftureModule[]): Result<AnySoftureModule[], "core.dependency_cycle">;
39
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAKA,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,WAAW,CAAC;AACjD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AAGnD,MAAM,WAAW,aAAa;IAC5B,0FAA0F;IAC1F,QAAQ,CAAC,QAAQ,EAAE;QAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACnD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,6EAA6E;IAC7E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,mFAAmF;IACnF,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,yDAAyD;IACzD,QAAQ,CAAC,OAAO,EAAE,SAAS,gBAAgB,EAAE,CAAC;CAC/C;AAED,kDAAkD;AAClD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,QAAQ,CAAC,EAAE;QAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACpD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,SAAS,gBAAgB,EAAE,CAAC;CAC/C;AAeD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,kBAAkB,GAAG,aAAa,CAkB5E;AAED,oFAAoF;AACpF,wBAAgB,SAAS,CAAC,MAAM,EAAE,aAAa,EAAE,EAAE,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAEzF;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,SAAS,gBAAgB,EAAE,GACnC,MAAM,CAAC,gBAAgB,EAAE,EAAE,uBAAuB,CAAC,CAGrD"}