@getstrata/bootstrap 2.0.9 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/README.md +1 -1
- package/dist/bootstrap/app.d.ts +7 -1
- package/dist/bootstrap/config.d.ts +1 -1
- package/dist/bootstrap/context.d.ts +8 -2
- package/dist/bootstrap/contracts.d.ts +1 -1
- package/dist/bootstrap/dependencies.d.ts +1 -1
- package/dist/bootstrap/discoverJobs.d.ts +11 -3
- package/dist/bootstrap/discoverListeners.d.ts +7 -3
- package/dist/bootstrap/discoverModels.d.ts +8 -0
- package/dist/bootstrap/discoverModules.d.ts +4 -1
- package/dist/bootstrap/httpKernel.d.ts +25 -20
- package/dist/bootstrap/inProcessCron.d.ts +2 -1
- package/dist/bootstrap/infrastructureDiscovery.d.ts +9 -0
- package/dist/bootstrap/modules.d.ts +1 -1
- package/dist/bootstrap/providers/tracing.d.ts +3 -0
- package/dist/bootstrap/public-api.d.ts +9 -4
- package/dist/bootstrap/web/index.d.ts +1 -1
- package/dist/bootstrap/web/routing.d.ts +2 -2
- package/dist/bootstrap/web/server.d.ts +15 -7
- package/dist/entries/buildModuleRoutes.js +212 -67
- package/dist/entries/buildWebModuleRoutes.js +212 -67
- package/dist/entries/cache/modelCacheTags.js +157 -51
- package/dist/entries/config.js +6 -0
- package/dist/entries/context.js +427 -188
- package/dist/entries/contracts.js +2 -0
- package/dist/entries/createWebRoutes.js +214 -69
- package/dist/entries/dependencies.js +429 -190
- package/dist/entries/discoverJobs.js +132 -50
- package/dist/entries/discoverListeners.js +85 -37
- package/dist/entries/discoverModels.js +39 -0
- package/dist/entries/discoverModules.js +158 -51
- package/dist/entries/health.js +3 -0
- package/dist/entries/httpKernel.js +52 -13
- package/dist/entries/listeners/invalidateCacheOnModelWrite.js +157 -51
- package/dist/entries/providers.js +307 -164
- package/dist/entries/web/routing.js +52 -13
- package/dist/entries/web/server.js +69 -14
- package/dist/index.js +591 -216
- package/package.json +8 -3
|
@@ -1,67 +1,149 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../../src/bootstrap/discoverJobs.ts
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { join } from "path";
|
|
3
|
+
import { join as join2, resolve } from "path";
|
|
4
|
+
import { Job } from "@getstrata/core/queue";
|
|
6
5
|
import { jobRegistry } from "@getstrata/core/queue/jobRegistry";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
6
|
+
|
|
7
|
+
// ../../src/bootstrap/infrastructureDiscovery.ts
|
|
8
|
+
import { readdir } from "fs/promises";
|
|
9
|
+
import { join, relative } from "path";
|
|
10
|
+
import { pathToFileURL } from "url";
|
|
11
|
+
async function infrastructureFiles(directory, exclude = []) {
|
|
12
|
+
const excluded = new Set([
|
|
13
|
+
"index.ts",
|
|
14
|
+
"index.js",
|
|
15
|
+
"index.mts",
|
|
16
|
+
"index.mjs",
|
|
17
|
+
"register.ts",
|
|
18
|
+
"register.js",
|
|
19
|
+
"register.mts",
|
|
20
|
+
"register.mjs",
|
|
21
|
+
...exclude
|
|
22
|
+
]);
|
|
23
|
+
const files = [];
|
|
24
|
+
async function visit(path) {
|
|
25
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
26
|
+
entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
const file = join(path, entry.name);
|
|
29
|
+
const key = relative(directory, file).replaceAll("\\", "/");
|
|
30
|
+
if (excluded.has(entry.name) || excluded.has(key))
|
|
31
|
+
continue;
|
|
32
|
+
if (entry.isDirectory())
|
|
33
|
+
await visit(file);
|
|
34
|
+
else if (entry.isFile() && /\.(ts|js|mts|mjs)$/.test(entry.name) && !/\.(d|test|spec)\.[cm]?[jt]s$/.test(entry.name))
|
|
35
|
+
files.push(file);
|
|
36
|
+
}
|
|
22
37
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
38
|
+
try {
|
|
39
|
+
await visit(directory);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (error.code !== "ENOENT")
|
|
42
|
+
throw error;
|
|
43
|
+
if (error.path !== directory)
|
|
44
|
+
throw error;
|
|
28
45
|
}
|
|
29
|
-
|
|
30
|
-
return typeof jobName === "string" && jobName.length > 0;
|
|
46
|
+
return files;
|
|
31
47
|
}
|
|
32
|
-
function
|
|
33
|
-
const jobsDirectory = resolveJobsDirectory();
|
|
34
|
-
let entries;
|
|
48
|
+
async function importInfrastructure(file) {
|
|
35
49
|
try {
|
|
36
|
-
|
|
50
|
+
return await import(pathToFileURL(file).href);
|
|
37
51
|
} catch (error) {
|
|
38
|
-
|
|
39
|
-
return [];
|
|
40
|
-
}
|
|
41
|
-
throw error;
|
|
52
|
+
throw new Error(`Cannot load infrastructure module ${file}`, { cause: error });
|
|
42
53
|
}
|
|
43
|
-
return entries.flatMap((fileName) => {
|
|
44
|
-
const filePath = join(jobsDirectory, fileName);
|
|
45
|
-
const loaded = requireJob(filePath);
|
|
46
|
-
return isDiscoveredJobClass(loaded.default) ? [loaded.default] : [];
|
|
47
|
-
});
|
|
48
54
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
55
|
+
|
|
56
|
+
// ../../src/bootstrap/discoverJobs.ts
|
|
57
|
+
var KEY = Symbol.for("@getstrata/discoverJobsState");
|
|
58
|
+
function state() {
|
|
59
|
+
const globals = globalThis;
|
|
60
|
+
globals[KEY] ??= { loads: new Map, registered: new Map };
|
|
61
|
+
return globals[KEY];
|
|
62
|
+
}
|
|
63
|
+
function validateClass(value, location) {
|
|
64
|
+
if (typeof value !== "function" || typeof value.jobName !== "string" || !value.jobName.trim() || !(value.prototype instanceof Job) && typeof value.prototype?.handle !== "function")
|
|
65
|
+
throw new TypeError(`Invalid job at ${location}: expected a class with nonempty static jobName and handle().`);
|
|
66
|
+
}
|
|
67
|
+
async function load(options, current) {
|
|
68
|
+
const jobs = new Map;
|
|
69
|
+
const add = (name, identity, create, location) => {
|
|
70
|
+
const previous = jobs.get(name);
|
|
71
|
+
if (previous && previous.identity !== identity)
|
|
72
|
+
throw new Error(`Duplicate job "${name}" at ${location}; first declared at ${previous.location}.`);
|
|
73
|
+
if (!previous)
|
|
74
|
+
jobs.set(name, { identity, create, location });
|
|
75
|
+
};
|
|
76
|
+
if (options.manifest) {
|
|
77
|
+
for (const [index, entry] of options.manifest.entries()) {
|
|
78
|
+
const location = `job manifest[${index}]`;
|
|
79
|
+
if (typeof entry === "function") {
|
|
80
|
+
validateClass(entry, location);
|
|
81
|
+
add(entry.jobName, entry, () => new entry, location);
|
|
82
|
+
} else {
|
|
83
|
+
if (!entry || typeof entry.name !== "string" || !entry.name.trim() || typeof entry.create !== "function")
|
|
84
|
+
throw new TypeError(`Invalid job factory at ${location}.`);
|
|
85
|
+
add(entry.name, entry.create, entry.create, location);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
for (const file of await infrastructureFiles(options.directory ?? join2(process.cwd(), "src", "jobs"), options.exclude)) {
|
|
90
|
+
const exports = await importInfrastructure(file);
|
|
91
|
+
const defaultValue = exports.default;
|
|
92
|
+
const legacy = defaultValue && typeof defaultValue === "object" && "default" in defaultValue ? defaultValue.default : defaultValue;
|
|
93
|
+
if (legacy !== undefined)
|
|
94
|
+
validateClass(legacy, `${file}:default`);
|
|
95
|
+
let found = false;
|
|
96
|
+
for (const [name, value] of Object.entries({
|
|
97
|
+
...exports,
|
|
98
|
+
...legacy === undefined ? {} : { default: legacy }
|
|
99
|
+
})) {
|
|
100
|
+
if (name !== "default" && !(typeof value === "function" && ("jobName" in value)))
|
|
101
|
+
continue;
|
|
102
|
+
validateClass(value, `${file}:${name}`);
|
|
103
|
+
found = true;
|
|
104
|
+
add(value.jobName, value, () => new value, `${file}:${name}`);
|
|
105
|
+
}
|
|
106
|
+
if (!found)
|
|
107
|
+
throw new TypeError(`No job exports at ${file}; exclude helper modules explicitly.`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (!current())
|
|
111
|
+
return [...jobs.keys()];
|
|
112
|
+
const registered = state().registered;
|
|
113
|
+
for (const [name, entry] of jobs) {
|
|
114
|
+
const previous = registered.get(name);
|
|
115
|
+
if (previous && previous.identity !== entry.identity)
|
|
116
|
+
throw new Error(`Duplicate discovered job "${name}" at ${entry.location}; first declared at ${previous.location}.`);
|
|
53
117
|
}
|
|
54
|
-
const
|
|
55
|
-
for (const
|
|
56
|
-
|
|
57
|
-
|
|
118
|
+
const existing = new Set(jobRegistry.names());
|
|
119
|
+
for (const [name, entry] of jobs) {
|
|
120
|
+
if (!existing.has(name)) {
|
|
121
|
+
jobRegistry.register(name, entry.create);
|
|
122
|
+
registered.set(name, entry);
|
|
123
|
+
}
|
|
58
124
|
}
|
|
59
|
-
|
|
60
|
-
|
|
125
|
+
return [...jobs.keys()];
|
|
126
|
+
}
|
|
127
|
+
function discoverJobs(options = {}) {
|
|
128
|
+
const cache = state().loads;
|
|
129
|
+
const key = options.manifest ?? JSON.stringify([
|
|
130
|
+
resolve(options.directory ?? join2(process.cwd(), "src", "jobs")),
|
|
131
|
+
[...options.exclude ?? []].sort()
|
|
132
|
+
]);
|
|
133
|
+
const existing = cache.get(key);
|
|
134
|
+
if (existing)
|
|
135
|
+
return existing;
|
|
136
|
+
const pending = load(options, () => state().loads === cache).catch((error) => {
|
|
137
|
+
if (cache.get(key) === pending)
|
|
138
|
+
cache.delete(key);
|
|
139
|
+
throw error;
|
|
140
|
+
});
|
|
141
|
+
cache.set(key, pending);
|
|
142
|
+
return pending;
|
|
61
143
|
}
|
|
62
144
|
function resetDiscoverJobsForTests() {
|
|
63
|
-
|
|
64
|
-
state.
|
|
145
|
+
state().loads = new Map;
|
|
146
|
+
state().registered = new Map;
|
|
65
147
|
}
|
|
66
148
|
export {
|
|
67
149
|
discoverJobs,
|
|
@@ -1,52 +1,100 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../../src/bootstrap/discoverListeners.ts
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
3
|
+
import { join as join2, resolve } from "path";
|
|
4
|
+
|
|
5
|
+
// ../../src/bootstrap/infrastructureDiscovery.ts
|
|
6
|
+
import { readdir } from "fs/promises";
|
|
7
|
+
import { join, relative } from "path";
|
|
8
|
+
import { pathToFileURL } from "url";
|
|
9
|
+
async function infrastructureFiles(directory, exclude = []) {
|
|
10
|
+
const excluded = new Set([
|
|
11
|
+
"index.ts",
|
|
12
|
+
"index.js",
|
|
13
|
+
"index.mts",
|
|
14
|
+
"index.mjs",
|
|
15
|
+
"register.ts",
|
|
16
|
+
"register.js",
|
|
17
|
+
"register.mts",
|
|
18
|
+
"register.mjs",
|
|
19
|
+
...exclude
|
|
20
|
+
]);
|
|
21
|
+
const files = [];
|
|
22
|
+
async function visit(path) {
|
|
23
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
24
|
+
entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
25
|
+
for (const entry of entries) {
|
|
26
|
+
const file = join(path, entry.name);
|
|
27
|
+
const key = relative(directory, file).replaceAll("\\", "/");
|
|
28
|
+
if (excluded.has(entry.name) || excluded.has(key))
|
|
29
|
+
continue;
|
|
30
|
+
if (entry.isDirectory())
|
|
31
|
+
await visit(file);
|
|
32
|
+
else if (entry.isFile() && /\.(ts|js|mts|mjs)$/.test(entry.name) && !/\.(d|test|spec)\.[cm]?[jt]s$/.test(entry.name))
|
|
33
|
+
files.push(file);
|
|
34
|
+
}
|
|
12
35
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return fromCwd;
|
|
36
|
+
try {
|
|
37
|
+
await visit(directory);
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (error.code !== "ENOENT")
|
|
40
|
+
throw error;
|
|
41
|
+
if (error.path !== directory)
|
|
42
|
+
throw error;
|
|
21
43
|
}
|
|
22
|
-
return
|
|
44
|
+
return files;
|
|
23
45
|
}
|
|
24
|
-
function
|
|
25
|
-
const listenersDirectory = resolveListenersDirectory();
|
|
26
|
-
let entries;
|
|
46
|
+
async function importInfrastructure(file) {
|
|
27
47
|
try {
|
|
28
|
-
|
|
48
|
+
return await import(pathToFileURL(file).href);
|
|
29
49
|
} catch (error) {
|
|
30
|
-
|
|
31
|
-
|
|
50
|
+
throw new Error(`Cannot load infrastructure module ${file}`, { cause: error });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ../../src/bootstrap/discoverListeners.ts
|
|
55
|
+
var KEY = Symbol.for("@getstrata/discoverListenersState");
|
|
56
|
+
function state() {
|
|
57
|
+
const globals = globalThis;
|
|
58
|
+
globals[KEY] ??= { loads: new Map };
|
|
59
|
+
return globals[KEY];
|
|
60
|
+
}
|
|
61
|
+
async function load(options) {
|
|
62
|
+
const listeners = new Set;
|
|
63
|
+
const add = (value, location) => {
|
|
64
|
+
if (typeof value !== "function")
|
|
65
|
+
throw new TypeError(`Invalid listener registrar at ${location}: expected a default function.`);
|
|
66
|
+
listeners.add(value);
|
|
67
|
+
};
|
|
68
|
+
if (options.manifest)
|
|
69
|
+
for (const [index, value] of options.manifest.entries())
|
|
70
|
+
add(value, `listener manifest[${index}]`);
|
|
71
|
+
else
|
|
72
|
+
for (const file of await infrastructureFiles(options.directory ?? join2(process.cwd(), "src", "listeners"), options.exclude)) {
|
|
73
|
+
const exports = await importInfrastructure(file);
|
|
74
|
+
const value = exports.default;
|
|
75
|
+
add(value && typeof value === "object" && "default" in value ? value.default : value, file);
|
|
32
76
|
}
|
|
77
|
+
return [...listeners];
|
|
78
|
+
}
|
|
79
|
+
function discoverListeners(options = {}) {
|
|
80
|
+
const cache = state().loads;
|
|
81
|
+
const key = options.manifest ?? JSON.stringify([
|
|
82
|
+
resolve(options.directory ?? join2(process.cwd(), "src", "listeners")),
|
|
83
|
+
[...options.exclude ?? []].sort()
|
|
84
|
+
]);
|
|
85
|
+
const existing = cache.get(key);
|
|
86
|
+
if (existing)
|
|
87
|
+
return existing;
|
|
88
|
+
const pending = load(options).catch((error) => {
|
|
89
|
+
if (cache.get(key) === pending)
|
|
90
|
+
cache.delete(key);
|
|
33
91
|
throw error;
|
|
34
|
-
}
|
|
35
|
-
const listeners = entries.map((fileName) => {
|
|
36
|
-
const filePath = join(listenersDirectory, fileName);
|
|
37
|
-
const loaded = requireListener(filePath);
|
|
38
|
-
return loaded.default;
|
|
39
92
|
});
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
function discoverListeners() {
|
|
43
|
-
const state = readDiscoverListenersState();
|
|
44
|
-
state.appListeners ??= loadDiscoveredListeners();
|
|
45
|
-
return state.appListeners;
|
|
93
|
+
cache.set(key, pending);
|
|
94
|
+
return pending;
|
|
46
95
|
}
|
|
47
96
|
function resetDiscoverListenersForTests() {
|
|
48
|
-
|
|
49
|
-
state.appListeners = undefined;
|
|
97
|
+
state().loads = new Map;
|
|
50
98
|
}
|
|
51
99
|
export {
|
|
52
100
|
discoverListeners,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// ../../src/bootstrap/discoverModels.ts
|
|
3
|
+
import { readdir } from "fs/promises";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { pathToFileURL } from "url";
|
|
6
|
+
import { bootModels, Model } from "@getstrata/core/database/model";
|
|
7
|
+
async function discoverModels(directory = join(process.cwd(), "src", "models"), options = {}) {
|
|
8
|
+
const excluded = new Set([
|
|
9
|
+
"register.ts",
|
|
10
|
+
"register.js",
|
|
11
|
+
"index.ts",
|
|
12
|
+
"index.js",
|
|
13
|
+
...options.exclude ?? []
|
|
14
|
+
]);
|
|
15
|
+
const models = new Set;
|
|
16
|
+
async function visit(path) {
|
|
17
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
18
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
const file = join(path, entry.name);
|
|
21
|
+
if (entry.isDirectory())
|
|
22
|
+
await visit(file);
|
|
23
|
+
else if (!excluded.has(entry.name) && entry.isFile() && /\.(ts|js|mts|mjs)$/.test(entry.name) && !/\.d\.[cm]?ts$/.test(entry.name)) {
|
|
24
|
+
const exports = await import(pathToFileURL(file).href);
|
|
25
|
+
for (const value of Object.values(exports)) {
|
|
26
|
+
if (typeof value === "function" && value.prototype instanceof Model)
|
|
27
|
+
models.add(value);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
await visit(directory);
|
|
33
|
+
const result = [...models];
|
|
34
|
+
bootModels(result);
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
export {
|
|
38
|
+
discoverModels
|
|
39
|
+
};
|
|
@@ -1,76 +1,183 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../../src/bootstrap/discoverModules.ts
|
|
3
|
-
import {
|
|
4
|
-
import { join } from "path";
|
|
3
|
+
import { readdir as readdir2 } from "fs/promises";
|
|
4
|
+
import { join as join2, resolve } from "path";
|
|
5
|
+
|
|
6
|
+
// ../../src/bootstrap/infrastructureDiscovery.ts
|
|
7
|
+
import { readdir } from "fs/promises";
|
|
8
|
+
import { join, relative } from "path";
|
|
5
9
|
import { pathToFileURL } from "url";
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
async function infrastructureFiles(directory, exclude = []) {
|
|
11
|
+
const excluded = new Set([
|
|
12
|
+
"index.ts",
|
|
13
|
+
"index.js",
|
|
14
|
+
"index.mts",
|
|
15
|
+
"index.mjs",
|
|
16
|
+
"register.ts",
|
|
17
|
+
"register.js",
|
|
18
|
+
"register.mts",
|
|
19
|
+
"register.mjs",
|
|
20
|
+
...exclude
|
|
21
|
+
]);
|
|
22
|
+
const files = [];
|
|
23
|
+
async function visit(path) {
|
|
24
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
25
|
+
entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
26
|
+
for (const entry of entries) {
|
|
27
|
+
const file = join(path, entry.name);
|
|
28
|
+
const key = relative(directory, file).replaceAll("\\", "/");
|
|
29
|
+
if (excluded.has(entry.name) || excluded.has(key))
|
|
30
|
+
continue;
|
|
31
|
+
if (entry.isDirectory())
|
|
32
|
+
await visit(file);
|
|
33
|
+
else if (entry.isFile() && /\.(ts|js|mts|mjs)$/.test(entry.name) && !/\.(d|test|spec)\.[cm]?[jt]s$/.test(entry.name))
|
|
34
|
+
files.push(file);
|
|
35
|
+
}
|
|
11
36
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
37
|
+
try {
|
|
38
|
+
await visit(directory);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if (error.code !== "ENOENT")
|
|
41
|
+
throw error;
|
|
42
|
+
if (error.path !== directory)
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
return files;
|
|
15
46
|
}
|
|
16
|
-
function
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
47
|
+
async function importInfrastructure(file) {
|
|
48
|
+
try {
|
|
49
|
+
return await import(pathToFileURL(file).href);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
throw new Error(`Cannot load infrastructure module ${file}`, { cause: error });
|
|
21
52
|
}
|
|
22
|
-
state.configuredModulesDir = modulesDir;
|
|
23
53
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
54
|
+
|
|
55
|
+
// ../../src/bootstrap/discoverModules.ts
|
|
56
|
+
var KEY = Symbol.for("@getstrata/discoverModulesState");
|
|
57
|
+
function state() {
|
|
58
|
+
const globals = globalThis;
|
|
59
|
+
globals[KEY] ??= { appModules: [] };
|
|
60
|
+
return globals[KEY];
|
|
61
|
+
}
|
|
62
|
+
function configureModulesDirectory(modulesDir) {
|
|
63
|
+
const current = state();
|
|
64
|
+
const directory = resolve(modulesDir);
|
|
65
|
+
if (current.configuredModulesDir !== directory || current.configuredManifest) {
|
|
66
|
+
current.appModules.length = 0;
|
|
67
|
+
current.modulesReady = undefined;
|
|
68
|
+
current.source = undefined;
|
|
28
69
|
}
|
|
29
|
-
|
|
30
|
-
|
|
70
|
+
current.configuredManifest = undefined;
|
|
71
|
+
current.configuredModulesDir = directory;
|
|
72
|
+
}
|
|
73
|
+
function configureModulesManifest(manifest) {
|
|
74
|
+
const current = state();
|
|
75
|
+
if (current.configuredManifest !== manifest) {
|
|
76
|
+
current.appModules.length = 0;
|
|
77
|
+
current.modulesReady = undefined;
|
|
78
|
+
current.source = undefined;
|
|
31
79
|
}
|
|
32
|
-
|
|
80
|
+
current.configuredManifest = manifest;
|
|
33
81
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
82
|
+
function validateModule(value, location) {
|
|
83
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
84
|
+
throw new TypeError(`Invalid application module at ${location}.`);
|
|
85
|
+
const module = value;
|
|
86
|
+
if (typeof module.name !== "string" || !module.name.trim())
|
|
87
|
+
throw new TypeError(`Invalid module name at ${location}.`);
|
|
88
|
+
if (module.order !== undefined && (typeof module.order !== "number" || !Number.isFinite(module.order)))
|
|
89
|
+
throw new TypeError(`Invalid module order at ${location}.`);
|
|
90
|
+
for (const hook of ["routes", "webRoutes"])
|
|
91
|
+
if (module[hook] !== undefined && typeof module[hook] !== "function")
|
|
92
|
+
throw new TypeError(`Invalid ${hook} at ${location}.`);
|
|
93
|
+
if (module.providers !== undefined) {
|
|
94
|
+
if (!Array.isArray(module.providers))
|
|
95
|
+
throw new TypeError(`Invalid providers at ${location}.`);
|
|
96
|
+
for (const provider of module.providers) {
|
|
97
|
+
if (!provider || typeof provider.name !== "string" || !provider.name.trim() || provider.register !== undefined && typeof provider.register !== "function" || provider.boot !== undefined && typeof provider.boot !== "function")
|
|
98
|
+
throw new TypeError(`Invalid provider at ${location}.`);
|
|
42
99
|
}
|
|
43
|
-
throw error;
|
|
44
100
|
}
|
|
45
|
-
const modules = await Promise.all(moduleNames.map(async (moduleName) => {
|
|
46
|
-
const moduleUrl = pathToFileURL(join(modulesDirectory, moduleName, "index.ts")).href;
|
|
47
|
-
const loaded = await import(moduleUrl);
|
|
48
|
-
return loaded.default;
|
|
49
|
-
}));
|
|
50
|
-
return modules.filter((module) => module?.name !== undefined).sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
|
|
51
101
|
}
|
|
52
|
-
async function
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
102
|
+
async function load(options) {
|
|
103
|
+
const manifest = options.manifest ?? (options.modulesDir === undefined ? state().configuredManifest : undefined);
|
|
104
|
+
const entries = [];
|
|
105
|
+
if (manifest)
|
|
106
|
+
for (const [index, value] of manifest.entries())
|
|
107
|
+
entries.push({ value, location: `module manifest[${index}]` });
|
|
108
|
+
else {
|
|
109
|
+
const directory = options.modulesDir ?? state().configuredModulesDir;
|
|
110
|
+
if (!directory)
|
|
111
|
+
throw new Error("configureModulesDirectory() or configureModulesManifest() must be called before discovering modules.");
|
|
112
|
+
let folders;
|
|
113
|
+
try {
|
|
114
|
+
folders = await readdir2(directory, { withFileTypes: true });
|
|
115
|
+
} catch (error) {
|
|
116
|
+
if (error.code === "ENOENT")
|
|
117
|
+
return [];
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
const excluded = new Set(options.exclude ?? []);
|
|
121
|
+
for (const folder of folders.filter((entry) => entry.isDirectory() && !excluded.has(entry.name)).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
|
|
122
|
+
const path = join2(directory, folder.name);
|
|
123
|
+
const files = await readdir2(path);
|
|
124
|
+
const candidates = files.filter((file) => /^index\.(ts|js|mts|mjs)$/.test(file));
|
|
125
|
+
if (candidates.length !== 1)
|
|
126
|
+
throw new Error(`Expected one module entrypoint at ${path}; found ${candidates.length}.`);
|
|
127
|
+
const file = join2(path, candidates[0]);
|
|
128
|
+
const exports = await importInfrastructure(file);
|
|
129
|
+
entries.push({ value: exports.default, location: file });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const names = new Map;
|
|
133
|
+
const modules = [];
|
|
134
|
+
for (const { value, location } of entries) {
|
|
135
|
+
validateModule(value, location);
|
|
136
|
+
const previous = names.get(value.name);
|
|
137
|
+
if (previous)
|
|
138
|
+
throw new Error(`Duplicate module "${value.name}" at ${location}; first declared at ${previous}.`);
|
|
139
|
+
names.set(value.name, location);
|
|
140
|
+
modules.push(value);
|
|
56
141
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
142
|
+
return modules.sort((left, right) => (left.order ?? 100) - (right.order ?? 100));
|
|
143
|
+
}
|
|
144
|
+
function ensureModulesLoaded(options = {}) {
|
|
145
|
+
const current = state();
|
|
146
|
+
const key = options.manifest ?? (options.modulesDir === undefined ? current.configuredManifest : undefined) ?? JSON.stringify([
|
|
147
|
+
options.modulesDir ? resolve(options.modulesDir) : current.configuredModulesDir,
|
|
148
|
+
[...options.exclude ?? []].sort()
|
|
149
|
+
]);
|
|
150
|
+
if (current.modulesReady && current.source === key)
|
|
151
|
+
return current.modulesReady;
|
|
152
|
+
current.appModules.length = 0;
|
|
153
|
+
current.source = key;
|
|
154
|
+
const pending = load(options).then((modules) => {
|
|
155
|
+
if (current.modulesReady !== pending)
|
|
156
|
+
return modules;
|
|
157
|
+
current.appModules.splice(0, current.appModules.length, ...modules);
|
|
158
|
+
return current.appModules;
|
|
159
|
+
}).catch((error) => {
|
|
160
|
+
if (current.modulesReady === pending)
|
|
161
|
+
current.modulesReady = undefined;
|
|
162
|
+
throw error;
|
|
60
163
|
});
|
|
61
|
-
|
|
164
|
+
current.modulesReady = pending;
|
|
165
|
+
return pending;
|
|
62
166
|
}
|
|
63
167
|
function discoverModules() {
|
|
64
|
-
return
|
|
168
|
+
return state().appModules;
|
|
65
169
|
}
|
|
66
170
|
function resetDiscoverModulesForTests() {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
171
|
+
const current = state();
|
|
172
|
+
current.configuredModulesDir = undefined;
|
|
173
|
+
current.configuredManifest = undefined;
|
|
174
|
+
current.appModules.length = 0;
|
|
175
|
+
current.modulesReady = undefined;
|
|
176
|
+
current.source = undefined;
|
|
71
177
|
}
|
|
72
178
|
export {
|
|
73
179
|
configureModulesDirectory,
|
|
180
|
+
configureModulesManifest,
|
|
74
181
|
discoverModules,
|
|
75
182
|
ensureModulesLoaded,
|
|
76
183
|
resetDiscoverModulesForTests
|
package/dist/entries/health.js
CHANGED
|
@@ -14,8 +14,11 @@ import {
|
|
|
14
14
|
CORE_CACHE_TOKEN,
|
|
15
15
|
CORE_CONFIG_TOKEN,
|
|
16
16
|
CORE_EVENT_BUS_TOKEN,
|
|
17
|
+
CORE_HTTP_CLEANUP_TOKEN,
|
|
17
18
|
CORE_POLICY_GATE_TOKEN,
|
|
18
19
|
CORE_QUEUE_TOKEN,
|
|
20
|
+
CORE_TENANT_RESOLVER_TOKEN,
|
|
21
|
+
CORE_THROTTLE_QUOTA_POLICY_TOKEN,
|
|
19
22
|
CORE_TOKEN_SERVICE_TOKEN
|
|
20
23
|
} from "@getstrata/core/contracts/serviceTokens";
|
|
21
24
|
var APP_PORT_CONFIG_KEY = "app.port";
|