@ontrails/config 1.0.0-beta.24 → 1.0.0-beta.29

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 CHANGED
@@ -1,5 +1,46 @@
1
1
  # @ontrails/config
2
2
 
3
+ ## 1.0.0-beta.29
4
+
5
+ ### Patch Changes
6
+
7
+ - @ontrails/core@1.0.0-beta.29
8
+
9
+ ## 1.0.0-beta.28
10
+
11
+ ### Patch Changes
12
+
13
+ - @ontrails/core@1.0.0-beta.28
14
+
15
+ ## 1.0.0-beta.27
16
+
17
+ ### Patch Changes
18
+
19
+ - @ontrails/core@1.0.0-beta.27
20
+
21
+ ## 1.0.0-beta.26
22
+
23
+ ### Patch Changes
24
+
25
+ - 1307568: Centralize Trails config module path conventions, move local config overrides to root `trails.config.local.*`, scaffold the matching gitignore entries, and load project-local Warden rules from `.trails/rules.ts` or `.trails/rules/`.
26
+ - ef09e46: Add shared Trails project-root discovery helpers and use them in Warden so nested
27
+ cwd invocations still load root `trails.config.*` and project-local
28
+ `.trails/rules*` governance.
29
+ - 38cd9d6: Add a shared Trails config file loader that treats `trails.config.ts` as the natural primary while supporting JSON, JSONC, YAML, and TOML peer formats. Release and Warden config loading now consume the same loader and local overrides can be authored as data files.
30
+ - Updated dependencies [1307568]
31
+ - Updated dependencies [371d19e]
32
+ - @ontrails/core@1.0.0-beta.26
33
+
34
+ ## 1.0.0-beta.25
35
+
36
+ ### Patch Changes
37
+
38
+ - Updated dependencies [c36aca9]
39
+ - Updated dependencies [3befcf1]
40
+ - Updated dependencies [a4f9cf6]
41
+ - Updated dependencies [9bcf34e]
42
+ - @ontrails/core@1.0.0-beta.25
43
+
3
44
  ## 1.0.0-beta.24
4
45
 
5
46
  ### Patch Changes
package/README.md CHANGED
@@ -90,6 +90,17 @@ Each layer overrides the previous. Environment variables always win.
90
90
 
91
91
  `appConfig()` discovers `*.config.toml`, `*.config.json`, `*.config.jsonc`, and `*.config.yaml` by default, plus dotfile equivalents when `dotfile: true`.
92
92
 
93
+ ## Trails project roots
94
+
95
+ `@ontrails/config` owns the shared project-root convention helpers used by framework tools. `resolveTrailsProjectRoot()` honors an explicit root first, then walks upward from a start directory looking for committed project markers:
96
+
97
+ - `trails.config.ts`, `.mts`, `.js`, `.mjs`, `.json`, `.jsonc`,
98
+ `.yaml`, or `.toml`
99
+ - `trails.lock`
100
+ - source-shaped projects with `src/trails/` or `trails/` when no committed marker exists above them
101
+
102
+ `trails.config.local.*` is a per-developer override and does not mark a project root by itself. A bare `.trails/` directory also does not mark a root; it is the committed-control home for project-local sections after a project root is known.
103
+
93
104
  ## Extensions
94
105
 
95
106
  ### `env()`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ontrails/config",
3
- "version": "1.0.0-beta.24",
3
+ "version": "1.0.0-beta.29",
4
4
  "files": [
5
5
  "src/**/*.ts",
6
6
  "!src/**/__tests__/**",
@@ -22,7 +22,7 @@
22
22
  "clean": "rm -rf dist *.tsbuildinfo"
23
23
  },
24
24
  "peerDependencies": {
25
- "@ontrails/core": "^1.0.0-beta.24",
25
+ "@ontrails/core": "^1.0.0-beta.29",
26
26
  "zod": "^4.3.5"
27
27
  }
28
28
  }
@@ -3,13 +3,11 @@
3
3
  * framework conventions for profile selection and local overrides.
4
4
  */
5
5
 
6
- import { existsSync } from 'node:fs';
7
- import { join } from 'node:path';
8
-
9
6
  import type { z } from 'zod';
10
7
 
11
8
  import { appConfig } from './app-config.js';
12
9
  import { deriveConfig } from './resolve.js';
10
+ import { loadTrailsLocalConfigValue } from './trails-config-file.js';
13
11
 
14
12
  // ---------------------------------------------------------------------------
15
13
  // Types
@@ -36,13 +34,8 @@ interface DefineConfigResolveOptions {
36
34
  // Local overrides discovery
37
35
  // ---------------------------------------------------------------------------
38
36
 
39
- const LOCAL_OVERRIDE_CANDIDATES = [
40
- 'config.local.ts',
41
- 'config.local.js',
42
- ] as const;
43
-
44
37
  /**
45
- * Discover and synchronously import a `.trails/config.local.{ts,js}` file.
38
+ * Discover and synchronously import a `trails.config.local.*` file.
46
39
  *
47
40
  * Skipped when `TRAILS_ENV=test` for hermetic test environments.
48
41
  */
@@ -54,15 +47,10 @@ const discoverLocalOverrides = async (
54
47
  return undefined;
55
48
  }
56
49
 
57
- for (const filename of LOCAL_OVERRIDE_CANDIDATES) {
58
- const candidate = join(cwd, '.trails', filename);
59
- if (existsSync(candidate)) {
60
- const mod: Record<string, unknown> = await import(candidate);
61
- return (mod['default'] ?? mod) as Record<string, unknown>;
62
- }
63
- }
64
-
65
- return undefined;
50
+ const loaded = await loadTrailsLocalConfigValue(cwd);
51
+ return loaded.value === undefined
52
+ ? undefined
53
+ : (loaded.value as Record<string, unknown>);
66
54
  };
67
55
 
68
56
  // ---------------------------------------------------------------------------
package/src/index.ts CHANGED
@@ -9,6 +9,30 @@ export {
9
9
  export { collectConfigMeta } from './collect.js';
10
10
  export { collectResourceConfigs, type ResourceConfigEntry } from './compose.js';
11
11
  export { defineConfig, type DefineConfigOptions } from './define-config.js';
12
+ export {
13
+ findTrailsConfigPaths,
14
+ findTrailsLocalConfigPaths,
15
+ trailsConfigDataCandidates,
16
+ trailsConfigFileCandidates,
17
+ findTrailsConfigModulePath,
18
+ findTrailsLocalConfigModulePath,
19
+ findTrailsProjectRoot,
20
+ resolveTrailsProjectRoot,
21
+ trailsConfigModuleCandidates,
22
+ trailsLockFileName,
23
+ trailsLocalConfigDataCandidates,
24
+ trailsLocalConfigFileCandidates,
25
+ trailsLocalConfigModuleCandidates,
26
+ trailsSourceRootCandidates,
27
+ type TrailsProjectRootMarker,
28
+ type TrailsProjectRootResolution,
29
+ } from './trails-conventions.js';
30
+ export {
31
+ loadTrailsConfigFileValue,
32
+ loadTrailsConfigValue,
33
+ loadTrailsLocalConfigValue,
34
+ type LoadedTrailsConfigValue,
35
+ } from './trails-config-file.js';
12
36
  export { deriveConfigFields, type FieldDescription } from './derive-fields.js';
13
37
  export {
14
38
  checkConfig,
@@ -0,0 +1,133 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+
5
+ import { NotFoundError, ValidationError } from '@ontrails/core';
6
+
7
+ import {
8
+ findTrailsConfigPaths,
9
+ findTrailsLocalConfigPaths,
10
+ } from './trails-conventions.js';
11
+
12
+ export interface LoadedTrailsConfigValue {
13
+ readonly configPath?: string | undefined;
14
+ readonly value?: unknown;
15
+ }
16
+
17
+ const MODULE_EXTENSIONS = new Set(['.ts', '.mts', '.js', '.mjs']);
18
+ const DATA_EXTENSIONS = new Set(['.json', '.jsonc', '.yaml', '.toml']);
19
+
20
+ const extensionFor = (filePath: string): string | undefined => {
21
+ for (const extension of [...MODULE_EXTENSIONS, ...DATA_EXTENSIONS]) {
22
+ if (filePath.endsWith(extension)) {
23
+ return extension;
24
+ }
25
+ }
26
+ return undefined;
27
+ };
28
+
29
+ const isModuleExtension = (extension: string | undefined): boolean =>
30
+ extension !== undefined && MODULE_EXTENSIONS.has(extension);
31
+
32
+ const parseDataConfig = (filePath: string, text: string): unknown => {
33
+ const extension = extensionFor(filePath);
34
+ try {
35
+ switch (extension) {
36
+ case '.json': {
37
+ return JSON.parse(text);
38
+ }
39
+ case '.jsonc': {
40
+ return Bun.JSONC.parse(text);
41
+ }
42
+ case '.toml': {
43
+ return Bun.TOML.parse(text);
44
+ }
45
+ case '.yaml': {
46
+ return Bun.YAML.parse(text);
47
+ }
48
+ default: {
49
+ throw new ValidationError(
50
+ `Unsupported Trails config file: ${filePath}`
51
+ );
52
+ }
53
+ }
54
+ } catch (error) {
55
+ if (error instanceof ValidationError) {
56
+ throw error;
57
+ }
58
+ throw new ValidationError(
59
+ `Failed to parse Trails config file: ${filePath}`,
60
+ {
61
+ cause: error instanceof Error ? error : new Error(String(error)),
62
+ context: { path: filePath },
63
+ }
64
+ );
65
+ }
66
+ };
67
+
68
+ export const loadTrailsConfigFileValue = async (
69
+ filePath: string
70
+ ): Promise<unknown> => {
71
+ const extension = extensionFor(filePath);
72
+ if (isModuleExtension(extension)) {
73
+ const url = pathToFileURL(filePath);
74
+ url.searchParams.set('t', Date.now().toString());
75
+ const mod = (await import(url.href)) as Record<string, unknown>;
76
+ return mod['default'] ?? mod;
77
+ }
78
+
79
+ const text = await Bun.file(filePath).text();
80
+ return parseDataConfig(filePath, text);
81
+ };
82
+
83
+ const findSingleConfigPath = (
84
+ paths: readonly string[],
85
+ label: string
86
+ ): string | undefined => {
87
+ if (paths.length <= 1) {
88
+ return paths[0];
89
+ }
90
+ throw new ValidationError(
91
+ `Multiple ${label} config files found: ${paths.join(', ')}. Keep one config file per project root.`
92
+ );
93
+ };
94
+
95
+ export const loadTrailsConfigValue = async ({
96
+ configPath,
97
+ rootDir,
98
+ }: {
99
+ readonly configPath?: string | undefined;
100
+ readonly rootDir: string;
101
+ }): Promise<LoadedTrailsConfigValue> => {
102
+ const located =
103
+ configPath === undefined
104
+ ? findSingleConfigPath(findTrailsConfigPaths(rootDir), 'Trails')
105
+ : resolve(rootDir, configPath);
106
+
107
+ if (located === undefined) {
108
+ return {};
109
+ }
110
+ if (!existsSync(located)) {
111
+ throw new NotFoundError(`Trails config file not found: ${located}`, {
112
+ context: { path: located },
113
+ });
114
+ }
115
+
116
+ return {
117
+ configPath: located,
118
+ value: await loadTrailsConfigFileValue(located),
119
+ };
120
+ };
121
+
122
+ export const loadTrailsLocalConfigValue = async (
123
+ rootDir: string
124
+ ): Promise<LoadedTrailsConfigValue> => {
125
+ const located = findSingleConfigPath(
126
+ findTrailsLocalConfigPaths(rootDir),
127
+ 'Trails local'
128
+ );
129
+
130
+ return located === undefined
131
+ ? {}
132
+ : { configPath: located, value: await loadTrailsConfigFileValue(located) };
133
+ };
@@ -0,0 +1,197 @@
1
+ import { statSync } from 'node:fs';
2
+ import { dirname, join, resolve } from 'node:path';
3
+
4
+ export const trailsConfigModuleCandidates = [
5
+ 'trails.config.ts',
6
+ 'trails.config.mts',
7
+ 'trails.config.js',
8
+ 'trails.config.mjs',
9
+ ] as const;
10
+
11
+ export const trailsConfigDataCandidates = [
12
+ 'trails.config.json',
13
+ 'trails.config.jsonc',
14
+ 'trails.config.yaml',
15
+ 'trails.config.toml',
16
+ ] as const;
17
+
18
+ export const trailsConfigFileCandidates = [
19
+ ...trailsConfigModuleCandidates,
20
+ ...trailsConfigDataCandidates,
21
+ ] as const;
22
+
23
+ export const trailsLocalConfigModuleCandidates = [
24
+ 'trails.config.local.ts',
25
+ 'trails.config.local.mts',
26
+ 'trails.config.local.js',
27
+ 'trails.config.local.mjs',
28
+ ] as const;
29
+
30
+ export const trailsLocalConfigDataCandidates = [
31
+ 'trails.config.local.json',
32
+ 'trails.config.local.jsonc',
33
+ 'trails.config.local.yaml',
34
+ 'trails.config.local.toml',
35
+ ] as const;
36
+
37
+ export const trailsLocalConfigFileCandidates = [
38
+ ...trailsLocalConfigModuleCandidates,
39
+ ...trailsLocalConfigDataCandidates,
40
+ ] as const;
41
+
42
+ export const trailsLockFileName = 'trails.lock' as const;
43
+
44
+ export const trailsSourceRootCandidates = ['src/trails', 'trails'] as const;
45
+
46
+ export type TrailsProjectRootMarker =
47
+ | 'config'
48
+ | 'explicit'
49
+ | 'fallback'
50
+ | 'lock'
51
+ | 'source';
52
+
53
+ export interface TrailsProjectRootResolution {
54
+ readonly marker: TrailsProjectRootMarker;
55
+ readonly markerPath?: string | undefined;
56
+ readonly rootDir: string;
57
+ }
58
+
59
+ export interface FindTrailsProjectRootOptions {
60
+ readonly startDir?: string | undefined;
61
+ }
62
+
63
+ export interface ResolveTrailsProjectRootOptions extends FindTrailsProjectRootOptions {
64
+ readonly explicitRootDir?: string | undefined;
65
+ }
66
+
67
+ const isFile = (path: string): boolean => {
68
+ try {
69
+ return statSync(path).isFile();
70
+ } catch {
71
+ return false;
72
+ }
73
+ };
74
+
75
+ const isDirectory = (path: string): boolean => {
76
+ try {
77
+ return statSync(path).isDirectory();
78
+ } catch {
79
+ return false;
80
+ }
81
+ };
82
+
83
+ const firstExistingCandidate = (
84
+ rootDir: string,
85
+ candidates: readonly string[]
86
+ ): string | undefined =>
87
+ candidates.map((entry) => resolve(rootDir, entry)).find(isFile);
88
+
89
+ const existingCandidates = (
90
+ rootDir: string,
91
+ candidates: readonly string[]
92
+ ): readonly string[] =>
93
+ candidates.map((entry) => resolve(rootDir, entry)).filter(isFile);
94
+
95
+ export const findTrailsConfigPaths = (rootDir: string): readonly string[] =>
96
+ existingCandidates(rootDir, trailsConfigFileCandidates);
97
+
98
+ export const findTrailsLocalConfigPaths = (
99
+ rootDir: string
100
+ ): readonly string[] =>
101
+ existingCandidates(rootDir, trailsLocalConfigFileCandidates);
102
+
103
+ export const findTrailsConfigModulePath = ({
104
+ configPath,
105
+ rootDir,
106
+ }: {
107
+ readonly configPath?: string | undefined;
108
+ readonly rootDir: string;
109
+ }): string | undefined => {
110
+ if (configPath !== undefined) {
111
+ return resolve(rootDir, configPath);
112
+ }
113
+ return firstExistingCandidate(rootDir, trailsConfigFileCandidates);
114
+ };
115
+
116
+ export const findTrailsLocalConfigModulePath = (
117
+ rootDir: string
118
+ ): string | undefined =>
119
+ firstExistingCandidate(rootDir, trailsLocalConfigFileCandidates);
120
+
121
+ const firstExistingSourceRoot = (rootDir: string): string | undefined =>
122
+ trailsSourceRootCandidates
123
+ .map((entry) => join(rootDir, entry))
124
+ .find(isDirectory);
125
+
126
+ const findProjectRootMarkerIn = (
127
+ rootDir: string
128
+ ): Omit<TrailsProjectRootResolution, 'rootDir'> | undefined => {
129
+ const configPath = findTrailsConfigModulePath({ rootDir });
130
+ if (configPath !== undefined) {
131
+ return { marker: 'config', markerPath: configPath };
132
+ }
133
+
134
+ const lockPath = join(rootDir, trailsLockFileName);
135
+ if (isFile(lockPath)) {
136
+ return { marker: 'lock', markerPath: lockPath };
137
+ }
138
+
139
+ return undefined;
140
+ };
141
+
142
+ const findSourceRootMarkerIn = (
143
+ rootDir: string
144
+ ): Omit<TrailsProjectRootResolution, 'rootDir'> | undefined => {
145
+ const sourcePath = firstExistingSourceRoot(rootDir);
146
+ if (sourcePath !== undefined) {
147
+ return { marker: 'source', markerPath: sourcePath };
148
+ }
149
+
150
+ return undefined;
151
+ };
152
+
153
+ export const findTrailsProjectRoot = ({
154
+ startDir = process.cwd(),
155
+ }: FindTrailsProjectRootOptions = {}):
156
+ | TrailsProjectRootResolution
157
+ | undefined => {
158
+ let current = resolve(startDir);
159
+ let sourceFallback: TrailsProjectRootResolution | undefined;
160
+
161
+ while (true) {
162
+ const marker = findProjectRootMarkerIn(current);
163
+ if (marker !== undefined) {
164
+ return { ...marker, rootDir: current };
165
+ }
166
+
167
+ const sourceMarker = findSourceRootMarkerIn(current);
168
+ if (sourceMarker !== undefined) {
169
+ sourceFallback = { ...sourceMarker, rootDir: current };
170
+ }
171
+
172
+ const parent = dirname(current);
173
+ if (parent === current) {
174
+ return sourceFallback;
175
+ }
176
+ current = parent;
177
+ }
178
+ };
179
+
180
+ export const resolveTrailsProjectRoot = ({
181
+ explicitRootDir,
182
+ startDir = process.cwd(),
183
+ }: ResolveTrailsProjectRootOptions = {}): TrailsProjectRootResolution => {
184
+ if (explicitRootDir !== undefined) {
185
+ return {
186
+ marker: 'explicit',
187
+ rootDir: resolve(startDir, explicitRootDir),
188
+ };
189
+ }
190
+
191
+ return (
192
+ findTrailsProjectRoot({ startDir }) ?? {
193
+ marker: 'fallback',
194
+ rootDir: resolve(startDir),
195
+ }
196
+ );
197
+ };