@getstrata/bootstrap 0.2.21 → 0.2.22

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.
@@ -1,5 +1,9 @@
1
1
  import type { AppModule } from "./contracts";
2
- declare let appModules: AppModule[];
3
- declare function ensureModulesLoaded(): Promise<AppModule[]>;
2
+ interface DiscoverModulesOptions {
3
+ modulesDir?: string;
4
+ }
5
+ declare function configureModulesDirectory(modulesDir: string): void;
6
+ declare function ensureModulesLoaded(options?: DiscoverModulesOptions): Promise<AppModule[]>;
4
7
  declare function discoverModules(): AppModule[];
5
- export { appModules, discoverModules, ensureModulesLoaded };
8
+ export type { DiscoverModulesOptions };
9
+ export { configureModulesDirectory, discoverModules, ensureModulesLoaded };
@@ -1 +1 @@
1
- export { appModules, discoverModules, ensureModulesLoaded } from "./discoverModules";
1
+ export { configureModulesDirectory, discoverModules, ensureModulesLoaded, } from "./discoverModules";
@@ -12,6 +12,8 @@ export type { AppContext, AppDependencies, AppModule, AppRouteMap, CachedJson, C
12
12
  export { assertAppDependenciesComplete, getRequiredDependency, resolveService, ServiceContainer, } from "./contracts.ts";
13
13
  export { createWebRoutes, mergeWebRoutes } from "./createWebRoutes.ts";
14
14
  export { createAppDependencies } from "./dependencies.ts";
15
+ export type { DiscoverModulesOptions } from "./discoverModules.ts";
16
+ export { configureModulesDirectory, discoverModules, ensureModulesLoaded, } from "./discoverModules.ts";
15
17
  export { type RouteModelAuthorization, securedBindRouteModel, securedBindRouteModelByKey, } from "./http/securedRouteModelBinding.ts";
16
18
  export { createHttpKernel, type HttpKernel, type MiddlewareGroupName } from "./httpKernel.ts";
17
19
  export { resolveMembershipService } from "./membershipService.ts";
@@ -1,8 +1,62 @@
1
1
  // @bun
2
2
  // ../../src/bootstrap/discoverModules.ts
3
- var appModules = [];
3
+ import { readdirSync } from "fs";
4
+ import { join } from "path";
5
+ import { pathToFileURL } from "url";
6
+ var DISCOVER_MODULES_STATE_KEY = Symbol.for("@getstrata/discoverModulesState");
7
+ function readDiscoverModulesState() {
8
+ const existing = globalThis[DISCOVER_MODULES_STATE_KEY];
9
+ if (existing) {
10
+ return existing;
11
+ }
12
+ const state = { appModules: [] };
13
+ globalThis[DISCOVER_MODULES_STATE_KEY] = state;
14
+ return state;
15
+ }
16
+ function configureModulesDirectory(modulesDir) {
17
+ readDiscoverModulesState().configuredModulesDir = modulesDir;
18
+ }
19
+ function resolveModulesDirectory(options) {
20
+ const state = readDiscoverModulesState();
21
+ if (options?.modulesDir) {
22
+ return options.modulesDir;
23
+ }
24
+ if (state.configuredModulesDir) {
25
+ return state.configuredModulesDir;
26
+ }
27
+ return join(import.meta.dir, "../modules");
28
+ }
29
+ async function loadDiscoveredModules(options) {
30
+ const modulesDirectory = resolveModulesDirectory(options);
31
+ let moduleNames;
32
+ try {
33
+ moduleNames = readdirSync(modulesDirectory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
34
+ } catch (error) {
35
+ if (error.code === "ENOENT") {
36
+ return [];
37
+ }
38
+ throw error;
39
+ }
40
+ const modules = await Promise.all(moduleNames.map(async (moduleName) => {
41
+ const moduleUrl = pathToFileURL(join(modulesDirectory, moduleName, "index.ts")).href;
42
+ const loaded = await import(moduleUrl);
43
+ return loaded.default;
44
+ }));
45
+ return modules.filter((module) => module?.name !== undefined).sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
46
+ }
47
+ async function ensureModulesLoaded(options) {
48
+ const state = readDiscoverModulesState();
49
+ if (state.appModules.length > 0) {
50
+ return state.appModules;
51
+ }
52
+ state.modulesReady ??= loadDiscoveredModules(options).then((modules) => {
53
+ state.appModules.splice(0, state.appModules.length, ...modules);
54
+ return state.appModules;
55
+ });
56
+ return state.modulesReady;
57
+ }
4
58
  function discoverModules() {
5
- return appModules;
59
+ return readDiscoverModulesState().appModules;
6
60
  }
7
61
 
8
62
  // ../../src/bootstrap/cache/modelCacheTags.ts
@@ -179,9 +179,63 @@ class ConfigStore {
179
179
  }
180
180
  }
181
181
  // ../../src/bootstrap/discoverModules.ts
182
- var appModules = [];
182
+ import { readdirSync } from "fs";
183
+ import { join } from "path";
184
+ import { pathToFileURL } from "url";
185
+ var DISCOVER_MODULES_STATE_KEY = Symbol.for("@getstrata/discoverModulesState");
186
+ function readDiscoverModulesState() {
187
+ const existing = globalThis[DISCOVER_MODULES_STATE_KEY];
188
+ if (existing) {
189
+ return existing;
190
+ }
191
+ const state = { appModules: [] };
192
+ globalThis[DISCOVER_MODULES_STATE_KEY] = state;
193
+ return state;
194
+ }
195
+ function configureModulesDirectory(modulesDir) {
196
+ readDiscoverModulesState().configuredModulesDir = modulesDir;
197
+ }
198
+ function resolveModulesDirectory(options) {
199
+ const state = readDiscoverModulesState();
200
+ if (options?.modulesDir) {
201
+ return options.modulesDir;
202
+ }
203
+ if (state.configuredModulesDir) {
204
+ return state.configuredModulesDir;
205
+ }
206
+ return join(import.meta.dir, "../modules");
207
+ }
208
+ async function loadDiscoveredModules(options) {
209
+ const modulesDirectory = resolveModulesDirectory(options);
210
+ let moduleNames;
211
+ try {
212
+ moduleNames = readdirSync(modulesDirectory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
213
+ } catch (error) {
214
+ if (error.code === "ENOENT") {
215
+ return [];
216
+ }
217
+ throw error;
218
+ }
219
+ const modules = await Promise.all(moduleNames.map(async (moduleName) => {
220
+ const moduleUrl = pathToFileURL(join(modulesDirectory, moduleName, "index.ts")).href;
221
+ const loaded = await import(moduleUrl);
222
+ return loaded.default;
223
+ }));
224
+ return modules.filter((module) => module?.name !== undefined).sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
225
+ }
226
+ async function ensureModulesLoaded(options) {
227
+ const state = readDiscoverModulesState();
228
+ if (state.appModules.length > 0) {
229
+ return state.appModules;
230
+ }
231
+ state.modulesReady ??= loadDiscoveredModules(options).then((modules) => {
232
+ state.appModules.splice(0, state.appModules.length, ...modules);
233
+ return state.appModules;
234
+ });
235
+ return state.modulesReady;
236
+ }
183
237
  function discoverModules() {
184
- return appModules;
238
+ return readDiscoverModulesState().appModules;
185
239
  }
186
240
  // ../../src/config/auth.ts
187
241
  var authConfig = {
@@ -1462,14 +1516,14 @@ var eventsProvider = {
1462
1516
  var events_default = eventsProvider;
1463
1517
 
1464
1518
  // ../../src/bootstrap/discoverListeners.ts
1465
- import { readdirSync } from "fs";
1466
- import { join } from "path";
1467
- import { pathToFileURL } from "url";
1519
+ import { readdirSync as readdirSync2 } from "fs";
1520
+ import { join as join2 } from "path";
1521
+ import { pathToFileURL as pathToFileURL2 } from "url";
1468
1522
  async function loadDiscoveredListeners() {
1469
- const listenersDirectory = join(import.meta.dir, "../listeners");
1523
+ const listenersDirectory = join2(import.meta.dir, "../listeners");
1470
1524
  let entries;
1471
1525
  try {
1472
- entries = readdirSync(listenersDirectory, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".ts")).map((entry) => entry.name);
1526
+ entries = readdirSync2(listenersDirectory, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".ts")).map((entry) => entry.name);
1473
1527
  } catch (error) {
1474
1528
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
1475
1529
  return [];
@@ -1477,7 +1531,7 @@ async function loadDiscoveredListeners() {
1477
1531
  throw error;
1478
1532
  }
1479
1533
  const listeners = await Promise.all(entries.map(async (fileName) => {
1480
- const moduleUrl = pathToFileURL(join(listenersDirectory, fileName)).href;
1534
+ const moduleUrl = pathToFileURL2(join2(listenersDirectory, fileName)).href;
1481
1535
  const loaded = await import(moduleUrl);
1482
1536
  return loaded.default;
1483
1537
  }));
@@ -1798,10 +1852,10 @@ function buildHavingClause(tableName, having, params) {
1798
1852
  return body.length > 0 ? ` HAVING ${body}` : "";
1799
1853
  }
1800
1854
  function buildJoinClause(joins = []) {
1801
- return joins.map((join2) => {
1802
- const joinType = join2.type === "left" ? "LEFT JOIN" : "INNER JOIN";
1803
- const onClause = join2.on.map(({ left, right }) => `${qualifyColumn(left.table, left.column)} = ${qualifyColumn(right.table, right.column)}`).join(" AND ");
1804
- return ` ${joinType} ${quoteIdentifier(join2.table)} ON ${onClause}`;
1855
+ return joins.map((join3) => {
1856
+ const joinType = join3.type === "left" ? "LEFT JOIN" : "INNER JOIN";
1857
+ const onClause = join3.on.map(({ left, right }) => `${qualifyColumn(left.table, left.column)} = ${qualifyColumn(right.table, right.column)}`).join(" AND ");
1858
+ return ` ${joinType} ${quoteIdentifier(join3.table)} ON ${onClause}`;
1805
1859
  }).join("");
1806
1860
  }
1807
1861
  function buildLimitClause(limit) {
@@ -2237,7 +2291,7 @@ class RepositoryQuery {
2237
2291
  const rightRef = parseQualifiedColumn(right);
2238
2292
  const table = type === "inner" ? rightRef.table : rightRef.table;
2239
2293
  const joins = this.queryOptions.joins ?? [];
2240
- const existing = joins.find((join2) => join2.table === table && join2.type === type);
2294
+ const existing = joins.find((join3) => join3.table === table && join3.type === type);
2241
2295
  if (existing) {
2242
2296
  existing.on.push({ left: leftRef, right: rightRef });
2243
2297
  return this;
@@ -3737,7 +3791,7 @@ var queue_default = queueProvider;
3737
3791
 
3738
3792
  // ../../src/core/storage/storage.ts
3739
3793
  import { mkdir, readFile, unlink, writeFile } from "fs/promises";
3740
- import { dirname, join as join2 } from "path";
3794
+ import { dirname, join as join3 } from "path";
3741
3795
  var {S3Client } = globalThis.Bun;
3742
3796
 
3743
3797
  class LocalStorageDriver {
@@ -3749,7 +3803,7 @@ class LocalStorageDriver {
3749
3803
  return this.rootDirectory ?? process.env.STORAGE_PATH ?? "storage";
3750
3804
  }
3751
3805
  resolvePath(path) {
3752
- return join2(this.resolveRootDirectory(), path.replace(/^\/+/, ""));
3806
+ return join3(this.resolveRootDirectory(), path.replace(/^\/+/, ""));
3753
3807
  }
3754
3808
  async put(path, contents) {
3755
3809
  const absolutePath = this.resolvePath(path);
@@ -3882,9 +3936,9 @@ function currentRequestMeta() {
3882
3936
  }
3883
3937
 
3884
3938
  // ../../src/core/view/etaViewEngine.ts
3885
- import { join as join3 } from "path";
3939
+ import { join as join4 } from "path";
3886
3940
  import { Eta } from "eta";
3887
- var DEFAULT_VIEWS_DIRECTORY = join3(process.cwd(), "resources/views");
3941
+ var DEFAULT_VIEWS_DIRECTORY = join4(process.cwd(), "resources/views");
3888
3942
  var DEFAULT_LAYOUT = "layouts/app.eta";
3889
3943
 
3890
3944
  class EtaViewEngine {
@@ -4177,7 +4231,7 @@ function assertProductionSecrets(env = process.env) {
4177
4231
  }
4178
4232
 
4179
4233
  // ../../src/bootstrap/context.ts
4180
- function collectProviders(modules = appModules) {
4234
+ function collectProviders(modules = discoverModules()) {
4181
4235
  return [...coreProviders, ...modules.flatMap((module) => module.providers ?? [])];
4182
4236
  }
4183
4237
  function runProviderPhase(providers, phase, context) {
@@ -1,6 +1,6 @@
1
1
  // @bun
2
2
  // ../../src/bootstrap/createWebRoutes.ts
3
- import { join as join2 } from "path";
3
+ import { join as join3 } from "path";
4
4
 
5
5
  // ../../src/config/frontend.ts
6
6
  function readFrontendMode() {
@@ -908,9 +908,63 @@ function createHttpKernel(dependencies) {
908
908
  }
909
909
 
910
910
  // ../../src/bootstrap/discoverModules.ts
911
- var appModules = [];
911
+ import { readdirSync } from "fs";
912
+ import { join as join2 } from "path";
913
+ import { pathToFileURL } from "url";
914
+ var DISCOVER_MODULES_STATE_KEY = Symbol.for("@getstrata/discoverModulesState");
915
+ function readDiscoverModulesState() {
916
+ const existing = globalThis[DISCOVER_MODULES_STATE_KEY];
917
+ if (existing) {
918
+ return existing;
919
+ }
920
+ const state = { appModules: [] };
921
+ globalThis[DISCOVER_MODULES_STATE_KEY] = state;
922
+ return state;
923
+ }
924
+ function configureModulesDirectory(modulesDir) {
925
+ readDiscoverModulesState().configuredModulesDir = modulesDir;
926
+ }
927
+ function resolveModulesDirectory(options) {
928
+ const state = readDiscoverModulesState();
929
+ if (options?.modulesDir) {
930
+ return options.modulesDir;
931
+ }
932
+ if (state.configuredModulesDir) {
933
+ return state.configuredModulesDir;
934
+ }
935
+ return join2(import.meta.dir, "../modules");
936
+ }
937
+ async function loadDiscoveredModules(options) {
938
+ const modulesDirectory = resolveModulesDirectory(options);
939
+ let moduleNames;
940
+ try {
941
+ moduleNames = readdirSync(modulesDirectory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
942
+ } catch (error) {
943
+ if (error.code === "ENOENT") {
944
+ return [];
945
+ }
946
+ throw error;
947
+ }
948
+ const modules = await Promise.all(moduleNames.map(async (moduleName) => {
949
+ const moduleUrl = pathToFileURL(join2(modulesDirectory, moduleName, "index.ts")).href;
950
+ const loaded = await import(moduleUrl);
951
+ return loaded.default;
952
+ }));
953
+ return modules.filter((module) => module?.name !== undefined).sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
954
+ }
955
+ async function ensureModulesLoaded(options) {
956
+ const state = readDiscoverModulesState();
957
+ if (state.appModules.length > 0) {
958
+ return state.appModules;
959
+ }
960
+ state.modulesReady ??= loadDiscoveredModules(options).then((modules) => {
961
+ state.appModules.splice(0, state.appModules.length, ...modules);
962
+ return state.appModules;
963
+ });
964
+ return state.modulesReady;
965
+ }
912
966
  function discoverModules() {
913
- return appModules;
967
+ return readDiscoverModulesState().appModules;
914
968
  }
915
969
  // ../../src/bootstrap/routeRegistry.ts
916
970
  class RouteRegistry {
@@ -964,7 +1018,7 @@ function createWebRoutes(dependencies) {
964
1018
  "/": () => Response.redirect("/organizations", 302)
965
1019
  };
966
1020
  registerRoute("GET", "/", ["global", "web"]);
967
- for (const module of appModules) {
1021
+ for (const module of discoverModules()) {
968
1022
  if (!module.webRoutes) {
969
1023
  continue;
970
1024
  }
@@ -979,7 +1033,7 @@ function createWebRoutes(dependencies) {
979
1033
  registerRoute("GET", "/assets/*", ["global", "web"]);
980
1034
  const pathname = new URL(request.url).pathname;
981
1035
  const relativePath = pathname.replace(/^\//, "");
982
- const file = Bun.file(join2(process.cwd(), "public", relativePath));
1036
+ const file = Bun.file(join3(process.cwd(), "public", relativePath));
983
1037
  if (!await file.exists()) {
984
1038
  return htmlResponse("Not Found", { status: 404 });
985
1039
  }
@@ -179,9 +179,63 @@ class ConfigStore {
179
179
  }
180
180
  }
181
181
  // ../../src/bootstrap/discoverModules.ts
182
- var appModules = [];
182
+ import { readdirSync } from "fs";
183
+ import { join } from "path";
184
+ import { pathToFileURL } from "url";
185
+ var DISCOVER_MODULES_STATE_KEY = Symbol.for("@getstrata/discoverModulesState");
186
+ function readDiscoverModulesState() {
187
+ const existing = globalThis[DISCOVER_MODULES_STATE_KEY];
188
+ if (existing) {
189
+ return existing;
190
+ }
191
+ const state = { appModules: [] };
192
+ globalThis[DISCOVER_MODULES_STATE_KEY] = state;
193
+ return state;
194
+ }
195
+ function configureModulesDirectory(modulesDir) {
196
+ readDiscoverModulesState().configuredModulesDir = modulesDir;
197
+ }
198
+ function resolveModulesDirectory(options) {
199
+ const state = readDiscoverModulesState();
200
+ if (options?.modulesDir) {
201
+ return options.modulesDir;
202
+ }
203
+ if (state.configuredModulesDir) {
204
+ return state.configuredModulesDir;
205
+ }
206
+ return join(import.meta.dir, "../modules");
207
+ }
208
+ async function loadDiscoveredModules(options) {
209
+ const modulesDirectory = resolveModulesDirectory(options);
210
+ let moduleNames;
211
+ try {
212
+ moduleNames = readdirSync(modulesDirectory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
213
+ } catch (error) {
214
+ if (error.code === "ENOENT") {
215
+ return [];
216
+ }
217
+ throw error;
218
+ }
219
+ const modules = await Promise.all(moduleNames.map(async (moduleName) => {
220
+ const moduleUrl = pathToFileURL(join(modulesDirectory, moduleName, "index.ts")).href;
221
+ const loaded = await import(moduleUrl);
222
+ return loaded.default;
223
+ }));
224
+ return modules.filter((module) => module?.name !== undefined).sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
225
+ }
226
+ async function ensureModulesLoaded(options) {
227
+ const state = readDiscoverModulesState();
228
+ if (state.appModules.length > 0) {
229
+ return state.appModules;
230
+ }
231
+ state.modulesReady ??= loadDiscoveredModules(options).then((modules) => {
232
+ state.appModules.splice(0, state.appModules.length, ...modules);
233
+ return state.appModules;
234
+ });
235
+ return state.modulesReady;
236
+ }
183
237
  function discoverModules() {
184
- return appModules;
238
+ return readDiscoverModulesState().appModules;
185
239
  }
186
240
  // ../../src/config/auth.ts
187
241
  var authConfig = {
@@ -1462,14 +1516,14 @@ var eventsProvider = {
1462
1516
  var events_default = eventsProvider;
1463
1517
 
1464
1518
  // ../../src/bootstrap/discoverListeners.ts
1465
- import { readdirSync } from "fs";
1466
- import { join } from "path";
1467
- import { pathToFileURL } from "url";
1519
+ import { readdirSync as readdirSync2 } from "fs";
1520
+ import { join as join2 } from "path";
1521
+ import { pathToFileURL as pathToFileURL2 } from "url";
1468
1522
  async function loadDiscoveredListeners() {
1469
- const listenersDirectory = join(import.meta.dir, "../listeners");
1523
+ const listenersDirectory = join2(import.meta.dir, "../listeners");
1470
1524
  let entries;
1471
1525
  try {
1472
- entries = readdirSync(listenersDirectory, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".ts")).map((entry) => entry.name);
1526
+ entries = readdirSync2(listenersDirectory, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".ts")).map((entry) => entry.name);
1473
1527
  } catch (error) {
1474
1528
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
1475
1529
  return [];
@@ -1477,7 +1531,7 @@ async function loadDiscoveredListeners() {
1477
1531
  throw error;
1478
1532
  }
1479
1533
  const listeners = await Promise.all(entries.map(async (fileName) => {
1480
- const moduleUrl = pathToFileURL(join(listenersDirectory, fileName)).href;
1534
+ const moduleUrl = pathToFileURL2(join2(listenersDirectory, fileName)).href;
1481
1535
  const loaded = await import(moduleUrl);
1482
1536
  return loaded.default;
1483
1537
  }));
@@ -1798,10 +1852,10 @@ function buildHavingClause(tableName, having, params) {
1798
1852
  return body.length > 0 ? ` HAVING ${body}` : "";
1799
1853
  }
1800
1854
  function buildJoinClause(joins = []) {
1801
- return joins.map((join2) => {
1802
- const joinType = join2.type === "left" ? "LEFT JOIN" : "INNER JOIN";
1803
- const onClause = join2.on.map(({ left, right }) => `${qualifyColumn(left.table, left.column)} = ${qualifyColumn(right.table, right.column)}`).join(" AND ");
1804
- return ` ${joinType} ${quoteIdentifier(join2.table)} ON ${onClause}`;
1855
+ return joins.map((join3) => {
1856
+ const joinType = join3.type === "left" ? "LEFT JOIN" : "INNER JOIN";
1857
+ const onClause = join3.on.map(({ left, right }) => `${qualifyColumn(left.table, left.column)} = ${qualifyColumn(right.table, right.column)}`).join(" AND ");
1858
+ return ` ${joinType} ${quoteIdentifier(join3.table)} ON ${onClause}`;
1805
1859
  }).join("");
1806
1860
  }
1807
1861
  function buildLimitClause(limit) {
@@ -2237,7 +2291,7 @@ class RepositoryQuery {
2237
2291
  const rightRef = parseQualifiedColumn(right);
2238
2292
  const table = type === "inner" ? rightRef.table : rightRef.table;
2239
2293
  const joins = this.queryOptions.joins ?? [];
2240
- const existing = joins.find((join2) => join2.table === table && join2.type === type);
2294
+ const existing = joins.find((join3) => join3.table === table && join3.type === type);
2241
2295
  if (existing) {
2242
2296
  existing.on.push({ left: leftRef, right: rightRef });
2243
2297
  return this;
@@ -3737,7 +3791,7 @@ var queue_default = queueProvider;
3737
3791
 
3738
3792
  // ../../src/core/storage/storage.ts
3739
3793
  import { mkdir, readFile, unlink, writeFile } from "fs/promises";
3740
- import { dirname, join as join2 } from "path";
3794
+ import { dirname, join as join3 } from "path";
3741
3795
  var {S3Client } = globalThis.Bun;
3742
3796
 
3743
3797
  class LocalStorageDriver {
@@ -3749,7 +3803,7 @@ class LocalStorageDriver {
3749
3803
  return this.rootDirectory ?? process.env.STORAGE_PATH ?? "storage";
3750
3804
  }
3751
3805
  resolvePath(path) {
3752
- return join2(this.resolveRootDirectory(), path.replace(/^\/+/, ""));
3806
+ return join3(this.resolveRootDirectory(), path.replace(/^\/+/, ""));
3753
3807
  }
3754
3808
  async put(path, contents) {
3755
3809
  const absolutePath = this.resolvePath(path);
@@ -3882,9 +3936,9 @@ function currentRequestMeta() {
3882
3936
  }
3883
3937
 
3884
3938
  // ../../src/core/view/etaViewEngine.ts
3885
- import { join as join3 } from "path";
3939
+ import { join as join4 } from "path";
3886
3940
  import { Eta } from "eta";
3887
- var DEFAULT_VIEWS_DIRECTORY = join3(process.cwd(), "resources/views");
3941
+ var DEFAULT_VIEWS_DIRECTORY = join4(process.cwd(), "resources/views");
3888
3942
  var DEFAULT_LAYOUT = "layouts/app.eta";
3889
3943
 
3890
3944
  class EtaViewEngine {
@@ -4177,7 +4231,7 @@ function assertProductionSecrets(env = process.env) {
4177
4231
  }
4178
4232
 
4179
4233
  // ../../src/bootstrap/context.ts
4180
- function collectProviders(modules = appModules) {
4234
+ function collectProviders(modules = discoverModules()) {
4181
4235
  return [...coreProviders, ...modules.flatMap((module) => module.providers ?? [])];
4182
4236
  }
4183
4237
  function runProviderPhase(providers, phase, context) {
@@ -0,0 +1,65 @@
1
+ // @bun
2
+ // ../../src/bootstrap/discoverModules.ts
3
+ import { readdirSync } from "fs";
4
+ import { join } from "path";
5
+ import { pathToFileURL } from "url";
6
+ var DISCOVER_MODULES_STATE_KEY = Symbol.for("@getstrata/discoverModulesState");
7
+ function readDiscoverModulesState() {
8
+ const existing = globalThis[DISCOVER_MODULES_STATE_KEY];
9
+ if (existing) {
10
+ return existing;
11
+ }
12
+ const state = { appModules: [] };
13
+ globalThis[DISCOVER_MODULES_STATE_KEY] = state;
14
+ return state;
15
+ }
16
+ function configureModulesDirectory(modulesDir) {
17
+ readDiscoverModulesState().configuredModulesDir = modulesDir;
18
+ }
19
+ function resolveModulesDirectory(options) {
20
+ const state = readDiscoverModulesState();
21
+ if (options?.modulesDir) {
22
+ return options.modulesDir;
23
+ }
24
+ if (state.configuredModulesDir) {
25
+ return state.configuredModulesDir;
26
+ }
27
+ return join(import.meta.dir, "../modules");
28
+ }
29
+ async function loadDiscoveredModules(options) {
30
+ const modulesDirectory = resolveModulesDirectory(options);
31
+ let moduleNames;
32
+ try {
33
+ moduleNames = readdirSync(modulesDirectory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
34
+ } catch (error) {
35
+ if (error.code === "ENOENT") {
36
+ return [];
37
+ }
38
+ throw error;
39
+ }
40
+ const modules = await Promise.all(moduleNames.map(async (moduleName) => {
41
+ const moduleUrl = pathToFileURL(join(modulesDirectory, moduleName, "index.ts")).href;
42
+ const loaded = await import(moduleUrl);
43
+ return loaded.default;
44
+ }));
45
+ return modules.filter((module) => module?.name !== undefined).sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
46
+ }
47
+ async function ensureModulesLoaded(options) {
48
+ const state = readDiscoverModulesState();
49
+ if (state.appModules.length > 0) {
50
+ return state.appModules;
51
+ }
52
+ state.modulesReady ??= loadDiscoveredModules(options).then((modules) => {
53
+ state.appModules.splice(0, state.appModules.length, ...modules);
54
+ return state.appModules;
55
+ });
56
+ return state.modulesReady;
57
+ }
58
+ function discoverModules() {
59
+ return readDiscoverModulesState().appModules;
60
+ }
61
+ export {
62
+ ensureModulesLoaded,
63
+ discoverModules,
64
+ configureModulesDirectory
65
+ };
@@ -3351,9 +3351,63 @@ function resolveApplicationDependencies() {
3351
3351
  return requireActiveApplicationContext().dependencies;
3352
3352
  }
3353
3353
  // ../../src/bootstrap/discoverModules.ts
3354
- var appModules = [];
3354
+ import { readdirSync as readdirSync2 } from "fs";
3355
+ import { join as join2 } from "path";
3356
+ import { pathToFileURL as pathToFileURL2 } from "url";
3357
+ var DISCOVER_MODULES_STATE_KEY = Symbol.for("@getstrata/discoverModulesState");
3358
+ function readDiscoverModulesState() {
3359
+ const existing = globalThis[DISCOVER_MODULES_STATE_KEY];
3360
+ if (existing) {
3361
+ return existing;
3362
+ }
3363
+ const state = { appModules: [] };
3364
+ globalThis[DISCOVER_MODULES_STATE_KEY] = state;
3365
+ return state;
3366
+ }
3367
+ function configureModulesDirectory(modulesDir) {
3368
+ readDiscoverModulesState().configuredModulesDir = modulesDir;
3369
+ }
3370
+ function resolveModulesDirectory(options) {
3371
+ const state = readDiscoverModulesState();
3372
+ if (options?.modulesDir) {
3373
+ return options.modulesDir;
3374
+ }
3375
+ if (state.configuredModulesDir) {
3376
+ return state.configuredModulesDir;
3377
+ }
3378
+ return join2(import.meta.dir, "../modules");
3379
+ }
3380
+ async function loadDiscoveredModules(options) {
3381
+ const modulesDirectory = resolveModulesDirectory(options);
3382
+ let moduleNames;
3383
+ try {
3384
+ moduleNames = readdirSync2(modulesDirectory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
3385
+ } catch (error) {
3386
+ if (error.code === "ENOENT") {
3387
+ return [];
3388
+ }
3389
+ throw error;
3390
+ }
3391
+ const modules = await Promise.all(moduleNames.map(async (moduleName) => {
3392
+ const moduleUrl = pathToFileURL2(join2(modulesDirectory, moduleName, "index.ts")).href;
3393
+ const loaded = await import(moduleUrl);
3394
+ return loaded.default;
3395
+ }));
3396
+ return modules.filter((module) => module?.name !== undefined).sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
3397
+ }
3398
+ async function ensureModulesLoaded(options) {
3399
+ const state = readDiscoverModulesState();
3400
+ if (state.appModules.length > 0) {
3401
+ return state.appModules;
3402
+ }
3403
+ state.modulesReady ??= loadDiscoveredModules(options).then((modules) => {
3404
+ state.appModules.splice(0, state.appModules.length, ...modules);
3405
+ return state.appModules;
3406
+ });
3407
+ return state.modulesReady;
3408
+ }
3355
3409
  function discoverModules() {
3356
- return appModules;
3410
+ return readDiscoverModulesState().appModules;
3357
3411
  }
3358
3412
 
3359
3413
  // ../../src/bootstrap/cache/modelCacheTags.ts
@@ -3674,7 +3728,7 @@ var queue_default = queueProvider;
3674
3728
 
3675
3729
  // ../../src/core/storage/storage.ts
3676
3730
  import { mkdir, readFile, unlink, writeFile } from "fs/promises";
3677
- import { dirname, join as join2 } from "path";
3731
+ import { dirname, join as join3 } from "path";
3678
3732
  var {S3Client } = globalThis.Bun;
3679
3733
 
3680
3734
  class LocalStorageDriver {
@@ -3686,7 +3740,7 @@ class LocalStorageDriver {
3686
3740
  return this.rootDirectory ?? process.env.STORAGE_PATH ?? "storage";
3687
3741
  }
3688
3742
  resolvePath(path) {
3689
- return join2(this.resolveRootDirectory(), path.replace(/^\/+/, ""));
3743
+ return join3(this.resolveRootDirectory(), path.replace(/^\/+/, ""));
3690
3744
  }
3691
3745
  async put(path, contents) {
3692
3746
  const absolutePath = this.resolvePath(path);
@@ -3819,9 +3873,9 @@ function currentRequestMeta() {
3819
3873
  }
3820
3874
 
3821
3875
  // ../../src/core/view/etaViewEngine.ts
3822
- import { join as join3 } from "path";
3876
+ import { join as join4 } from "path";
3823
3877
  import { Eta } from "eta";
3824
- var DEFAULT_VIEWS_DIRECTORY = join3(process.cwd(), "resources/views");
3878
+ var DEFAULT_VIEWS_DIRECTORY = join4(process.cwd(), "resources/views");
3825
3879
  var DEFAULT_LAYOUT = "layouts/app.eta";
3826
3880
 
3827
3881
  class EtaViewEngine {
package/dist/index.js CHANGED
@@ -583,9 +583,63 @@ function resolveApplicationDependencies() {
583
583
  return requireActiveApplicationContext().dependencies;
584
584
  }
585
585
  // ../../src/bootstrap/discoverModules.ts
586
- var appModules = [];
586
+ import { readdirSync } from "fs";
587
+ import { join } from "path";
588
+ import { pathToFileURL } from "url";
589
+ var DISCOVER_MODULES_STATE_KEY = Symbol.for("@getstrata/discoverModulesState");
590
+ function readDiscoverModulesState() {
591
+ const existing = globalThis[DISCOVER_MODULES_STATE_KEY];
592
+ if (existing) {
593
+ return existing;
594
+ }
595
+ const state = { appModules: [] };
596
+ globalThis[DISCOVER_MODULES_STATE_KEY] = state;
597
+ return state;
598
+ }
599
+ function configureModulesDirectory(modulesDir) {
600
+ readDiscoverModulesState().configuredModulesDir = modulesDir;
601
+ }
602
+ function resolveModulesDirectory(options) {
603
+ const state = readDiscoverModulesState();
604
+ if (options?.modulesDir) {
605
+ return options.modulesDir;
606
+ }
607
+ if (state.configuredModulesDir) {
608
+ return state.configuredModulesDir;
609
+ }
610
+ return join(import.meta.dir, "../modules");
611
+ }
612
+ async function loadDiscoveredModules(options) {
613
+ const modulesDirectory = resolveModulesDirectory(options);
614
+ let moduleNames;
615
+ try {
616
+ moduleNames = readdirSync(modulesDirectory, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name);
617
+ } catch (error) {
618
+ if (error.code === "ENOENT") {
619
+ return [];
620
+ }
621
+ throw error;
622
+ }
623
+ const modules = await Promise.all(moduleNames.map(async (moduleName) => {
624
+ const moduleUrl = pathToFileURL(join(modulesDirectory, moduleName, "index.ts")).href;
625
+ const loaded = await import(moduleUrl);
626
+ return loaded.default;
627
+ }));
628
+ return modules.filter((module) => module?.name !== undefined).sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
629
+ }
630
+ async function ensureModulesLoaded(options) {
631
+ const state = readDiscoverModulesState();
632
+ if (state.appModules.length > 0) {
633
+ return state.appModules;
634
+ }
635
+ state.modulesReady ??= loadDiscoveredModules(options).then((modules) => {
636
+ state.appModules.splice(0, state.appModules.length, ...modules);
637
+ return state.appModules;
638
+ });
639
+ return state.modulesReady;
640
+ }
587
641
  function discoverModules() {
588
- return appModules;
642
+ return readDiscoverModulesState().appModules;
589
643
  }
590
644
 
591
645
  // ../../src/bootstrap/cache/modelCacheTags.ts
@@ -1803,14 +1857,14 @@ var eventsProvider = {
1803
1857
  var events_default = eventsProvider;
1804
1858
 
1805
1859
  // ../../src/bootstrap/discoverListeners.ts
1806
- import { readdirSync } from "fs";
1807
- import { join } from "path";
1808
- import { pathToFileURL } from "url";
1860
+ import { readdirSync as readdirSync2 } from "fs";
1861
+ import { join as join2 } from "path";
1862
+ import { pathToFileURL as pathToFileURL2 } from "url";
1809
1863
  async function loadDiscoveredListeners() {
1810
- const listenersDirectory = join(import.meta.dir, "../listeners");
1864
+ const listenersDirectory = join2(import.meta.dir, "../listeners");
1811
1865
  let entries;
1812
1866
  try {
1813
- entries = readdirSync(listenersDirectory, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".ts")).map((entry) => entry.name);
1867
+ entries = readdirSync2(listenersDirectory, { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".ts")).map((entry) => entry.name);
1814
1868
  } catch (error) {
1815
1869
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
1816
1870
  return [];
@@ -1818,7 +1872,7 @@ async function loadDiscoveredListeners() {
1818
1872
  throw error;
1819
1873
  }
1820
1874
  const listeners = await Promise.all(entries.map(async (fileName) => {
1821
- const moduleUrl = pathToFileURL(join(listenersDirectory, fileName)).href;
1875
+ const moduleUrl = pathToFileURL2(join2(listenersDirectory, fileName)).href;
1822
1876
  const loaded = await import(moduleUrl);
1823
1877
  return loaded.default;
1824
1878
  }));
@@ -2139,10 +2193,10 @@ function buildHavingClause(tableName, having, params) {
2139
2193
  return body.length > 0 ? ` HAVING ${body}` : "";
2140
2194
  }
2141
2195
  function buildJoinClause(joins = []) {
2142
- return joins.map((join2) => {
2143
- const joinType = join2.type === "left" ? "LEFT JOIN" : "INNER JOIN";
2144
- const onClause = join2.on.map(({ left, right }) => `${qualifyColumn(left.table, left.column)} = ${qualifyColumn(right.table, right.column)}`).join(" AND ");
2145
- return ` ${joinType} ${quoteIdentifier(join2.table)} ON ${onClause}`;
2196
+ return joins.map((join3) => {
2197
+ const joinType = join3.type === "left" ? "LEFT JOIN" : "INNER JOIN";
2198
+ const onClause = join3.on.map(({ left, right }) => `${qualifyColumn(left.table, left.column)} = ${qualifyColumn(right.table, right.column)}`).join(" AND ");
2199
+ return ` ${joinType} ${quoteIdentifier(join3.table)} ON ${onClause}`;
2146
2200
  }).join("");
2147
2201
  }
2148
2202
  function buildLimitClause(limit) {
@@ -2555,7 +2609,7 @@ class RepositoryQuery {
2555
2609
  const rightRef = parseQualifiedColumn(right);
2556
2610
  const table = type === "inner" ? rightRef.table : rightRef.table;
2557
2611
  const joins = this.queryOptions.joins ?? [];
2558
- const existing = joins.find((join2) => join2.table === table && join2.type === type);
2612
+ const existing = joins.find((join3) => join3.table === table && join3.type === type);
2559
2613
  if (existing) {
2560
2614
  existing.on.push({ left: leftRef, right: rightRef });
2561
2615
  return this;
@@ -3917,7 +3971,7 @@ var queue_default = queueProvider;
3917
3971
 
3918
3972
  // ../../src/core/storage/storage.ts
3919
3973
  import { mkdir, readFile, unlink, writeFile } from "fs/promises";
3920
- import { dirname, join as join2 } from "path";
3974
+ import { dirname, join as join3 } from "path";
3921
3975
  var {S3Client } = globalThis.Bun;
3922
3976
 
3923
3977
  class LocalStorageDriver {
@@ -3929,7 +3983,7 @@ class LocalStorageDriver {
3929
3983
  return this.rootDirectory ?? process.env.STORAGE_PATH ?? "storage";
3930
3984
  }
3931
3985
  resolvePath(path) {
3932
- return join2(this.resolveRootDirectory(), path.replace(/^\/+/, ""));
3986
+ return join3(this.resolveRootDirectory(), path.replace(/^\/+/, ""));
3933
3987
  }
3934
3988
  async put(path, contents) {
3935
3989
  const absolutePath = this.resolvePath(path);
@@ -4062,9 +4116,9 @@ function currentRequestMeta() {
4062
4116
  }
4063
4117
 
4064
4118
  // ../../src/core/view/etaViewEngine.ts
4065
- import { join as join3 } from "path";
4119
+ import { join as join4 } from "path";
4066
4120
  import { Eta } from "eta";
4067
- var DEFAULT_VIEWS_DIRECTORY = join3(process.cwd(), "resources/views");
4121
+ var DEFAULT_VIEWS_DIRECTORY = join4(process.cwd(), "resources/views");
4068
4122
  var DEFAULT_LAYOUT = "layouts/app.eta";
4069
4123
 
4070
4124
  class EtaViewEngine {
@@ -4357,7 +4411,7 @@ function assertProductionSecrets(env = process.env) {
4357
4411
  }
4358
4412
 
4359
4413
  // ../../src/bootstrap/context.ts
4360
- function collectProviders(modules = appModules) {
4414
+ function collectProviders(modules = discoverModules()) {
4361
4415
  return [...coreProviders, ...modules.flatMap((module) => module.providers ?? [])];
4362
4416
  }
4363
4417
  function runProviderPhase(providers, phase, context) {
@@ -4390,7 +4444,7 @@ function createAppContext() {
4390
4444
  return appContext;
4391
4445
  }
4392
4446
  // ../../src/bootstrap/createWebRoutes.ts
4393
- import { join as join4 } from "path";
4447
+ import { join as join5 } from "path";
4394
4448
 
4395
4449
  // ../../src/core/http/middleware.ts
4396
4450
  function isRouteHandler(value) {
@@ -4719,7 +4773,7 @@ function createWebRoutes(dependencies) {
4719
4773
  "/": () => Response.redirect("/organizations", 302)
4720
4774
  };
4721
4775
  registerRoute("GET", "/", ["global", "web"]);
4722
- for (const module of appModules) {
4776
+ for (const module of discoverModules()) {
4723
4777
  if (!module.webRoutes) {
4724
4778
  continue;
4725
4779
  }
@@ -4734,7 +4788,7 @@ function createWebRoutes(dependencies) {
4734
4788
  registerRoute("GET", "/assets/*", ["global", "web"]);
4735
4789
  const pathname = new URL(request.url).pathname;
4736
4790
  const relativePath = pathname.replace(/^\//, "");
4737
- const file = Bun.file(join4(process.cwd(), "public", relativePath));
4791
+ const file = Bun.file(join5(process.cwd(), "public", relativePath));
4738
4792
  if (!await file.exists()) {
4739
4793
  return htmlResponse("Not Found", { status: 404 });
4740
4794
  }
@@ -5304,6 +5358,8 @@ export {
5304
5358
  parseFormBody,
5305
5359
  mergeWebRoutes,
5306
5360
  getRequiredDependency,
5361
+ ensureModulesLoaded,
5362
+ discoverModules,
5307
5363
  discoverModelTableNames,
5308
5364
  createWebServer,
5309
5365
  createWebRoutes,
@@ -5313,6 +5369,7 @@ export {
5313
5369
  createAppDependencies,
5314
5370
  createAppContext,
5315
5371
  coreProviders,
5372
+ configureModulesDirectory,
5316
5373
  collectProviders,
5317
5374
  cacheTagsForModelWrite,
5318
5375
  assertProductionSecrets,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getstrata/bootstrap",
3
- "version": "0.2.21",
3
+ "version": "0.2.22",
4
4
  "description": "Strata application bootstrap — HttpKernel, DI, web session helpers",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -50,6 +50,11 @@
50
50
  "import": "./dist/entries/dependencies.js",
51
51
  "default": "./dist/entries/dependencies.js"
52
52
  },
53
+ "./discoverModules": {
54
+ "types": "./dist/bootstrap/discoverModules.d.ts",
55
+ "import": "./dist/entries/discoverModules.js",
56
+ "default": "./dist/entries/discoverModules.js"
57
+ },
53
58
  "./http/securedRouteModelBinding": {
54
59
  "types": "./dist/bootstrap/http/securedRouteModelBinding.d.ts",
55
60
  "import": "./dist/entries/http/securedRouteModelBinding.js",
@@ -127,14 +132,14 @@
127
132
  "build:bundle": "bun build index.ts --outdir dist --target bun --external bun --external eta --external @getstrata/core",
128
133
  "build:types": "tsc -p tsconfig.types.json",
129
134
  "prepublishOnly": "bun run build",
130
- "build:subpaths": "bun build entries/applicationRegistry.ts entries/cache/modelCacheTags.ts entries/config.ts entries/context.ts entries/contracts.ts entries/createWebRoutes.ts entries/dependencies.ts entries/http/securedRouteModelBinding.ts entries/httpKernel.ts entries/membershipService.ts entries/queue/defaultJobs.ts entries/providers.ts entries/providers/view.ts entries/routeRegistry.ts entries/secretsGuard.ts entries/web/forms.ts entries/web/routing.ts entries/web/server.ts entries/web/session.ts entries/web/slug.ts --outdir dist --root . --target bun --external bun --external eta --external @getstrata/core",
135
+ "build:subpaths": "bun build entries/applicationRegistry.ts entries/cache/modelCacheTags.ts entries/config.ts entries/context.ts entries/contracts.ts entries/createWebRoutes.ts entries/dependencies.ts entries/discoverModules.ts entries/http/securedRouteModelBinding.ts entries/httpKernel.ts entries/membershipService.ts entries/queue/defaultJobs.ts entries/providers.ts entries/providers/view.ts entries/routeRegistry.ts entries/secretsGuard.ts entries/web/forms.ts entries/web/routing.ts entries/web/server.ts entries/web/session.ts entries/web/slug.ts --outdir dist --root . --target bun --external bun --external eta --external @getstrata/core",
131
136
  "build:shims": "true"
132
137
  },
133
138
  "publishConfig": {
134
139
  "access": "public"
135
140
  },
136
141
  "peerDependencies": {
137
- "@getstrata/core": "^0.5.29",
142
+ "@getstrata/core": "^0.5.30",
138
143
  "typescript": "^5.9.0"
139
144
  }
140
145
  }