@warlock.js/core 5.24.0 → 5.26.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 +64 -1
- package/bin/warlock.js +0 -0
- package/esm/application/application.d.mts +21 -0
- package/esm/application/application.d.mts.map +1 -1
- package/esm/application/application.mjs +29 -1
- package/esm/application/application.mjs.map +1 -1
- package/esm/application/boot-signal.d.mts +2 -1
- package/esm/application/boot-signal.d.mts.map +1 -1
- package/esm/application/boot-signal.mjs.map +1 -1
- package/esm/application/index.d.mts +1 -0
- package/esm/application/index.mjs +1 -0
- package/esm/application/roles.d.mts +72 -0
- package/esm/application/roles.d.mts.map +1 -0
- package/esm/application/roles.mjs +124 -0
- package/esm/application/roles.mjs.map +1 -0
- package/esm/cli/commands/add.command.mjs.map +1 -1
- package/esm/cli/commands/create-database.command.mjs.map +1 -1
- package/esm/cli/commands/dev-server.command.mjs.map +1 -1
- package/esm/cli/commands/doctor/boot-for-diagnostics.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/index.mjs +2 -0
- package/esm/cli/commands/doctor/checks/index.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/locales-ts-deprecated.check.mjs +170 -0
- package/esm/cli/commands/doctor/checks/locales-ts-deprecated.check.mjs.map +1 -0
- package/esm/cli/commands/doctor/doctor-command.action.mjs +7 -2
- package/esm/cli/commands/doctor/doctor-command.action.mjs.map +1 -1
- package/esm/cli/commands/doctor.command.mjs +5 -0
- package/esm/cli/commands/doctor.command.mjs.map +1 -1
- package/esm/cli/commands/drop-tables.command.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/module.generator.mjs +1 -12
- package/esm/cli/commands/generate/generators/module.generator.mjs.map +1 -1
- package/esm/cli/commands/generate/utils/path-resolver.mjs.map +1 -1
- package/esm/cli/commands/migrate.command.mjs.map +1 -1
- package/esm/cli/commands/routes.command.mjs.map +1 -1
- package/esm/cli/commands/seed.command.mjs.map +1 -1
- package/esm/cli/commands/start-production.command.mjs +54 -5
- package/esm/cli/commands/start-production.command.mjs.map +1 -1
- package/esm/cli/commands/storage-put.action.mjs.map +1 -1
- package/esm/cli/commands/storage-put.command.mjs.map +1 -1
- package/esm/cli/commands/typings-generator.command.mjs +20 -1
- package/esm/cli/commands/typings-generator.command.mjs.map +1 -1
- package/esm/cli/commands/update.command.mjs.map +1 -1
- package/esm/cli/commands-loader.mjs.map +1 -1
- package/esm/config/config-loader.d.mts.map +1 -1
- package/esm/config/config-loader.mjs +3 -1
- package/esm/config/config-loader.mjs.map +1 -1
- package/esm/connectors/connectors-manager.mjs.map +1 -1
- package/esm/connectors/database-connector.mjs.map +1 -1
- package/esm/connectors/herald-connector.mjs.map +1 -1
- package/esm/connectors/http-connector.d.mts.map +1 -1
- package/esm/connectors/http-connector.mjs +7 -0
- package/esm/connectors/http-connector.mjs.map +1 -1
- package/esm/connectors/logger-connector.mjs.map +1 -1
- package/esm/connectors/mail-connector.mjs.map +1 -1
- package/esm/connectors/types.d.mts +8 -0
- package/esm/connectors/types.d.mts.map +1 -1
- package/esm/connectors/types.mjs.map +1 -1
- package/esm/database/create-database-action.mjs.map +1 -1
- package/esm/database/drop-tables-action.mjs.map +1 -1
- package/esm/database/migrate-action.mjs +4 -3
- package/esm/database/migrate-action.mjs.map +1 -1
- package/esm/database/models/database-log/database-log.mjs.map +1 -1
- package/esm/database/seed-command-action.mjs +2 -1
- package/esm/database/seed-command-action.mjs.map +1 -1
- package/esm/dev-server/dependency-graph.mjs.map +1 -1
- package/esm/dev-server/development-server.mjs +2 -0
- package/esm/dev-server/development-server.mjs.map +1 -1
- package/esm/dev-server/files-orchestrator.mjs.map +1 -1
- package/esm/dev-server/layer-executor.mjs.map +1 -1
- package/esm/dev-server/legacy-locales-warning.mjs +16 -0
- package/esm/dev-server/legacy-locales-warning.mjs.map +1 -0
- package/esm/dev-server/manifest-manager.mjs.map +1 -1
- package/esm/dev-server/module-loader.mjs +26 -5
- package/esm/dev-server/module-loader.mjs.map +1 -1
- package/esm/dev-server/register-installed-devtools.mjs +47 -0
- package/esm/dev-server/register-installed-devtools.mjs.map +1 -0
- package/esm/dev-server/run-connector-typings.mjs +28 -0
- package/esm/dev-server/run-connector-typings.mjs.map +1 -0
- package/esm/dev-server/special-file-patterns.mjs +5 -3
- package/esm/dev-server/special-file-patterns.mjs.map +1 -1
- package/esm/dev-server/special-files-collector.d.mts +2 -2
- package/esm/dev-server/special-files-collector.d.mts.map +1 -1
- package/esm/dev-server/special-files-collector.mjs +7 -4
- package/esm/dev-server/special-files-collector.mjs.map +1 -1
- package/esm/dev-server/translation-keys-sources.mjs +7 -3
- package/esm/dev-server/translation-keys-sources.mjs.map +1 -1
- package/esm/dev-server/type-generator.mjs +27 -4
- package/esm/dev-server/type-generator.mjs.map +1 -1
- package/esm/encryption/password.mjs.map +1 -1
- package/esm/generations/add-command.action.mjs +3 -1
- package/esm/generations/add-command.action.mjs.map +1 -1
- package/esm/generations/features/access.feature.mjs +38 -18
- package/esm/generations/features/access.feature.mjs.map +1 -1
- package/esm/generations/features/ai-anthropic.feature.mjs.map +1 -1
- package/esm/generations/features/ai-bedrock.feature.mjs.map +1 -1
- package/esm/generations/features/ai-google.feature.mjs.map +1 -1
- package/esm/generations/features/ai-ollama.feature.mjs.map +1 -1
- package/esm/generations/features/ai-openai.feature.mjs.map +1 -1
- package/esm/generations/features/ai-panoptic.feature.mjs.map +1 -1
- package/esm/generations/features/ai-tools.feature.mjs.map +1 -1
- package/esm/generations/features/ai-workspace.feature.mjs.map +1 -1
- package/esm/generations/features/ai.feature.mjs.map +1 -1
- package/esm/generations/features/bull-board.feature.mjs +2 -2
- package/esm/generations/features/bull-board.feature.mjs.map +1 -1
- package/esm/generations/features/devtools.feature.mjs +23 -0
- package/esm/generations/features/devtools.feature.mjs.map +1 -0
- package/esm/generations/features/herald.feature.mjs.map +1 -1
- package/esm/generations/features/image.feature.mjs.map +1 -1
- package/esm/generations/features/index.mjs +2 -0
- package/esm/generations/features/index.mjs.map +1 -1
- package/esm/generations/features/mail.feature.mjs.map +1 -1
- package/esm/generations/features/mongodb.feature.mjs.map +1 -1
- package/esm/generations/features/notifications.feature.mjs.map +1 -1
- package/esm/generations/features/postgres.feature.mjs.map +1 -1
- package/esm/generations/features/react-email.feature.mjs.map +1 -1
- package/esm/generations/features/react.feature.mjs.map +1 -1
- package/esm/generations/features/redis.feature.mjs.map +1 -1
- package/esm/generations/features/s3.feature.mjs.map +1 -1
- package/esm/generations/features/scheduler.feature.mjs.map +1 -1
- package/esm/generations/features/ses.feature.mjs.map +1 -1
- package/esm/generations/features/socket.feature.mjs.map +1 -1
- package/esm/generations/features/test.feature.mjs +6 -2
- package/esm/generations/features/test.feature.mjs.map +1 -1
- package/esm/generations/features/types.mjs.map +1 -1
- package/esm/generations/features/web.feature.mjs.map +1 -1
- package/esm/http/build-rate-limit-options.d.mts +8 -2
- package/esm/http/build-rate-limit-options.d.mts.map +1 -1
- package/esm/http/build-rate-limit-options.mjs +1 -0
- package/esm/http/build-rate-limit-options.mjs.map +1 -1
- package/esm/http/database/RequestLog.mjs.map +1 -1
- package/esm/http/errors/errors.d.mts +1 -17
- package/esm/http/errors/errors.d.mts.map +1 -1
- package/esm/http/errors/errors.mjs +1 -20
- package/esm/http/errors/errors.mjs.map +1 -1
- package/esm/http/index.d.mts +4 -3
- package/esm/http/index.mjs +3 -2
- package/esm/http/middleware/inject-request-context.d.mts +1 -1
- package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
- package/esm/http/middleware/inject-request-context.mjs +9 -1
- package/esm/http/middleware/inject-request-context.mjs.map +1 -1
- package/esm/http/middleware/rate-limit.middleware.d.mts.map +1 -1
- package/esm/http/middleware/rate-limit.middleware.mjs +5 -1
- package/esm/http/middleware/rate-limit.middleware.mjs.map +1 -1
- package/esm/http/request.d.mts +7 -24
- package/esm/http/request.d.mts.map +1 -1
- package/esm/http/request.mjs +1 -22
- package/esm/http/request.mjs.map +1 -1
- package/esm/http/response.d.mts +5 -5
- package/esm/http/response.d.mts.map +1 -1
- package/esm/http/response.mjs +4 -2
- package/esm/http/response.mjs.map +1 -1
- package/esm/http/server-rebuilt.d.mts +20 -0
- package/esm/http/server-rebuilt.d.mts.map +1 -0
- package/esm/http/server-rebuilt.mjs +21 -0
- package/esm/http/server-rebuilt.mjs.map +1 -0
- package/esm/http/tracing/index.mjs +1 -1
- package/esm/http/tracing/tracing-dispatcher.d.mts +13 -6
- package/esm/http/tracing/tracing-dispatcher.d.mts.map +1 -1
- package/esm/http/tracing/tracing-dispatcher.mjs +46 -14
- package/esm/http/tracing/tracing-dispatcher.mjs.map +1 -1
- package/esm/http/tracing/tracing.type.d.mts +2 -1
- package/esm/http/tracing/tracing.type.d.mts.map +1 -1
- package/esm/http/types.d.mts +9 -0
- package/esm/http/types.d.mts.map +1 -1
- package/esm/image/image.mjs.map +1 -1
- package/esm/index.d.mts +8 -4
- package/esm/index.mjs +8 -3
- package/esm/loader/app-module-importer.d.mts +17 -0
- package/esm/loader/app-module-importer.d.mts.map +1 -0
- package/esm/loader/app-module-importer.mjs +20 -0
- package/esm/loader/app-module-importer.mjs.map +1 -0
- package/esm/localization/index.mjs +4 -0
- package/esm/localization/parse-locale-dictionary.d.mts +23 -0
- package/esm/localization/parse-locale-dictionary.d.mts.map +1 -0
- package/esm/localization/parse-locale-dictionary.mjs +215 -0
- package/esm/localization/parse-locale-dictionary.mjs.map +1 -0
- package/esm/localization/register-locale-dictionary.d.mts +12 -0
- package/esm/localization/register-locale-dictionary.d.mts.map +1 -0
- package/esm/localization/register-locale-dictionary.mjs +58 -0
- package/esm/localization/register-locale-dictionary.mjs.map +1 -0
- package/esm/logger/logger.mjs.map +1 -1
- package/esm/mail/events.d.mts +7 -0
- package/esm/mail/events.d.mts.map +1 -1
- package/esm/mail/events.mjs +7 -0
- package/esm/mail/events.mjs.map +1 -1
- package/esm/mail/send-mail.d.mts.map +1 -1
- package/esm/mail/send-mail.mjs +23 -2
- package/esm/mail/send-mail.mjs.map +1 -1
- package/esm/mail/types.d.mts +5 -1
- package/esm/mail/types.d.mts.map +1 -1
- package/esm/mail/types.mjs.map +1 -1
- package/esm/manifest/manifest-manager.mjs.map +1 -1
- package/esm/production/build-contributions.mjs +19 -3
- package/esm/production/build-contributions.mjs.map +1 -1
- package/esm/production/production-builder.mjs +39 -10
- package/esm/production/production-builder.mjs.map +1 -1
- package/esm/production/production-supervisor.mjs +6 -0
- package/esm/production/production-supervisor.mjs.map +1 -1
- package/esm/production/resolve-build-config.d.mts +1 -1
- package/esm/production/resolve-build-config.mjs +15 -0
- package/esm/production/resolve-build-config.mjs.map +1 -1
- package/esm/production/route-registration-child.mjs +14 -2
- package/esm/production/route-registration-child.mjs.map +1 -1
- package/esm/production/route-registration-snapshot.mjs +9 -2
- package/esm/production/route-registration-snapshot.mjs.map +1 -1
- package/esm/resource/resource-field-builder.mjs +1 -1
- package/esm/resource/resource-field-builder.mjs.map +1 -1
- package/esm/resource/resource.d.mts +5 -0
- package/esm/resource/resource.d.mts.map +1 -1
- package/esm/resource/resource.mjs +17 -6
- package/esm/resource/resource.mjs.map +1 -1
- package/esm/router/router.d.mts.map +1 -1
- package/esm/router/router.mjs +1 -0
- package/esm/router/router.mjs.map +1 -1
- package/esm/router/types.d.mts +10 -1
- package/esm/router/types.d.mts.map +1 -1
- package/esm/tests/index.d.mts +3 -2
- package/esm/tests/index.mjs +3 -2
- package/esm/tests/start-http-development-server.d.mts +10 -0
- package/esm/tests/start-http-development-server.d.mts.map +1 -1
- package/esm/tests/start-http-development-server.mjs +16 -2
- package/esm/tests/start-http-development-server.mjs.map +1 -1
- package/esm/tests/vitest-setup.d.mts +16 -1
- package/esm/tests/vitest-setup.d.mts.map +1 -1
- package/esm/tests/vitest-setup.mjs +20 -2
- package/esm/tests/vitest-setup.mjs.map +1 -1
- package/esm/use-cases/use-case-events.mjs.map +1 -1
- package/esm/utils/is-test-file.mjs +15 -0
- package/esm/utils/is-test-file.mjs.map +1 -0
- package/esm/utils/normalized-path.d.mts +3 -1
- package/esm/utils/normalized-path.d.mts.map +1 -1
- package/esm/utils/normalized-path.mjs +4 -2
- package/esm/utils/normalized-path.mjs.map +1 -1
- package/esm/utils/paths.mjs.map +1 -1
- package/esm/validation/plugins/database-plugin.mjs.map +1 -1
- package/esm/validation/validateAll.mjs +57 -4
- package/esm/validation/validateAll.mjs.map +1 -1
- package/esm/warlock-config/default-configurations.mjs +1 -0
- package/esm/warlock-config/default-configurations.mjs.map +1 -1
- package/esm/warlock-config/types.d.mts +7 -0
- package/esm/warlock-config/types.d.mts.map +1 -1
- package/esm/warlock-config/warlock-config.manager.mjs.map +1 -1
- package/llms-full.txt +382 -214
- package/llms.txt +1 -1
- package/package.json +23 -19
- package/skills/README.md +1 -1
- package/skills/configure-app/SKILL.md +2 -0
- package/skills/create-controller/SKILL.md +4 -0
- package/skills/define-resource/SKILL.md +4 -0
- package/skills/request-tracing/SKILL.md +38 -15
- package/skills/run-app/SKILL.md +71 -34
- package/skills/send-mail/SKILL.md +55 -25
- package/skills/test-http/SKILL.md +28 -9
- package/skills/test-service/SKILL.md +61 -40
- package/skills/use-localization/SKILL.md +39 -26
- package/skills/warlock-conventions/SKILL.md +26 -18
- package/skills/write-cli-command/SKILL.md +50 -45
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
//#region ../core/src/localization/parse-locale-dictionary.ts
|
|
2
|
+
const SEGMENT = /^[A-Za-z_][A-Za-z0-9_-]*$/;
|
|
3
|
+
const LOCALE_CODE = /^[A-Za-z][A-Za-z0-9]*(?:-[A-Za-z0-9]+)*$/;
|
|
4
|
+
const FORBIDDEN_SEGMENTS = new Set([
|
|
5
|
+
"__proto__",
|
|
6
|
+
"prototype",
|
|
7
|
+
"constructor"
|
|
8
|
+
]);
|
|
9
|
+
/** A source error that always names both the locale file and the offending JSON key. */
|
|
10
|
+
var LocaleDictionaryError = class extends Error {
|
|
11
|
+
constructor(sourceFile, key, detail) {
|
|
12
|
+
super(`Cannot parse route locales in "${sourceFile}" at "${key}": ${detail}.`);
|
|
13
|
+
this.sourceFile = sourceFile;
|
|
14
|
+
this.key = key;
|
|
15
|
+
this.name = "LocaleDictionaryError";
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
function fail(sourceFile, key, detail) {
|
|
19
|
+
throw new LocaleDictionaryError(sourceFile, key, detail);
|
|
20
|
+
}
|
|
21
|
+
/** A deliberately small JSON parser: unlike JSON.parse, it preserves duplicate property names. */
|
|
22
|
+
var StrictJsonParser = class {
|
|
23
|
+
constructor(sourceFile, source) {
|
|
24
|
+
this.sourceFile = sourceFile;
|
|
25
|
+
this.source = source;
|
|
26
|
+
this.offset = 0;
|
|
27
|
+
}
|
|
28
|
+
parse() {
|
|
29
|
+
if (this.source.charCodeAt(0) === 65279) this.offset = 1;
|
|
30
|
+
const value = this.value("$");
|
|
31
|
+
this.whitespace();
|
|
32
|
+
if (this.offset !== this.source.length) this.invalid("$", "unexpected trailing JSON");
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
value(key) {
|
|
36
|
+
this.whitespace();
|
|
37
|
+
const character = this.source[this.offset];
|
|
38
|
+
if (character === "{") return this.object(key);
|
|
39
|
+
if (character === "[") return this.array(key);
|
|
40
|
+
if (character === "\"") return this.string(key);
|
|
41
|
+
if (this.source.startsWith("true", this.offset)) return this.literal("true", true);
|
|
42
|
+
if (this.source.startsWith("false", this.offset)) return this.literal("false", false);
|
|
43
|
+
if (this.source.startsWith("null", this.offset)) return this.literal("null", null);
|
|
44
|
+
if (character === "-" || character !== void 0 && /[0-9]/.test(character)) return this.number(key);
|
|
45
|
+
this.invalid(key, "invalid JSON value");
|
|
46
|
+
}
|
|
47
|
+
object(key) {
|
|
48
|
+
this.offset++;
|
|
49
|
+
this.whitespace();
|
|
50
|
+
const object = /* @__PURE__ */ new Map();
|
|
51
|
+
if (this.source[this.offset] === "}") {
|
|
52
|
+
this.offset++;
|
|
53
|
+
return object;
|
|
54
|
+
}
|
|
55
|
+
while (true) {
|
|
56
|
+
this.whitespace();
|
|
57
|
+
if (this.source[this.offset] !== "\"") this.invalid(key, "object keys must be JSON strings");
|
|
58
|
+
const property = this.string(key);
|
|
59
|
+
const propertyKey = `${key}.${property}`;
|
|
60
|
+
if (object.has(property)) this.invalid(propertyKey, "duplicate JSON property");
|
|
61
|
+
this.whitespace();
|
|
62
|
+
if (this.source[this.offset] !== ":") this.invalid(propertyKey, "expected ':' after object key");
|
|
63
|
+
this.offset++;
|
|
64
|
+
object.set(property, this.value(propertyKey));
|
|
65
|
+
this.whitespace();
|
|
66
|
+
const separator = this.source[this.offset];
|
|
67
|
+
if (separator === "}") {
|
|
68
|
+
this.offset++;
|
|
69
|
+
return object;
|
|
70
|
+
}
|
|
71
|
+
if (separator !== ",") this.invalid(key, "expected ',' or '}' in object");
|
|
72
|
+
this.offset++;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
array(key) {
|
|
76
|
+
this.offset++;
|
|
77
|
+
this.whitespace();
|
|
78
|
+
const values = [];
|
|
79
|
+
if (this.source[this.offset] === "]") {
|
|
80
|
+
this.offset++;
|
|
81
|
+
return values;
|
|
82
|
+
}
|
|
83
|
+
while (true) {
|
|
84
|
+
values.push(this.value(key));
|
|
85
|
+
this.whitespace();
|
|
86
|
+
const separator = this.source[this.offset];
|
|
87
|
+
if (separator === "]") {
|
|
88
|
+
this.offset++;
|
|
89
|
+
return values;
|
|
90
|
+
}
|
|
91
|
+
if (separator !== ",") this.invalid(key, "expected ',' or ']' in array");
|
|
92
|
+
this.offset++;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
string(key) {
|
|
96
|
+
const start = this.offset++;
|
|
97
|
+
let escaped = false;
|
|
98
|
+
while (this.offset < this.source.length) {
|
|
99
|
+
const character = this.source[this.offset++];
|
|
100
|
+
if (escaped) {
|
|
101
|
+
escaped = false;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (character === "\\") {
|
|
105
|
+
escaped = true;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (character === "\"") try {
|
|
109
|
+
return JSON.parse(this.source.slice(start, this.offset));
|
|
110
|
+
} catch {
|
|
111
|
+
this.invalid(key, "invalid JSON string");
|
|
112
|
+
}
|
|
113
|
+
if (character !== void 0 && character.charCodeAt(0) < 32) this.invalid(key, "unescaped control character in JSON string");
|
|
114
|
+
}
|
|
115
|
+
this.invalid(key, "unterminated JSON string");
|
|
116
|
+
}
|
|
117
|
+
number(key) {
|
|
118
|
+
const match = /-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/y;
|
|
119
|
+
match.lastIndex = this.offset;
|
|
120
|
+
const token = match.exec(this.source)?.[0];
|
|
121
|
+
if (token === void 0) this.invalid(key, "invalid JSON number");
|
|
122
|
+
this.offset += token.length;
|
|
123
|
+
return Number(token);
|
|
124
|
+
}
|
|
125
|
+
literal(written, value) {
|
|
126
|
+
this.offset += written.length;
|
|
127
|
+
return value;
|
|
128
|
+
}
|
|
129
|
+
whitespace() {
|
|
130
|
+
while (this.offset < this.source.length) {
|
|
131
|
+
const character = this.source[this.offset];
|
|
132
|
+
if (character === void 0 || !" \n\r".includes(character)) return;
|
|
133
|
+
this.offset++;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
invalid(key, detail) {
|
|
137
|
+
fail(this.sourceFile, key, detail);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
function assertNamespaceSegment(sourceFile, segment, key) {
|
|
141
|
+
if (!SEGMENT.test(segment)) fail(sourceFile, key, "segments must match [A-Za-z_][A-Za-z0-9_-]*");
|
|
142
|
+
if (FORBIDDEN_SEGMENTS.has(segment)) fail(sourceFile, key, "segments cannot be reserved");
|
|
143
|
+
}
|
|
144
|
+
function assertSegment(sourceFile, segment, key) {
|
|
145
|
+
if (segment.includes(".")) fail(sourceFile, key, "JSON keys cannot contain '.'");
|
|
146
|
+
assertNamespaceSegment(sourceFile, segment, key);
|
|
147
|
+
}
|
|
148
|
+
function explicitGroup(sourceFile, value) {
|
|
149
|
+
if (value === "") fail(sourceFile, "$group", "'$group' cannot be empty");
|
|
150
|
+
for (const segment of value.split(".")) {
|
|
151
|
+
if (segment === "") fail(sourceFile, "$group", "'$group' cannot contain empty segments");
|
|
152
|
+
assertNamespaceSegment(sourceFile, segment, "$group");
|
|
153
|
+
}
|
|
154
|
+
return value;
|
|
155
|
+
}
|
|
156
|
+
function isObject(value) {
|
|
157
|
+
return value instanceof Map;
|
|
158
|
+
}
|
|
159
|
+
function readLeaf(sourceFile, key, value, localeCodes) {
|
|
160
|
+
const locales = localeCodes === void 0 ? void 0 : new Set(localeCodes);
|
|
161
|
+
const entry = Object.create(null);
|
|
162
|
+
for (const [locale, translation] of value) {
|
|
163
|
+
if (!LOCALE_CODE.test(locale)) fail(sourceFile, `${key}.${locale}`, "invalid locale code");
|
|
164
|
+
if (typeof translation !== "string") fail(sourceFile, `${key}.${locale}`, "locale values must be strings");
|
|
165
|
+
if (locales !== void 0 && !locales.has(locale)) fail(sourceFile, `${key}.${locale}`, "unknown locale code");
|
|
166
|
+
entry[locale] = translation;
|
|
167
|
+
}
|
|
168
|
+
if (localeCodes !== void 0) {
|
|
169
|
+
for (const locale of localeCodes) if (!(locale in entry)) fail(sourceFile, key, `missing locale code "${locale}"`);
|
|
170
|
+
}
|
|
171
|
+
return entry;
|
|
172
|
+
}
|
|
173
|
+
function collectEntries(sourceFile, object, group, localeCodes, entries, nested) {
|
|
174
|
+
if (nested && object.size === 0) fail(sourceFile, group, "nested objects cannot be empty");
|
|
175
|
+
for (const [segment, value] of object) {
|
|
176
|
+
const key = group === "" ? segment : `${group}.${segment}`;
|
|
177
|
+
if (segment === "$group") fail(sourceFile, key, "'$group' is only allowed at the root");
|
|
178
|
+
assertSegment(sourceFile, segment, key);
|
|
179
|
+
if (!isObject(value)) fail(sourceFile, key, "entries must be locale objects or nested objects");
|
|
180
|
+
if (value.has("$group")) fail(sourceFile, `${key}.$group`, "'$group' is only allowed at the root");
|
|
181
|
+
const values = [...value.values()];
|
|
182
|
+
const hasString = values.some((item) => typeof item === "string");
|
|
183
|
+
const allObjects = values.every(isObject);
|
|
184
|
+
if (hasString) {
|
|
185
|
+
if (!values.every((item) => typeof item === "string")) fail(sourceFile, key, "cannot mix locale values with nested entries");
|
|
186
|
+
entries[key] = readLeaf(sourceFile, key, value, localeCodes);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
if (!allObjects) fail(sourceFile, key, "cannot mix locale values with nested entries");
|
|
190
|
+
if (localeCodes !== void 0 && [...value.keys()].some((child) => localeCodes.includes(child))) fail(sourceFile, key, "cannot mix locale codes with nested entries");
|
|
191
|
+
collectEntries(sourceFile, value, key, localeCodes, entries, true);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/** Parses one locale dictionary source without reading the filesystem or loading application code. */
|
|
195
|
+
function parseLocaleDictionary(input) {
|
|
196
|
+
const { sourceFile, source, defaultNamespace, localeCodes } = input;
|
|
197
|
+
const parsed = new StrictJsonParser(sourceFile, source).parse();
|
|
198
|
+
if (!isObject(parsed)) fail(sourceFile, "$", "root JSON value must be an object");
|
|
199
|
+
const groupValue = parsed.get("$group");
|
|
200
|
+
if (groupValue !== void 0 && typeof groupValue !== "string") fail(sourceFile, "$group", "'$group' must be a string");
|
|
201
|
+
const group = groupValue === void 0 ? defaultNamespace === "" ? "" : explicitGroup(sourceFile, defaultNamespace) : explicitGroup(sourceFile, groupValue);
|
|
202
|
+
const entries = Object.create(null);
|
|
203
|
+
const rootEntries = new Map(parsed);
|
|
204
|
+
rootEntries.delete("$group");
|
|
205
|
+
collectEntries(sourceFile, rootEntries, group, localeCodes, entries, false);
|
|
206
|
+
return {
|
|
207
|
+
sourceFile,
|
|
208
|
+
group,
|
|
209
|
+
entries
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
//#endregion
|
|
214
|
+
export { LocaleDictionaryError, parseLocaleDictionary };
|
|
215
|
+
//# sourceMappingURL=parse-locale-dictionary.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-locale-dictionary.mjs","names":[],"sources":["../../../../../../../core/src/localization/parse-locale-dictionary.ts"],"sourcesContent":["export type LocaleDictionaryEntries = Record<string, Record<string, string>>;\n\nexport type ParsedLocaleDictionary = {\n sourceFile: string;\n group: string;\n entries: LocaleDictionaryEntries;\n};\n\ntype JsonValue = string | number | boolean | null | JsonObject | JsonValue[];\ntype JsonObject = Map<string, JsonValue>;\n\nconst SEGMENT = /^[A-Za-z_][A-Za-z0-9_-]*$/;\nconst LOCALE_CODE = /^[A-Za-z][A-Za-z0-9]*(?:-[A-Za-z0-9]+)*$/;\nconst FORBIDDEN_SEGMENTS = new Set([\"__proto__\", \"prototype\", \"constructor\"]);\n\n/** A source error that always names both the locale file and the offending JSON key. */\nexport class LocaleDictionaryError extends Error {\n public constructor(\n public readonly sourceFile: string,\n public readonly key: string,\n detail: string,\n ) {\n super(`Cannot parse route locales in \"${sourceFile}\" at \"${key}\": ${detail}.`);\n this.name = \"LocaleDictionaryError\";\n }\n}\n\nfunction fail(sourceFile: string, key: string, detail: string): never {\n throw new LocaleDictionaryError(sourceFile, key, detail);\n}\n\n/** A deliberately small JSON parser: unlike JSON.parse, it preserves duplicate property names. */\nclass StrictJsonParser {\n private offset = 0;\n\n public constructor(\n private readonly sourceFile: string,\n private readonly source: string,\n ) {}\n\n public parse(): JsonValue {\n if (this.source.charCodeAt(0) === 0xfeff) this.offset = 1;\n const value = this.value(\"$\");\n this.whitespace();\n if (this.offset !== this.source.length) this.invalid(\"$\", \"unexpected trailing JSON\");\n return value;\n }\n\n private value(key: string): JsonValue {\n this.whitespace();\n const character = this.source[this.offset];\n if (character === \"{\") return this.object(key);\n if (character === \"[\") return this.array(key);\n if (character === '\"') return this.string(key);\n if (this.source.startsWith(\"true\", this.offset)) return this.literal(\"true\", true);\n if (this.source.startsWith(\"false\", this.offset)) return this.literal(\"false\", false);\n if (this.source.startsWith(\"null\", this.offset)) return this.literal(\"null\", null);\n if (character === \"-\" || (character !== undefined && /[0-9]/.test(character)))\n return this.number(key);\n this.invalid(key, \"invalid JSON value\");\n }\n\n private object(key: string): JsonObject {\n this.offset++;\n this.whitespace();\n const object = new Map<string, JsonValue>();\n if (this.source[this.offset] === \"}\") {\n this.offset++;\n return object;\n }\n while (true) {\n this.whitespace();\n if (this.source[this.offset] !== '\"') this.invalid(key, \"object keys must be JSON strings\");\n const property = this.string(key);\n const propertyKey = `${key}.${property}`;\n if (object.has(property)) this.invalid(propertyKey, \"duplicate JSON property\");\n this.whitespace();\n if (this.source[this.offset] !== \":\")\n this.invalid(propertyKey, \"expected ':' after object key\");\n this.offset++;\n object.set(property, this.value(propertyKey));\n this.whitespace();\n const separator = this.source[this.offset];\n if (separator === \"}\") {\n this.offset++;\n return object;\n }\n if (separator !== \",\") this.invalid(key, \"expected ',' or '}' in object\");\n this.offset++;\n }\n }\n\n private array(key: string): JsonValue[] {\n this.offset++;\n this.whitespace();\n const values: JsonValue[] = [];\n if (this.source[this.offset] === \"]\") {\n this.offset++;\n return values;\n }\n while (true) {\n values.push(this.value(key));\n this.whitespace();\n const separator = this.source[this.offset];\n if (separator === \"]\") {\n this.offset++;\n return values;\n }\n if (separator !== \",\") this.invalid(key, \"expected ',' or ']' in array\");\n this.offset++;\n }\n }\n\n private string(key: string): string {\n const start = this.offset++;\n let escaped = false;\n while (this.offset < this.source.length) {\n const character = this.source[this.offset++];\n if (escaped) {\n escaped = false;\n continue;\n }\n if (character === \"\\\\\") {\n escaped = true;\n continue;\n }\n if (character === '\"') {\n try {\n return JSON.parse(this.source.slice(start, this.offset)) as string;\n } catch {\n this.invalid(key, \"invalid JSON string\");\n }\n }\n if (character !== undefined && character.charCodeAt(0) < 0x20)\n this.invalid(key, \"unescaped control character in JSON string\");\n }\n this.invalid(key, \"unterminated JSON string\");\n }\n\n private number(key: string): number {\n const match = /-?(?:0|[1-9][0-9]*)(?:\\.[0-9]+)?(?:[eE][+-]?[0-9]+)?/y;\n match.lastIndex = this.offset;\n const token = match.exec(this.source)?.[0];\n if (token === undefined) this.invalid(key, \"invalid JSON number\");\n this.offset += token.length;\n return Number(token);\n }\n\n private literal<T extends boolean | null>(written: string, value: T): T {\n this.offset += written.length;\n return value;\n }\n\n private whitespace(): void {\n while (this.offset < this.source.length) {\n const character = this.source[this.offset];\n if (character === undefined || !\" \\t\\n\\r\".includes(character)) return;\n this.offset++;\n }\n }\n\n private invalid(key: string, detail: string): never {\n fail(this.sourceFile, key, detail);\n }\n}\n\nfunction assertNamespaceSegment(sourceFile: string, segment: string, key: string): void {\n if (!SEGMENT.test(segment)) fail(sourceFile, key, \"segments must match [A-Za-z_][A-Za-z0-9_-]*\");\n if (FORBIDDEN_SEGMENTS.has(segment)) fail(sourceFile, key, \"segments cannot be reserved\");\n}\n\nfunction assertSegment(sourceFile: string, segment: string, key: string): void {\n if (segment.includes(\".\")) fail(sourceFile, key, \"JSON keys cannot contain '.'\");\n assertNamespaceSegment(sourceFile, segment, key);\n}\n\nfunction explicitGroup(sourceFile: string, value: string): string {\n if (value === \"\") fail(sourceFile, \"$group\", \"'$group' cannot be empty\");\n for (const segment of value.split(\".\")) {\n if (segment === \"\") fail(sourceFile, \"$group\", \"'$group' cannot contain empty segments\");\n assertNamespaceSegment(sourceFile, segment, \"$group\");\n }\n return value;\n}\n\nfunction isObject(value: JsonValue): value is JsonObject {\n return value instanceof Map;\n}\n\nfunction readLeaf(\n sourceFile: string,\n key: string,\n value: JsonObject,\n localeCodes: readonly string[] | undefined,\n): Record<string, string> {\n const locales = localeCodes === undefined ? undefined : new Set(localeCodes);\n const entry: Record<string, string> = Object.create(null) as Record<string, string>;\n for (const [locale, translation] of value) {\n if (!LOCALE_CODE.test(locale)) fail(sourceFile, `${key}.${locale}`, \"invalid locale code\");\n if (typeof translation !== \"string\")\n fail(sourceFile, `${key}.${locale}`, \"locale values must be strings\");\n if (locales !== undefined && !locales.has(locale))\n fail(sourceFile, `${key}.${locale}`, \"unknown locale code\");\n entry[locale] = translation;\n }\n if (localeCodes !== undefined)\n for (const locale of localeCodes)\n if (!(locale in entry)) fail(sourceFile, key, `missing locale code \"${locale}\"`);\n return entry;\n}\n\nfunction collectEntries(\n sourceFile: string,\n object: JsonObject,\n group: string,\n localeCodes: readonly string[] | undefined,\n entries: LocaleDictionaryEntries,\n nested: boolean,\n): void {\n if (nested && object.size === 0) fail(sourceFile, group, \"nested objects cannot be empty\");\n for (const [segment, value] of object) {\n const key = group === \"\" ? segment : `${group}.${segment}`;\n if (segment === \"$group\") fail(sourceFile, key, \"'$group' is only allowed at the root\");\n assertSegment(sourceFile, segment, key);\n if (!isObject(value)) fail(sourceFile, key, \"entries must be locale objects or nested objects\");\n if (value.has(\"$group\"))\n fail(sourceFile, `${key}.$group`, \"'$group' is only allowed at the root\");\n const values = [...value.values()];\n const hasString = values.some((item) => typeof item === \"string\");\n const allObjects = values.every(isObject);\n if (hasString) {\n if (!values.every((item) => typeof item === \"string\"))\n fail(sourceFile, key, \"cannot mix locale values with nested entries\");\n entries[key] = readLeaf(sourceFile, key, value, localeCodes);\n continue;\n }\n if (!allObjects) fail(sourceFile, key, \"cannot mix locale values with nested entries\");\n if (localeCodes !== undefined && [...value.keys()].some((child) => localeCodes.includes(child)))\n fail(sourceFile, key, \"cannot mix locale codes with nested entries\");\n collectEntries(sourceFile, value, key, localeCodes, entries, true);\n }\n}\n\n/** Parses one locale dictionary source without reading the filesystem or loading application code. */\nexport function parseLocaleDictionary(input: {\n sourceFile: string;\n source: string;\n defaultNamespace: string;\n localeCodes?: readonly string[];\n}): ParsedLocaleDictionary {\n const { sourceFile, source, defaultNamespace, localeCodes } = input;\n const parsed = new StrictJsonParser(sourceFile, source).parse();\n if (!isObject(parsed)) fail(sourceFile, \"$\", \"root JSON value must be an object\");\n const groupValue = parsed.get(\"$group\");\n if (groupValue !== undefined && typeof groupValue !== \"string\")\n fail(sourceFile, \"$group\", \"'$group' must be a string\");\n const group =\n groupValue === undefined\n ? defaultNamespace === \"\"\n ? \"\"\n : explicitGroup(sourceFile, defaultNamespace)\n : explicitGroup(sourceFile, groupValue);\n const entries: LocaleDictionaryEntries = Object.create(null) as LocaleDictionaryEntries;\n const rootEntries = new Map(parsed);\n rootEntries.delete(\"$group\");\n collectEntries(sourceFile, rootEntries, group, localeCodes, entries, false);\n return { sourceFile, group, entries };\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region ../core/src/localization/register-locale-dictionary.d.ts
|
|
2
|
+
declare function registerGroupedTranslationKeys(sourceFile: string, keys: readonly string[]): void;
|
|
3
|
+
/** Undo JSON entries during HMR while preserving unrelated global translations. */
|
|
4
|
+
declare function unregisterLocaleDictionary(sourceFile: string): void;
|
|
5
|
+
declare function registerLocaleDictionary(input: {
|
|
6
|
+
sourceFile: string;
|
|
7
|
+
source: string;
|
|
8
|
+
defaultNamespace: string;
|
|
9
|
+
}): () => void;
|
|
10
|
+
//#endregion
|
|
11
|
+
export { registerGroupedTranslationKeys, registerLocaleDictionary, unregisterLocaleDictionary };
|
|
12
|
+
//# sourceMappingURL=register-locale-dictionary.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register-locale-dictionary.d.mts","names":[],"sources":["../../../../../../../core/src/localization/register-locale-dictionary.ts"],"mappings":";iBAwBgB,8BAAA,CAA+B,UAAA,UAAoB,IAAuB;AAA1F;AAAA,iBAKgB,0BAAA,CAA2B,UAAkB;AAAA,iBAyB7C,wBAAA,CAAyB,KAAA;EACvC,UAAA;EACA,MAAA;EACA,gBAAA;AAAA"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { parseLocaleDictionary } from "./parse-locale-dictionary.mjs";
|
|
2
|
+
import config from "@mongez/config";
|
|
3
|
+
import { getTranslationsList, groupedTranslations, setTranslationsList } from "@mongez/localization";
|
|
4
|
+
|
|
5
|
+
//#region ../core/src/localization/register-locale-dictionary.ts
|
|
6
|
+
const owners = /* @__PURE__ */ new Map();
|
|
7
|
+
const keysBySource = /* @__PURE__ */ new Map();
|
|
8
|
+
function claim(sourceFile, kind, keys) {
|
|
9
|
+
for (const key of keys) {
|
|
10
|
+
const owner = owners.get(key);
|
|
11
|
+
if (owner && owner.sourceFile !== sourceFile) throw new Error(`Translation key "${key}" is owned by both "${owner.sourceFile}" and "${sourceFile}".`);
|
|
12
|
+
}
|
|
13
|
+
for (const key of keys) owners.set(key, {
|
|
14
|
+
sourceFile,
|
|
15
|
+
kind
|
|
16
|
+
});
|
|
17
|
+
keysBySource.set(sourceFile, [...keys]);
|
|
18
|
+
}
|
|
19
|
+
function registerGroupedTranslationKeys(sourceFile, keys) {
|
|
20
|
+
claim(sourceFile, "ts", keys);
|
|
21
|
+
}
|
|
22
|
+
/** Undo JSON entries during HMR while preserving unrelated global translations. */
|
|
23
|
+
function unregisterLocaleDictionary(sourceFile) {
|
|
24
|
+
const keys = keysBySource.get(sourceFile) ?? [];
|
|
25
|
+
keysBySource.delete(sourceFile);
|
|
26
|
+
for (const key of keys) owners.delete(key);
|
|
27
|
+
const translations = getTranslationsList();
|
|
28
|
+
for (const key of keys) for (const locale of Object.keys(translations)) {
|
|
29
|
+
const segments = key.split(".");
|
|
30
|
+
let target = translations[locale];
|
|
31
|
+
for (const segment of segments.slice(0, -1)) {
|
|
32
|
+
const next = target?.[segment];
|
|
33
|
+
if (!next || typeof next !== "object") {
|
|
34
|
+
target = void 0;
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
target = next;
|
|
38
|
+
}
|
|
39
|
+
if (target) delete target[segments.at(-1)];
|
|
40
|
+
}
|
|
41
|
+
setTranslationsList(translations);
|
|
42
|
+
}
|
|
43
|
+
function registerLocaleDictionary(input) {
|
|
44
|
+
unregisterLocaleDictionary(input.sourceFile);
|
|
45
|
+
const localeCodes = config.get("app.localeCodes");
|
|
46
|
+
const parsed = parseLocaleDictionary({
|
|
47
|
+
...input,
|
|
48
|
+
localeCodes: Array.isArray(localeCodes) ? localeCodes : void 0
|
|
49
|
+
});
|
|
50
|
+
const keys = Object.keys(parsed.entries);
|
|
51
|
+
claim(input.sourceFile, "json", keys);
|
|
52
|
+
groupedTranslations(parsed.entries);
|
|
53
|
+
return () => unregisterLocaleDictionary(input.sourceFile);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
//#endregion
|
|
57
|
+
export { registerGroupedTranslationKeys, registerLocaleDictionary, unregisterLocaleDictionary };
|
|
58
|
+
//# sourceMappingURL=register-locale-dictionary.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"register-locale-dictionary.mjs","names":[],"sources":["../../../../../../../core/src/localization/register-locale-dictionary.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport {\n getTranslationsList,\n groupedTranslations,\n setTranslationsList,\n} from \"@mongez/localization\";\nimport { parseLocaleDictionary } from \"./parse-locale-dictionary\";\n\ntype Owner = { sourceFile: string; kind: \"json\" | \"ts\" };\nconst owners = new Map<string, Owner>();\nconst keysBySource = new Map<string, string[]>();\n\nfunction claim(sourceFile: string, kind: Owner[\"kind\"], keys: readonly string[]): void {\n for (const key of keys) {\n const owner = owners.get(key);\n if (owner && owner.sourceFile !== sourceFile)\n throw new Error(\n `Translation key \"${key}\" is owned by both \"${owner.sourceFile}\" and \"${sourceFile}\".`,\n );\n }\n for (const key of keys) owners.set(key, { sourceFile, kind });\n keysBySource.set(sourceFile, [...keys]);\n}\n\nexport function registerGroupedTranslationKeys(sourceFile: string, keys: readonly string[]): void {\n claim(sourceFile, \"ts\", keys);\n}\n\n/** Undo JSON entries during HMR while preserving unrelated global translations. */\nexport function unregisterLocaleDictionary(sourceFile: string): void {\n const keys = keysBySource.get(sourceFile) ?? [];\n keysBySource.delete(sourceFile);\n for (const key of keys) owners.delete(key);\n const translations = getTranslationsList();\n for (const key of keys)\n for (const locale of Object.keys(translations)) {\n const segments = key.split(\".\");\n let target: Record<string, unknown> | undefined = translations[locale] as Record<\n string,\n unknown\n >;\n for (const segment of segments.slice(0, -1)) {\n const next = target?.[segment];\n if (!next || typeof next !== \"object\") {\n target = undefined;\n break;\n }\n target = next as Record<string, unknown>;\n }\n if (target) delete target[segments.at(-1)!];\n }\n setTranslationsList(translations);\n}\n\nexport function registerLocaleDictionary(input: {\n sourceFile: string;\n source: string;\n defaultNamespace: string;\n}): () => void {\n unregisterLocaleDictionary(input.sourceFile);\n const localeCodes = config.get(\"app.localeCodes\");\n const parsed = parseLocaleDictionary({\n ...input,\n localeCodes: Array.isArray(localeCodes) ? localeCodes : undefined,\n });\n const keys = Object.keys(parsed.entries);\n claim(input.sourceFile, \"json\", keys);\n groupedTranslations(parsed.entries);\n return () => unregisterLocaleDictionary(input.sourceFile);\n}\n"],"mappings":";;;;;AASA,MAAM,yBAAS,IAAI,IAAmB;AACtC,MAAM,+BAAe,IAAI,IAAsB;AAE/C,SAAS,MAAM,YAAoB,MAAqB,MAA+B;CACrF,KAAK,MAAM,OAAO,MAAM;EACtB,MAAM,QAAQ,OAAO,IAAI,GAAG;EAC5B,IAAI,SAAS,MAAM,eAAe,YAChC,MAAM,IAAI,MACR,oBAAoB,IAAI,sBAAsB,MAAM,WAAW,SAAS,WAAW,GACrF;CACJ;CACA,KAAK,MAAM,OAAO,MAAM,OAAO,IAAI,KAAK;EAAE;EAAY;CAAK,CAAC;CAC5D,aAAa,IAAI,YAAY,CAAC,GAAG,IAAI,CAAC;AACxC;AAEA,SAAgB,+BAA+B,YAAoB,MAA+B;CAChG,MAAM,YAAY,MAAM,IAAI;AAC9B;;AAGA,SAAgB,2BAA2B,YAA0B;CACnE,MAAM,OAAO,aAAa,IAAI,UAAU,KAAK,CAAC;CAC9C,aAAa,OAAO,UAAU;CAC9B,KAAK,MAAM,OAAO,MAAM,OAAO,OAAO,GAAG;CACzC,MAAM,eAAe,oBAAoB;CACzC,KAAK,MAAM,OAAO,MAChB,KAAK,MAAM,UAAU,OAAO,KAAK,YAAY,GAAG;EAC9C,MAAM,WAAW,IAAI,MAAM,GAAG;EAC9B,IAAI,SAA8C,aAAa;EAI/D,KAAK,MAAM,WAAW,SAAS,MAAM,GAAG,EAAE,GAAG;GAC3C,MAAM,OAAO,SAAS;GACtB,IAAI,CAAC,QAAQ,OAAO,SAAS,UAAU;IACrC,SAAS;IACT;GACF;GACA,SAAS;EACX;EACA,IAAI,QAAQ,OAAO,OAAO,SAAS,GAAG,EAAE;CAC1C;CACF,oBAAoB,YAAY;AAClC;AAEA,SAAgB,yBAAyB,OAI1B;CACb,2BAA2B,MAAM,UAAU;CAC3C,MAAM,cAAc,OAAO,IAAI,iBAAiB;CAChD,MAAM,SAAS,sBAAsB;EACnC,GAAG;EACH,aAAa,MAAM,QAAQ,WAAW,IAAI,cAAc;CAC1D,CAAC;CACD,MAAM,OAAO,OAAO,KAAK,OAAO,OAAO;CACvC,MAAM,MAAM,YAAY,QAAQ,IAAI;CACpC,oBAAoB,OAAO,OAAO;CAClC,aAAa,2BAA2B,MAAM,UAAU;AAC1D"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logger.mjs","names":[],"sources":["../../../../../../../core/src/logger/logger.ts"],"sourcesContent":["import type { LogChannel } from \"@warlock.js/logger\";\
|
|
1
|
+
{"version":3,"file":"logger.mjs","names":[],"sources":["../../../../../../../core/src/logger/logger.ts"],"sourcesContent":["import type { LogChannel } from \"@warlock.js/logger\";\nimport { log } from \"@warlock.js/logger\";\nimport { environment } from \"../utils\";\nimport type { LogConfigurations } from \"./types\";\n\nexport function setLogConfigurations(options: LogConfigurations) {\n // log configurations\n const channels: LogChannel[] = [];\n\n const env = environment();\n\n const envOptions = options[env as \"development\" | \"test\" | \"production\"];\n\n // the environment flag wins over the top-level flag\n const enabled = envOptions?.enabled ?? options.enabled ?? true;\n\n if (!enabled) {\n log.configure({ channels: [] });\n return;\n }\n\n const envChannels = envOptions?.channels;\n const defaultChannels = options.channels;\n\n if (defaultChannels) {\n channels.push(...defaultChannels);\n }\n\n if (envChannels) {\n channels.push(...envChannels);\n }\n\n log.configure({\n channels,\n });\n}\n"],"mappings":";;;;;AAKA,SAAgB,qBAAqB,SAA4B;CAE/D,MAAM,WAAyB,CAAC;CAIhC,MAAM,aAAa,QAFP,YAEiB;CAK7B,IAAI,EAFY,YAAY,WAAW,QAAQ,WAAW,OAE5C;EACZ,IAAI,UAAU,EAAE,UAAU,CAAC,EAAE,CAAC;EAC9B;CACF;CAEA,MAAM,cAAc,YAAY;CAChC,MAAM,kBAAkB,QAAQ;CAEhC,IAAI,iBACF,SAAS,KAAK,GAAG,eAAe;CAGlC,IAAI,aACF,SAAS,KAAK,GAAG,WAAW;CAG9B,IAAI,UAAU,EACZ,SACF,CAAC;AACH"}
|
package/esm/mail/events.d.mts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { CapturedMail } from "./types.mjs";
|
|
2
|
+
|
|
1
3
|
//#region ../core/src/mail/events.d.ts
|
|
2
4
|
/**
|
|
3
5
|
* Generate a unique mail ID for event namespacing
|
|
@@ -8,6 +10,7 @@ declare function generateMailId(): string;
|
|
|
8
10
|
*/
|
|
9
11
|
declare const MAIL_EVENTS: {
|
|
10
12
|
readonly BEFORE_SENDING: "mail.beforeSending";
|
|
13
|
+
readonly CAPTURED: "mail.captured";
|
|
11
14
|
readonly SENT: "mail.sent";
|
|
12
15
|
readonly SUCCESS: "mail.success";
|
|
13
16
|
readonly ERROR: "mail.error";
|
|
@@ -61,6 +64,10 @@ declare const mailEvents: {
|
|
|
61
64
|
* Subscribe to global mail events
|
|
62
65
|
*/
|
|
63
66
|
on: (eventName: keyof typeof MAIL_EVENTS, callback: (...args: any[]) => void) => import("@mongez/events").EventSubscription;
|
|
67
|
+
/**
|
|
68
|
+
* Subscribe to mail captured in development or test mode.
|
|
69
|
+
*/
|
|
70
|
+
onCaptured: (callback: (mail: CapturedMail) => void) => import("@mongez/events").EventSubscription;
|
|
64
71
|
/**
|
|
65
72
|
* Subscribe to beforeSending event (all mails)
|
|
66
73
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.d.mts","names":[],"sources":["../../../../../../../core/src/mail/events.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"events.d.mts","names":[],"sources":["../../../../../../../core/src/mail/events.ts"],"mappings":";;;;;AAOA;iBAAgB,cAAA;;;AAAc;cAOjB,WAAA;EAAA;;;;;;;;;;AAcb;;iBAAgB,gBAAA,CACd,MAAA,UACA,KAAqD;;AAAA;AAkCvD;;;;;;;;;;;;;;;;;;;;;;;;;;;cAAa,UAAA;;;;oCAIuB,WAAA,KAAW,IAAA;;;;mCAQ7B,KAAA,qDACuC,IAAA;EAkBJ;;;+BATtB,WAAA,EAAW,QAAA,MAAgB,IAAA,8CAAoB,iBAAA;EAgBF;;;0BAPlD,IAAA,EAAM,YAAA,uCAAqB,iBAAA;EAc/B;;;+BAPS,IAAA,iBAAqB,OAAA,iBAAwB,OAAA,qCAAc,iBAAA;;;;sBAOpE,IAAA,OAAW,MAAA,OAAa,KAAA,iBAAsB,OAAA,oCAAa,iBAAA;EAO7C;;;yBAAX,IAAA,OAAW,MAAA,iBAAuB,OAAA,oCAAa,iBAAA;;;;uBAOjD,IAAA,OAAW,KAAA,iBAAsB,OAAA,oCAAa,iBAAA;;;;wCAUnD,QAAA,GACH,IAAA,iBAAqB,OAAA,iBAAwB,OAAA,qCAAc,iBAAA;EAA3D;;;+BASG,QAAA,GACH,IAAA,OAAW,MAAA,OAAa,KAAA,iBAAsB,OAAA,oCAAa,iBAAA;EAVA;;;kCAkB1C,QAAA,GAAa,IAAA,OAAW,MAAA,iBAAuB,OAAA,oCAAa,iBAAA;EARlE;;;gCAeI,QAAA,GAAa,IAAA,OAAW,KAAA,iBAAsB,OAAA,oCAAa,iBAAA;AAAA"}
|
package/esm/mail/events.mjs
CHANGED
|
@@ -13,6 +13,7 @@ function generateMailId() {
|
|
|
13
13
|
*/
|
|
14
14
|
const MAIL_EVENTS = {
|
|
15
15
|
BEFORE_SENDING: "mail.beforeSending",
|
|
16
|
+
CAPTURED: "mail.captured",
|
|
16
17
|
SENT: "mail.sent",
|
|
17
18
|
SUCCESS: "mail.success",
|
|
18
19
|
ERROR: "mail.error"
|
|
@@ -75,6 +76,12 @@ const mailEvents = {
|
|
|
75
76
|
return events.subscribe(MAIL_EVENTS[eventName], callback);
|
|
76
77
|
},
|
|
77
78
|
/**
|
|
79
|
+
* Subscribe to mail captured in development or test mode.
|
|
80
|
+
*/
|
|
81
|
+
onCaptured: (callback) => {
|
|
82
|
+
return events.subscribe(MAIL_EVENTS.CAPTURED, callback);
|
|
83
|
+
},
|
|
84
|
+
/**
|
|
78
85
|
* Subscribe to beforeSending event (all mails)
|
|
79
86
|
*/
|
|
80
87
|
onBeforeSending: (callback) => {
|
package/esm/mail/events.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"events.mjs","names":[],"sources":["../../../../../../../core/src/mail/events.ts"],"sourcesContent":["import events from \"@mongez/events\";\nimport { Random } from \"@mongez/reinforcements\";\n\n/**\n * Generate a unique mail ID for event namespacing\n */\nexport function generateMailId(): string {\n return \"M\" + Random.int(32);\n}\n\n/**\n * Mail event names (global events)\n */\nexport const MAIL_EVENTS = {\n BEFORE_SENDING: \"mail.beforeSending\",\n SENT: \"mail.sent\",\n SUCCESS: \"mail.success\",\n ERROR: \"mail.error\",\n} as const;\n\n/**\n * Get namespaced event name for a specific mail\n * @param mailId Unique mail identifier\n * @param event Event type\n * @returns Namespaced event name (e.g., \"mail.abc123.success\")\n */\nexport function getMailEventName(\n mailId: string,\n event: \"beforeSending\" | \"sent\" | \"success\" | \"error\",\n): string {\n return `mail.${mailId}.${event}`;\n}\n\n/**\n * Mail events wrapper\n *\n * Supports two event patterns:\n * 1. Global events: `mail.success` - fires for ALL emails\n * 2. Specific events: `mail.$mailId.success` - fires for ONE email\n *\n * ## Usage\n *\n * ```typescript\n * // Global listener - all emails\n * mailEvents.onSuccess((mail, result) => {\n * console.log(\"Any mail sent:\", result.messageId);\n * });\n *\n * // Specific listener - one email by ID\n * const mailId = generateMailId();\n * mailEvents.onMailSuccess(mailId, (mail, result) => {\n * console.log(\"This specific mail sent:\", result.messageId);\n * });\n *\n * await sendMail({\n * id: mailId, // Use the same ID\n * to: \"user@example.com\",\n * subject: \"Hello\",\n * html: \"<p>World</p>\",\n * });\n * ```\n */\nexport const mailEvents = {\n /**\n * Trigger a global mail event\n */\n trigger: (eventName: keyof typeof MAIL_EVENTS, ...args: any[]) => {\n return events.trigger(MAIL_EVENTS[eventName], ...args);\n },\n\n /**\n * Trigger a specific mail event (by mail ID)\n */\n triggerForMail: (\n mailId: string,\n event: \"beforeSending\" | \"sent\" | \"success\" | \"error\",\n ...args: any[]\n ) => {\n return events.trigger(getMailEventName(mailId, event), ...args);\n },\n\n /**\n * Subscribe to global mail events\n */\n on: (eventName: keyof typeof MAIL_EVENTS, callback: (...args: any[]) => void) => {\n return events.subscribe(MAIL_EVENTS[eventName], callback);\n },\n\n // === GLOBAL EVENT HELPERS ===\n\n /**\n * Subscribe to beforeSending event (all mails)\n */\n onBeforeSending: (callback: (mail: any) => void | Promise<void> | false | Promise<false>) => {\n return events.subscribe(MAIL_EVENTS.BEFORE_SENDING, callback);\n },\n\n /**\n * Subscribe to sent event (all mails, after attempt)\n */\n onSent: (callback: (mail: any, result: any, error: any) => void | Promise<void>) => {\n return events.subscribe(MAIL_EVENTS.SENT, callback);\n },\n\n /**\n * Subscribe to success event (all mails)\n */\n onSuccess: (callback: (mail: any, result: any) => void | Promise<void>) => {\n return events.subscribe(MAIL_EVENTS.SUCCESS, callback);\n },\n\n /**\n * Subscribe to error event (all mails)\n */\n onError: (callback: (mail: any, error: any) => void | Promise<void>) => {\n return events.subscribe(MAIL_EVENTS.ERROR, callback);\n },\n\n // === SPECIFIC MAIL EVENT HELPERS ===\n\n /**\n * Subscribe to beforeSending event for a specific mail\n */\n onMailBeforeSending: (\n mailId: string,\n callback: (mail: any) => void | Promise<void> | false | Promise<false>,\n ) => {\n return events.subscribe(getMailEventName(mailId, \"beforeSending\"), callback);\n },\n\n /**\n * Subscribe to sent event for a specific mail\n */\n onMailSent: (\n mailId: string,\n callback: (mail: any, result: any, error: any) => void | Promise<void>,\n ) => {\n return events.subscribe(getMailEventName(mailId, \"sent\"), callback);\n },\n\n /**\n * Subscribe to success event for a specific mail\n */\n onMailSuccess: (mailId: string, callback: (mail: any, result: any) => void | Promise<void>) => {\n return events.subscribe(getMailEventName(mailId, \"success\"), callback);\n },\n\n /**\n * Subscribe to error event for a specific mail\n */\n onMailError: (mailId: string, callback: (mail: any, error: any) => void | Promise<void>) => {\n return events.subscribe(getMailEventName(mailId, \"error\"), callback);\n },\n};\n"],"mappings":";;;;;;;
|
|
1
|
+
{"version":3,"file":"events.mjs","names":[],"sources":["../../../../../../../core/src/mail/events.ts"],"sourcesContent":["import events from \"@mongez/events\";\nimport { Random } from \"@mongez/reinforcements\";\nimport type { CapturedMail } from \"./types\";\n\n/**\n * Generate a unique mail ID for event namespacing\n */\nexport function generateMailId(): string {\n return \"M\" + Random.int(32);\n}\n\n/**\n * Mail event names (global events)\n */\nexport const MAIL_EVENTS = {\n BEFORE_SENDING: \"mail.beforeSending\",\n CAPTURED: \"mail.captured\",\n SENT: \"mail.sent\",\n SUCCESS: \"mail.success\",\n ERROR: \"mail.error\",\n} as const;\n\n/**\n * Get namespaced event name for a specific mail\n * @param mailId Unique mail identifier\n * @param event Event type\n * @returns Namespaced event name (e.g., \"mail.abc123.success\")\n */\nexport function getMailEventName(\n mailId: string,\n event: \"beforeSending\" | \"sent\" | \"success\" | \"error\",\n): string {\n return `mail.${mailId}.${event}`;\n}\n\n/**\n * Mail events wrapper\n *\n * Supports two event patterns:\n * 1. Global events: `mail.success` - fires for ALL emails\n * 2. Specific events: `mail.$mailId.success` - fires for ONE email\n *\n * ## Usage\n *\n * ```typescript\n * // Global listener - all emails\n * mailEvents.onSuccess((mail, result) => {\n * console.log(\"Any mail sent:\", result.messageId);\n * });\n *\n * // Specific listener - one email by ID\n * const mailId = generateMailId();\n * mailEvents.onMailSuccess(mailId, (mail, result) => {\n * console.log(\"This specific mail sent:\", result.messageId);\n * });\n *\n * await sendMail({\n * id: mailId, // Use the same ID\n * to: \"user@example.com\",\n * subject: \"Hello\",\n * html: \"<p>World</p>\",\n * });\n * ```\n */\nexport const mailEvents = {\n /**\n * Trigger a global mail event\n */\n trigger: (eventName: keyof typeof MAIL_EVENTS, ...args: any[]) => {\n return events.trigger(MAIL_EVENTS[eventName], ...args);\n },\n\n /**\n * Trigger a specific mail event (by mail ID)\n */\n triggerForMail: (\n mailId: string,\n event: \"beforeSending\" | \"sent\" | \"success\" | \"error\",\n ...args: any[]\n ) => {\n return events.trigger(getMailEventName(mailId, event), ...args);\n },\n\n /**\n * Subscribe to global mail events\n */\n on: (eventName: keyof typeof MAIL_EVENTS, callback: (...args: any[]) => void) => {\n return events.subscribe(MAIL_EVENTS[eventName], callback);\n },\n\n // === GLOBAL EVENT HELPERS ===\n\n /**\n * Subscribe to mail captured in development or test mode.\n */\n onCaptured: (callback: (mail: CapturedMail) => void) => {\n return events.subscribe(MAIL_EVENTS.CAPTURED, callback);\n },\n\n /**\n * Subscribe to beforeSending event (all mails)\n */\n onBeforeSending: (callback: (mail: any) => void | Promise<void> | false | Promise<false>) => {\n return events.subscribe(MAIL_EVENTS.BEFORE_SENDING, callback);\n },\n\n /**\n * Subscribe to sent event (all mails, after attempt)\n */\n onSent: (callback: (mail: any, result: any, error: any) => void | Promise<void>) => {\n return events.subscribe(MAIL_EVENTS.SENT, callback);\n },\n\n /**\n * Subscribe to success event (all mails)\n */\n onSuccess: (callback: (mail: any, result: any) => void | Promise<void>) => {\n return events.subscribe(MAIL_EVENTS.SUCCESS, callback);\n },\n\n /**\n * Subscribe to error event (all mails)\n */\n onError: (callback: (mail: any, error: any) => void | Promise<void>) => {\n return events.subscribe(MAIL_EVENTS.ERROR, callback);\n },\n\n // === SPECIFIC MAIL EVENT HELPERS ===\n\n /**\n * Subscribe to beforeSending event for a specific mail\n */\n onMailBeforeSending: (\n mailId: string,\n callback: (mail: any) => void | Promise<void> | false | Promise<false>,\n ) => {\n return events.subscribe(getMailEventName(mailId, \"beforeSending\"), callback);\n },\n\n /**\n * Subscribe to sent event for a specific mail\n */\n onMailSent: (\n mailId: string,\n callback: (mail: any, result: any, error: any) => void | Promise<void>,\n ) => {\n return events.subscribe(getMailEventName(mailId, \"sent\"), callback);\n },\n\n /**\n * Subscribe to success event for a specific mail\n */\n onMailSuccess: (mailId: string, callback: (mail: any, result: any) => void | Promise<void>) => {\n return events.subscribe(getMailEventName(mailId, \"success\"), callback);\n },\n\n /**\n * Subscribe to error event for a specific mail\n */\n onMailError: (mailId: string, callback: (mail: any, error: any) => void | Promise<void>) => {\n return events.subscribe(getMailEventName(mailId, \"error\"), callback);\n },\n};\n"],"mappings":";;;;;;;AAOA,SAAgB,iBAAyB;CACvC,OAAO,MAAM,OAAO,IAAI,EAAE;AAC5B;;;;AAKA,MAAa,cAAc;CACzB,gBAAgB;CAChB,UAAU;CACV,MAAM;CACN,SAAS;CACT,OAAO;AACT;;;;;;;AAQA,SAAgB,iBACd,QACA,OACQ;CACR,OAAO,QAAQ,OAAO,GAAG;AAC3B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA+BA,MAAa,aAAa;;;;CAIxB,UAAU,WAAqC,GAAG,SAAgB;EAChE,OAAO,OAAO,QAAQ,YAAY,YAAY,GAAG,IAAI;CACvD;;;;CAKA,iBACE,QACA,OACA,GAAG,SACA;EACH,OAAO,OAAO,QAAQ,iBAAiB,QAAQ,KAAK,GAAG,GAAG,IAAI;CAChE;;;;CAKA,KAAK,WAAqC,aAAuC;EAC/E,OAAO,OAAO,UAAU,YAAY,YAAY,QAAQ;CAC1D;;;;CAOA,aAAa,aAA2C;EACtD,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ;CACxD;;;;CAKA,kBAAkB,aAA2E;EAC3F,OAAO,OAAO,UAAU,YAAY,gBAAgB,QAAQ;CAC9D;;;;CAKA,SAAS,aAA2E;EAClF,OAAO,OAAO,UAAU,YAAY,MAAM,QAAQ;CACpD;;;;CAKA,YAAY,aAA+D;EACzE,OAAO,OAAO,UAAU,YAAY,SAAS,QAAQ;CACvD;;;;CAKA,UAAU,aAA8D;EACtE,OAAO,OAAO,UAAU,YAAY,OAAO,QAAQ;CACrD;;;;CAOA,sBACE,QACA,aACG;EACH,OAAO,OAAO,UAAU,iBAAiB,QAAQ,eAAe,GAAG,QAAQ;CAC7E;;;;CAKA,aACE,QACA,aACG;EACH,OAAO,OAAO,UAAU,iBAAiB,QAAQ,MAAM,GAAG,QAAQ;CACpE;;;;CAKA,gBAAgB,QAAgB,aAA+D;EAC7F,OAAO,OAAO,UAAU,iBAAiB,QAAQ,SAAS,GAAG,QAAQ;CACvE;;;;CAKA,cAAc,QAAgB,aAA8D;EAC1F,OAAO,OAAO,UAAU,iBAAiB,QAAQ,OAAO,GAAG,QAAQ;CACrE;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"send-mail.d.mts","names":[],"sources":["../../../../../../../core/src/mail/send-mail.ts"],"mappings":";;;;;
|
|
1
|
+
{"version":3,"file":"send-mail.d.mts","names":[],"sources":["../../../../../../../core/src/mail/send-mail.ts"],"mappings":";;;;;AAuRA;;;;;;;;;;;;;AAAwE;;;;;;;;;;;;;;;iBAAlD,QAAA,CAAS,OAAA,EAAS,WAAA,GAAc,OAAA,CAAQ,UAAA"}
|
package/esm/mail/send-mail.mjs
CHANGED
|
@@ -154,6 +154,17 @@ async function triggerEvents(mailId, event, ...args) {
|
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
156
|
/**
|
|
157
|
+
* Notify development and test tooling of a captured mail without allowing a
|
|
158
|
+
* synchronous listener failure to interrupt delivery.
|
|
159
|
+
*/
|
|
160
|
+
function triggerCapturedMail(captured) {
|
|
161
|
+
try {
|
|
162
|
+
mailEvents.trigger("CAPTURED", captured);
|
|
163
|
+
} catch (error) {
|
|
164
|
+
log.error("mail", "event", `Captured mail event handler error: ${error}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
157
168
|
* Send an email
|
|
158
169
|
*
|
|
159
170
|
* @param options Mail options including recipients, content, and configuration
|
|
@@ -215,12 +226,15 @@ async function sendMail(options) {
|
|
|
215
226
|
rejected: [],
|
|
216
227
|
response: "Test mode - mail captured"
|
|
217
228
|
};
|
|
218
|
-
|
|
229
|
+
const captured = {
|
|
230
|
+
id: mailId,
|
|
219
231
|
options,
|
|
220
232
|
normalized,
|
|
221
233
|
timestamp: /* @__PURE__ */ new Date(),
|
|
222
234
|
result
|
|
223
|
-
}
|
|
235
|
+
};
|
|
236
|
+
captureMail(captured);
|
|
237
|
+
triggerCapturedMail(captured);
|
|
224
238
|
await runMailEvent(options.onSuccess, options, result);
|
|
225
239
|
await triggerEvents(mailId, "success", options, result);
|
|
226
240
|
await runMailEvent(options.onSent, options, result, null);
|
|
@@ -238,6 +252,13 @@ async function sendMail(options) {
|
|
|
238
252
|
rejected: [],
|
|
239
253
|
response: "Development mode - mail logged"
|
|
240
254
|
};
|
|
255
|
+
triggerCapturedMail({
|
|
256
|
+
id: mailId,
|
|
257
|
+
options,
|
|
258
|
+
normalized,
|
|
259
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
260
|
+
result
|
|
261
|
+
});
|
|
241
262
|
await runMailEvent(options.onSuccess, options, result);
|
|
242
263
|
await triggerEvents(mailId, "success", options, result);
|
|
243
264
|
await runMailEvent(options.onSent, options, result, null);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"send-mail.mjs","names":[],"sources":["../../../../../../../core/src/mail/send-mail.ts"],"sourcesContent":["import { log } from \"@warlock.js/logger\";\nimport { readFile } from \"node:fs/promises\";\nimport type { Options as NodemailerOptions } from \"nodemailer/lib/mailer\";\nimport { isDevelopmentMode, isTestMode, resolveMailConfig } from \"./config\";\nimport { MAIL_EVENTS, generateMailId, mailEvents } from \"./events\";\nimport { getMailer } from \"./mailer-pool\";\nimport { renderReactMail } from \"./react-mail\";\nimport { captureMail } from \"./test-mailbox\";\nimport {\n MailError,\n type CapturedMail,\n type MailAttachment,\n type MailOptions,\n type MailPriority,\n type MailResult,\n type NormalizedMail,\n} from \"./types\";\n\n/**\n * Normalize recipients to array\n */\nfunction normalizeRecipients(recipients: string | string[] | undefined): string[] {\n if (!recipients) {\n return [];\n }\n\n return Array.isArray(recipients) ? recipients : [recipients];\n}\n\n/**\n * Map priority to nodemailer format\n */\nfunction mapPriority(priority: MailPriority): \"high\" | \"normal\" | \"low\" {\n return priority;\n}\n\n/**\n * Normalize mail options to internal format\n */\nfunction normalizeMail(options: MailOptions): NormalizedMail {\n const config = resolveMailConfig(options);\n\n return {\n to: normalizeRecipients(options.to),\n cc: normalizeRecipients(options.cc),\n bcc: normalizeRecipients(options.bcc),\n replyTo: options.replyTo,\n from: options.from || config.from || { name: \"No Reply\", address: \"noreply@localhost\" },\n subject: options.subject,\n html: options.html,\n text: options.text,\n attachments: options.attachments || [],\n priority: options.priority || \"normal\",\n headers: options.headers || {},\n tags: options.tags || [],\n correlationId: options.correlationId,\n config,\n };\n}\n\n/**\n * Resolve attachment - read file if path is provided\n */\nasync function resolveAttachment(attachment: MailAttachment): Promise<{\n filename: string;\n content: Buffer | string;\n contentType?: string;\n encoding?: string;\n cid?: string;\n}> {\n // If path is provided, read the file\n if (attachment.path) {\n try {\n const content = await readFile(attachment.path);\n return {\n filename: attachment.filename,\n content,\n contentType: attachment.contentType,\n encoding: attachment.encoding,\n cid: attachment.cid,\n };\n } catch (error) {\n throw new MailError(\n `Failed to read attachment file \"${attachment.path}\": ${error instanceof Error ? error.message : \"Unknown error\"}`,\n \"CONFIG_ERROR\",\n error instanceof Error ? error : undefined,\n );\n }\n }\n\n // Already has content\n return {\n filename: attachment.filename,\n content: attachment.content!,\n contentType: attachment.contentType,\n encoding: attachment.encoding,\n cid: attachment.cid,\n };\n}\n\n/**\n * Build nodemailer options from normalized mail\n */\nasync function buildNodemailerOptions(normalized: NormalizedMail): Promise<NodemailerOptions> {\n const options: NodemailerOptions = {\n to: normalized.to,\n // nodemailer encodes/escapes {name, address} objects itself\n from: normalized.from,\n subject: normalized.subject,\n priority: mapPriority(normalized.priority),\n };\n\n if (normalized.cc.length > 0) {\n options.cc = normalized.cc;\n }\n\n if (normalized.bcc.length > 0) {\n options.bcc = normalized.bcc;\n }\n\n if (normalized.replyTo) {\n options.replyTo = normalized.replyTo;\n }\n\n if (normalized.html) {\n options.html = normalized.html;\n }\n\n if (normalized.text) {\n options.text = normalized.text;\n }\n\n if (normalized.attachments.length > 0) {\n // Resolve all attachments (read files if needed)\n const resolvedAttachments = await Promise.all(\n normalized.attachments.map((att) => resolveAttachment(att as MailAttachment)),\n );\n\n options.attachments = resolvedAttachments.map((att) => ({\n filename: att.filename,\n content: att.content,\n contentType: att.contentType,\n encoding: att.encoding,\n cid: att.cid,\n }));\n }\n\n if (Object.keys(normalized.headers).length > 0) {\n options.headers = normalized.headers;\n }\n\n return options;\n}\n\n/**\n * Map a send failure to a MailError code, preferring nodemailer's `code` / `responseCode`\n */\nfunction detectErrorCode(error: unknown): MailError[\"code\"] {\n if (error instanceof MailError) {\n return error.code;\n }\n\n const { code, responseCode } = (error ?? {}) as { code?: string; responseCode?: number };\n\n if (code === \"EAUTH\" || responseCode === 535 || responseCode === 534) return \"AUTH_ERROR\";\n\n if ([\"ECONNECTION\", \"ECONNREFUSED\", \"ENOTFOUND\", \"ESOCKET\"].includes(code as string)) {\n return \"CONNECTION_ERROR\";\n }\n\n if ([\"ETIMEDOUT\", \"ETIMEOUT\", \"ECONNECTIONTIMEOUT\"].includes(code as string)) return \"TIMEOUT\";\n if (code === \"EENVELOPE\") return \"INVALID_ADDRESS\";\n if (responseCode && [421, 450, 451, 452].includes(responseCode)) return \"RATE_LIMIT\";\n\n const message = error instanceof Error ? error.message : \"\";\n\n if (message.includes(\"ECONNREFUSED\") || message.includes(\"ENOTFOUND\")) return \"CONNECTION_ERROR\";\n if (message.includes(\"authentication\") || message.includes(\"auth\")) return \"AUTH_ERROR\";\n if (message.includes(\"timeout\") || message.includes(\"ETIMEDOUT\")) return \"TIMEOUT\";\n if (message.includes(\"rate\") || message.includes(\"limit\")) return \"RATE_LIMIT\";\n\n return \"UNKNOWN\";\n}\n\n/**\n * Run per-mail event handler\n */\nasync function runMailEvent<T extends (...args: any[]) => any>(\n handler: T | undefined,\n ...args: Parameters<T>\n): Promise<ReturnType<T> | undefined> {\n if (!handler) {\n return undefined;\n }\n\n try {\n return await handler(...args);\n } catch (error) {\n log.error(\"mail\", \"event\", `Per-mail event handler error: ${error}`);\n return undefined;\n }\n}\n\n/**\n * Trigger both global and mail-specific events\n */\nasync function triggerEvents(\n mailId: string,\n event: \"beforeSending\" | \"sent\" | \"success\" | \"error\",\n ...args: any[]\n): Promise<any[]> {\n try {\n // Trigger global event (e.g., mail.success)\n const globalEventName =\n event === \"beforeSending\"\n ? \"BEFORE_SENDING\"\n : event === \"sent\"\n ? \"SENT\"\n : event === \"success\"\n ? \"SUCCESS\"\n : \"ERROR\";\n\n const globalResults = await mailEvents.trigger(\n globalEventName as keyof typeof MAIL_EVENTS,\n ...args,\n );\n\n // Trigger mail-specific event (e.g., mail.$id.success)\n const specificResults = await mailEvents.triggerForMail(mailId, event, ...args);\n\n return [...(globalResults || []), ...(specificResults || [])];\n } catch (error) {\n log.error(\"mail\", \"event\", `Event handler error: ${error}`);\n return [];\n }\n}\n\n/**\n * Send an email\n *\n * @param options Mail options including recipients, content, and configuration\n * @returns Result containing success status, message ID, and accepted/rejected recipients\n *\n * @example\n * ```typescript\n * // Basic usage\n * await sendMail({\n * to: \"user@example.com\",\n * subject: \"Hello\",\n * html: \"<p>World</p>\",\n * });\n *\n * // With React component\n * await sendMail({\n * to: \"user@example.com\",\n * subject: \"Welcome!\",\n * component: <WelcomeEmail name=\"John\" />,\n * });\n *\n * // Track specific mail with events\n * const mailId = generateMailId();\n * mailEvents.onMailSuccess(mailId, (mail, result) => {\n * console.log(\"This specific mail was sent!\");\n * });\n * await sendMail({ id: mailId, to: \"...\", subject: \"...\", html: \"...\" });\n * ```\n */\nexport async function sendMail(options: MailOptions): Promise<MailResult> {\n // Generate or use provided mail ID\n const mailId = options.id || generateMailId();\n\n // Render React component if provided\n if (options.component) {\n try {\n options.html = await renderReactMail(options.component);\n } catch (error) {\n const mailError = new MailError(\n `Failed to render React component: ${error instanceof Error ? error.message : \"Unknown error\"}`,\n \"RENDER_ERROR\",\n error instanceof Error ? error : undefined,\n );\n\n await runMailEvent(options.onError, options, mailError);\n await triggerEvents(mailId, \"error\", options, mailError);\n\n throw mailError;\n }\n }\n\n // Normalize mail\n const normalized = normalizeMail(options);\n const driver = normalized.config.driver || \"smtp\";\n\n // Run beforeSending event (per-mail, global, and mail-specific)\n const beforeResult = await runMailEvent(options.beforeSending, options);\n const globalBeforeResults = await triggerEvents(mailId, \"beforeSending\", options);\n\n // If any returns false, cancel sending\n if (beforeResult === false || globalBeforeResults.some((r) => r === false)) {\n log.info(`mail.${driver}`, \"cancelled\", \"Mail sending cancelled by beforeSending event\");\n\n const result: MailResult = {\n success: false,\n accepted: [],\n rejected: normalized.to,\n response: \"Cancelled by beforeSending event\",\n };\n\n return result;\n }\n\n // Test mode - capture without sending\n if (isTestMode()) {\n log.info(`mail.${driver}`, \"test\", `[TEST MODE] Captured mail to: ${normalized.to.join(\", \")}`);\n\n const result: MailResult = {\n success: true,\n messageId: `test-${mailId}@localhost`,\n accepted: normalized.to,\n rejected: [],\n response: \"Test mode - mail captured\",\n };\n\n const captured: CapturedMail = {\n options,\n normalized,\n timestamp: new Date(),\n result,\n };\n\n captureMail(captured);\n\n await runMailEvent(options.onSuccess, options, result);\n await triggerEvents(mailId, \"success\", options, result);\n await runMailEvent(options.onSent, options, result, null);\n await triggerEvents(mailId, \"sent\", options, result, null);\n\n return result;\n }\n\n // Development mode - log without sending\n if (isDevelopmentMode()) {\n log.info(`mail.${driver}`, \"dev\", `[DEV MODE] Would send mail to: ${normalized.to.join(\", \")}`);\n log.info(`mail.${driver}`, \"dev\", `Subject: ${normalized.subject}`);\n\n if (normalized.html) {\n log.info(`mail.${driver}`, \"dev\", `HTML length: ${normalized.html.length} chars`);\n }\n\n const result: MailResult = {\n success: true,\n messageId: `dev-${mailId}@localhost`,\n accepted: normalized.to,\n rejected: [],\n response: \"Development mode - mail logged\",\n };\n\n await runMailEvent(options.onSuccess, options, result);\n await triggerEvents(mailId, \"success\", options, result);\n await runMailEvent(options.onSent, options, result, null);\n await triggerEvents(mailId, \"sent\", options, result, null);\n\n return result;\n }\n\n // Production mode - actually send\n try {\n const config = resolveMailConfig(options);\n const mailer = await getMailer(config);\n const nodemailerOptions = await buildNodemailerOptions(normalized);\n\n log.info(`mail.${driver}`, \"send\", `Sending mail to: ${normalized.to.join(\", \")}`);\n\n const output = await mailer.sendMail(nodemailerOptions);\n\n const accepted = output.accepted || (output.messageId ? normalized.to : []);\n const rejected = output.rejected || [];\n\n if (accepted.length === 0) {\n throw new MailError(\n `All recipients were rejected: ${(rejected as string[]).join(\", \")}`,\n \"REJECTED\",\n );\n }\n\n const result: MailResult = {\n success: true,\n messageId: output.messageId,\n accepted: accepted as string[],\n rejected: rejected as string[],\n response: output.response,\n envelope: output.envelope,\n };\n\n if (rejected.length > 0) {\n log.warn(`mail.${driver}`, \"partial\", `Mail partially rejected: ${rejected.join(\", \")}`);\n }\n\n log.success(`mail.${driver}`, \"sent\", `Mail sent successfully (ID: ${result.messageId})`);\n await runMailEvent(options.onSuccess, options, result);\n await triggerEvents(mailId, \"success\", options, result);\n\n await runMailEvent(options.onSent, options, result, null);\n await triggerEvents(mailId, \"sent\", options, result, null);\n\n return result;\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : \"Unknown error\";\n const code = detectErrorCode(error);\n\n const mailError = new MailError(\n `Failed to send mail: ${errorMessage}`,\n code,\n error instanceof Error ? error : undefined,\n );\n\n log.error(`mail.${driver}`, \"error\", mailError.message);\n\n await runMailEvent(options.onError, options, mailError);\n await triggerEvents(mailId, \"error\", options, mailError);\n await runMailEvent(options.onSent, options, null, mailError);\n await triggerEvents(mailId, \"sent\", options, null, mailError);\n\n throw mailError;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"send-mail.mjs","names":[],"sources":["../../../../../../../core/src/mail/send-mail.ts"],"sourcesContent":["import { log } from \"@warlock.js/logger\";\nimport { readFile } from \"node:fs/promises\";\nimport type { Options as NodemailerOptions } from \"nodemailer/lib/mailer\";\nimport { isDevelopmentMode, isTestMode, resolveMailConfig } from \"./config\";\nimport { MAIL_EVENTS, generateMailId, mailEvents } from \"./events\";\nimport { getMailer } from \"./mailer-pool\";\nimport { renderReactMail } from \"./react-mail\";\nimport { captureMail } from \"./test-mailbox\";\nimport {\n MailError,\n type CapturedMail,\n type MailAttachment,\n type MailOptions,\n type MailPriority,\n type MailResult,\n type NormalizedMail,\n} from \"./types\";\n\n/**\n * Normalize recipients to array\n */\nfunction normalizeRecipients(recipients: string | string[] | undefined): string[] {\n if (!recipients) {\n return [];\n }\n\n return Array.isArray(recipients) ? recipients : [recipients];\n}\n\n/**\n * Map priority to nodemailer format\n */\nfunction mapPriority(priority: MailPriority): \"high\" | \"normal\" | \"low\" {\n return priority;\n}\n\n/**\n * Normalize mail options to internal format\n */\nfunction normalizeMail(options: MailOptions): NormalizedMail {\n const config = resolveMailConfig(options);\n\n return {\n to: normalizeRecipients(options.to),\n cc: normalizeRecipients(options.cc),\n bcc: normalizeRecipients(options.bcc),\n replyTo: options.replyTo,\n from: options.from || config.from || { name: \"No Reply\", address: \"noreply@localhost\" },\n subject: options.subject,\n html: options.html,\n text: options.text,\n attachments: options.attachments || [],\n priority: options.priority || \"normal\",\n headers: options.headers || {},\n tags: options.tags || [],\n correlationId: options.correlationId,\n config,\n };\n}\n\n/**\n * Resolve attachment - read file if path is provided\n */\nasync function resolveAttachment(attachment: MailAttachment): Promise<{\n filename: string;\n content: Buffer | string;\n contentType?: string;\n encoding?: string;\n cid?: string;\n}> {\n // If path is provided, read the file\n if (attachment.path) {\n try {\n const content = await readFile(attachment.path);\n return {\n filename: attachment.filename,\n content,\n contentType: attachment.contentType,\n encoding: attachment.encoding,\n cid: attachment.cid,\n };\n } catch (error) {\n throw new MailError(\n `Failed to read attachment file \"${attachment.path}\": ${error instanceof Error ? error.message : \"Unknown error\"}`,\n \"CONFIG_ERROR\",\n error instanceof Error ? error : undefined,\n );\n }\n }\n\n // Already has content\n return {\n filename: attachment.filename,\n content: attachment.content!,\n contentType: attachment.contentType,\n encoding: attachment.encoding,\n cid: attachment.cid,\n };\n}\n\n/**\n * Build nodemailer options from normalized mail\n */\nasync function buildNodemailerOptions(normalized: NormalizedMail): Promise<NodemailerOptions> {\n const options: NodemailerOptions = {\n to: normalized.to,\n // nodemailer encodes/escapes {name, address} objects itself\n from: normalized.from,\n subject: normalized.subject,\n priority: mapPriority(normalized.priority),\n };\n\n if (normalized.cc.length > 0) {\n options.cc = normalized.cc;\n }\n\n if (normalized.bcc.length > 0) {\n options.bcc = normalized.bcc;\n }\n\n if (normalized.replyTo) {\n options.replyTo = normalized.replyTo;\n }\n\n if (normalized.html) {\n options.html = normalized.html;\n }\n\n if (normalized.text) {\n options.text = normalized.text;\n }\n\n if (normalized.attachments.length > 0) {\n // Resolve all attachments (read files if needed)\n const resolvedAttachments = await Promise.all(\n normalized.attachments.map((att) => resolveAttachment(att as MailAttachment)),\n );\n\n options.attachments = resolvedAttachments.map((att) => ({\n filename: att.filename,\n content: att.content,\n contentType: att.contentType,\n encoding: att.encoding,\n cid: att.cid,\n }));\n }\n\n if (Object.keys(normalized.headers).length > 0) {\n options.headers = normalized.headers;\n }\n\n return options;\n}\n\n/**\n * Map a send failure to a MailError code, preferring nodemailer's `code` / `responseCode`\n */\nfunction detectErrorCode(error: unknown): MailError[\"code\"] {\n if (error instanceof MailError) {\n return error.code;\n }\n\n const { code, responseCode } = (error ?? {}) as { code?: string; responseCode?: number };\n\n if (code === \"EAUTH\" || responseCode === 535 || responseCode === 534) return \"AUTH_ERROR\";\n\n if ([\"ECONNECTION\", \"ECONNREFUSED\", \"ENOTFOUND\", \"ESOCKET\"].includes(code as string)) {\n return \"CONNECTION_ERROR\";\n }\n\n if ([\"ETIMEDOUT\", \"ETIMEOUT\", \"ECONNECTIONTIMEOUT\"].includes(code as string)) return \"TIMEOUT\";\n if (code === \"EENVELOPE\") return \"INVALID_ADDRESS\";\n if (responseCode && [421, 450, 451, 452].includes(responseCode)) return \"RATE_LIMIT\";\n\n const message = error instanceof Error ? error.message : \"\";\n\n if (message.includes(\"ECONNREFUSED\") || message.includes(\"ENOTFOUND\")) return \"CONNECTION_ERROR\";\n if (message.includes(\"authentication\") || message.includes(\"auth\")) return \"AUTH_ERROR\";\n if (message.includes(\"timeout\") || message.includes(\"ETIMEDOUT\")) return \"TIMEOUT\";\n if (message.includes(\"rate\") || message.includes(\"limit\")) return \"RATE_LIMIT\";\n\n return \"UNKNOWN\";\n}\n\n/**\n * Run per-mail event handler\n */\nasync function runMailEvent<T extends (...args: any[]) => any>(\n handler: T | undefined,\n ...args: Parameters<T>\n): Promise<ReturnType<T> | undefined> {\n if (!handler) {\n return undefined;\n }\n\n try {\n return await handler(...args);\n } catch (error) {\n log.error(\"mail\", \"event\", `Per-mail event handler error: ${error}`);\n return undefined;\n }\n}\n\n/**\n * Trigger both global and mail-specific events\n */\nasync function triggerEvents(\n mailId: string,\n event: \"beforeSending\" | \"sent\" | \"success\" | \"error\",\n ...args: any[]\n): Promise<any[]> {\n try {\n // Trigger global event (e.g., mail.success)\n const globalEventName =\n event === \"beforeSending\"\n ? \"BEFORE_SENDING\"\n : event === \"sent\"\n ? \"SENT\"\n : event === \"success\"\n ? \"SUCCESS\"\n : \"ERROR\";\n\n const globalResults = await mailEvents.trigger(\n globalEventName as keyof typeof MAIL_EVENTS,\n ...args,\n );\n\n // Trigger mail-specific event (e.g., mail.$id.success)\n const specificResults = await mailEvents.triggerForMail(mailId, event, ...args);\n\n return [...(globalResults || []), ...(specificResults || [])];\n } catch (error) {\n log.error(\"mail\", \"event\", `Event handler error: ${error}`);\n return [];\n }\n}\n\n/**\n * Notify development and test tooling of a captured mail without allowing a\n * synchronous listener failure to interrupt delivery.\n */\nfunction triggerCapturedMail(captured: CapturedMail): void {\n try {\n mailEvents.trigger(\"CAPTURED\", captured);\n } catch (error) {\n log.error(\"mail\", \"event\", `Captured mail event handler error: ${error}`);\n }\n}\n\n/**\n * Send an email\n *\n * @param options Mail options including recipients, content, and configuration\n * @returns Result containing success status, message ID, and accepted/rejected recipients\n *\n * @example\n * ```typescript\n * // Basic usage\n * await sendMail({\n * to: \"user@example.com\",\n * subject: \"Hello\",\n * html: \"<p>World</p>\",\n * });\n *\n * // With React component\n * await sendMail({\n * to: \"user@example.com\",\n * subject: \"Welcome!\",\n * component: <WelcomeEmail name=\"John\" />,\n * });\n *\n * // Track specific mail with events\n * const mailId = generateMailId();\n * mailEvents.onMailSuccess(mailId, (mail, result) => {\n * console.log(\"This specific mail was sent!\");\n * });\n * await sendMail({ id: mailId, to: \"...\", subject: \"...\", html: \"...\" });\n * ```\n */\nexport async function sendMail(options: MailOptions): Promise<MailResult> {\n // Generate or use provided mail ID\n const mailId = options.id || generateMailId();\n\n // Render React component if provided\n if (options.component) {\n try {\n options.html = await renderReactMail(options.component);\n } catch (error) {\n const mailError = new MailError(\n `Failed to render React component: ${error instanceof Error ? error.message : \"Unknown error\"}`,\n \"RENDER_ERROR\",\n error instanceof Error ? error : undefined,\n );\n\n await runMailEvent(options.onError, options, mailError);\n await triggerEvents(mailId, \"error\", options, mailError);\n\n throw mailError;\n }\n }\n\n // Normalize mail\n const normalized = normalizeMail(options);\n const driver = normalized.config.driver || \"smtp\";\n\n // Run beforeSending event (per-mail, global, and mail-specific)\n const beforeResult = await runMailEvent(options.beforeSending, options);\n const globalBeforeResults = await triggerEvents(mailId, \"beforeSending\", options);\n\n // If any returns false, cancel sending\n if (beforeResult === false || globalBeforeResults.some((r) => r === false)) {\n log.info(`mail.${driver}`, \"cancelled\", \"Mail sending cancelled by beforeSending event\");\n\n const result: MailResult = {\n success: false,\n accepted: [],\n rejected: normalized.to,\n response: \"Cancelled by beforeSending event\",\n };\n\n return result;\n }\n\n // Test mode - capture without sending\n if (isTestMode()) {\n log.info(`mail.${driver}`, \"test\", `[TEST MODE] Captured mail to: ${normalized.to.join(\", \")}`);\n\n const result: MailResult = {\n success: true,\n messageId: `test-${mailId}@localhost`,\n accepted: normalized.to,\n rejected: [],\n response: \"Test mode - mail captured\",\n };\n\n const captured: CapturedMail = {\n id: mailId,\n options,\n normalized,\n timestamp: new Date(),\n result,\n };\n\n captureMail(captured);\n triggerCapturedMail(captured);\n\n await runMailEvent(options.onSuccess, options, result);\n await triggerEvents(mailId, \"success\", options, result);\n await runMailEvent(options.onSent, options, result, null);\n await triggerEvents(mailId, \"sent\", options, result, null);\n\n return result;\n }\n\n // Development mode - log without sending\n if (isDevelopmentMode()) {\n log.info(`mail.${driver}`, \"dev\", `[DEV MODE] Would send mail to: ${normalized.to.join(\", \")}`);\n log.info(`mail.${driver}`, \"dev\", `Subject: ${normalized.subject}`);\n\n if (normalized.html) {\n log.info(`mail.${driver}`, \"dev\", `HTML length: ${normalized.html.length} chars`);\n }\n\n const result: MailResult = {\n success: true,\n messageId: `dev-${mailId}@localhost`,\n accepted: normalized.to,\n rejected: [],\n response: \"Development mode - mail logged\",\n };\n\n const captured: CapturedMail = {\n id: mailId,\n options,\n normalized,\n timestamp: new Date(),\n result,\n };\n\n triggerCapturedMail(captured);\n\n await runMailEvent(options.onSuccess, options, result);\n await triggerEvents(mailId, \"success\", options, result);\n await runMailEvent(options.onSent, options, result, null);\n await triggerEvents(mailId, \"sent\", options, result, null);\n\n return result;\n }\n\n // Production mode - actually send\n try {\n const config = resolveMailConfig(options);\n const mailer = await getMailer(config);\n const nodemailerOptions = await buildNodemailerOptions(normalized);\n\n log.info(`mail.${driver}`, \"send\", `Sending mail to: ${normalized.to.join(\", \")}`);\n\n const output = await mailer.sendMail(nodemailerOptions);\n\n const accepted = output.accepted || (output.messageId ? normalized.to : []);\n const rejected = output.rejected || [];\n\n if (accepted.length === 0) {\n throw new MailError(\n `All recipients were rejected: ${(rejected as string[]).join(\", \")}`,\n \"REJECTED\",\n );\n }\n\n const result: MailResult = {\n success: true,\n messageId: output.messageId,\n accepted: accepted as string[],\n rejected: rejected as string[],\n response: output.response,\n envelope: output.envelope,\n };\n\n if (rejected.length > 0) {\n log.warn(`mail.${driver}`, \"partial\", `Mail partially rejected: ${rejected.join(\", \")}`);\n }\n\n log.success(`mail.${driver}`, \"sent\", `Mail sent successfully (ID: ${result.messageId})`);\n await runMailEvent(options.onSuccess, options, result);\n await triggerEvents(mailId, \"success\", options, result);\n\n await runMailEvent(options.onSent, options, result, null);\n await triggerEvents(mailId, \"sent\", options, result, null);\n\n return result;\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : \"Unknown error\";\n const code = detectErrorCode(error);\n\n const mailError = new MailError(\n `Failed to send mail: ${errorMessage}`,\n code,\n error instanceof Error ? error : undefined,\n );\n\n log.error(`mail.${driver}`, \"error\", mailError.message);\n\n await runMailEvent(options.onError, options, mailError);\n await triggerEvents(mailId, \"error\", options, mailError);\n await runMailEvent(options.onSent, options, null, mailError);\n await triggerEvents(mailId, \"sent\", options, null, mailError);\n\n throw mailError;\n }\n}\n"],"mappings":"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"}
|
package/esm/mail/types.d.mts
CHANGED
|
@@ -245,9 +245,13 @@ type NormalizedMail = {
|
|
|
245
245
|
config: MailConfigurations;
|
|
246
246
|
};
|
|
247
247
|
/**
|
|
248
|
-
* Captured mail for test
|
|
248
|
+
* Captured mail for development and test modes.
|
|
249
|
+
*
|
|
250
|
+
* Attachments retain their normalized contents so test assertions can inspect
|
|
251
|
+
* the exact mail that would be sent.
|
|
249
252
|
*/
|
|
250
253
|
type CapturedMail = {
|
|
254
|
+
/** The mail ID used for event namespacing and result correlation. */id: string;
|
|
251
255
|
options: MailOptions;
|
|
252
256
|
normalized: NormalizedMail;
|
|
253
257
|
timestamp: Date;
|
package/esm/mail/types.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../core/src/mail/types.ts"],"mappings":";;;;;;AAMA;KAAY,iBAAA;EACV,MAAA;EACA,WAAA;EACA,eAAA;EACA,MAAA;EACA,IAAA,GAAO,WAAW;AAAA;;;;KAMR,kBAAA,GAAqB,kBAAA,GAAqB,iBAAiB;AAAvE;;;AAAA,KAKY,QAAA;AAL2D;AAKvE;;AALuE,KAU3D,YAAA;;AALQ;AAKpB;KAKY,WAAA;EAGN,IAAA;EACA,OAAO;AAAA;AAJb;;;;AAAA,KAWY,cAAA;EACV,QAAA;EACA,WAAA;EACA,QAAA;EACA,GAAA;AAAA;EAGI,OAAA,EAAS,MAAM;EACf,IAAA;AAAA;EAGA,OAAA;EACA,IAAA;AAAA;;;;AAAI;AAqEV;;;;;;;;;;;;;AAwBoB;AAMpB;;;;;;;;;;;;;;AAQ6C;AAM7C;;;;;;;;;;;;;AAQM;AAON;;;;AAAyB;AAezB;;;;;;;;;;;;;KA1EY,kBAAA,GAAqB,aAAA,CAAc,OAAA;EA+FR;;;EA3FrC,MAAA;EA2EA;;;;EAtEA,GAAA;EA0EA;;;;EArEA,QAAA;EAwES;;;;EAnET,QAAA;EAwEa;;;;EAnEb,IAAA,GAAO,WAAW;AAAA;;;;KAMR,aAAA;EAiEuD;AAAA;AAMnE;EAnEE,OAAA,GAAU,kBAAA;;;;EAIV,OAAA,GAAU,MAAA,SAAe,kBAAA;AAAA;;;;KAMf,UAAA;EACV,OAAA;EACA,SAAA;EACA,QAAA;EACA,QAAA;EACA,QAAA;EACA,QAAA;IACE,IAAA;IACA,EAAA;EAAA;AAAA;;;;KAOQ,aAAA;;;;;KAeA,UAAA;EA8DV;;;;EAzDA,aAAA,IAAiB,IAAA,EAAM,WAAA,YAAuB,OAAA,iBAAwB,OAAA;EA8DtE;;;EA1DA,MAAA,IACE,IAAA,EAAM,WAAA,EACN,MAAA,EAAQ,UAAA,SACR,KAAA,EAAO,SAAA,mBACG,OAAA;EA4DF;;;EAxDV,SAAA,IAAa,IAAA,EAAM,WAAA,EAAa,MAAA,EAAQ,UAAA,YAAsB,OAAA;EAiEjD;;;EA7Db,OAAA,IAAW,IAAA,EAAM,WAAA,EAAa,KAAA,EAAO,SAAA,YAAqB,OAAA;AAAA;;;;KAMhD,WAAA;EAkDV;;;;;;;;;;;EAtCA,EAAA;EAGA,EAAA;EACA,EAAA;EACA,GAAA;EACA,OAAA;EAGA,OAAA;EACA,IAAA;EACA,IAAA;EACA,SAAA,GAAY,KAAA,CAAM,YAAA;EAGlB,MAAA,GAAS,kBAAA;EACT,MAAA;EACA,IAAA,GAAO,WAAA;EAGP,WAAA,GAAc,cAAA;EAGd,QAAA,GAAW,YAAA;EACX,OAAA,GAAU,MAAA;EACV,IAAA;EAGA,aAAA;AAAA,IACE,UAAA;;;;KAKQ,cAAA;EACV,EAAA;EACA,EAAA;EACA,GAAA;EACA,OAAA;EACA,IAAA,EAAM,WAAA;EACN,OAAA;EACA,IAAA;EACA,IAAA;EACA,WAAA,EAAa,cAAA;EACb,QAAA,EAAU,YAAA;EACV,OAAA,EAAS,MAAA;EACT,IAAA;EACA,aAAA;EACA,MAAA,EAAQ,kBAAA;AAAA
|
|
1
|
+
{"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../core/src/mail/types.ts"],"mappings":";;;;;;AAMA;KAAY,iBAAA;EACV,MAAA;EACA,WAAA;EACA,eAAA;EACA,MAAA;EACA,IAAA,GAAO,WAAW;AAAA;;;;KAMR,kBAAA,GAAqB,kBAAA,GAAqB,iBAAiB;AAAvE;;;AAAA,KAKY,QAAA;AAL2D;AAKvE;;AALuE,KAU3D,YAAA;;AALQ;AAKpB;KAKY,WAAA;EAGN,IAAA;EACA,OAAO;AAAA;AAJb;;;;AAAA,KAWY,cAAA;EACV,QAAA;EACA,WAAA;EACA,QAAA;EACA,GAAA;AAAA;EAGI,OAAA,EAAS,MAAM;EACf,IAAA;AAAA;EAGA,OAAA;EACA,IAAA;AAAA;;;;AAAI;AAqEV;;;;;;;;;;;;;AAwBoB;AAMpB;;;;;;;;;;;;;;AAQ6C;AAM7C;;;;;;;;;;;;;AAQM;AAON;;;;AAAyB;AAezB;;;;;;;;;;;;;KA1EY,kBAAA,GAAqB,aAAA,CAAc,OAAA;EA+FR;;;EA3FrC,MAAA;EA2EA;;;;EAtEA,GAAA;EA0EA;;;;EArEA,QAAA;EAwES;;;;EAnET,QAAA;EAwEa;;;;EAnEb,IAAA,GAAO,WAAW;AAAA;;;;KAMR,aAAA;EAiEuD;AAAA;AAMnE;EAnEE,OAAA,GAAU,kBAAA;;;;EAIV,OAAA,GAAU,MAAA,SAAe,kBAAA;AAAA;;;;KAMf,UAAA;EACV,OAAA;EACA,SAAA;EACA,QAAA;EACA,QAAA;EACA,QAAA;EACA,QAAA;IACE,IAAA;IACA,EAAA;EAAA;AAAA;;;;KAOQ,aAAA;;;;;KAeA,UAAA;EA8DV;;;;EAzDA,aAAA,IAAiB,IAAA,EAAM,WAAA,YAAuB,OAAA,iBAAwB,OAAA;EA8DtE;;;EA1DA,MAAA,IACE,IAAA,EAAM,WAAA,EACN,MAAA,EAAQ,UAAA,SACR,KAAA,EAAO,SAAA,mBACG,OAAA;EA4DF;;;EAxDV,SAAA,IAAa,IAAA,EAAM,WAAA,EAAa,MAAA,EAAQ,UAAA,YAAsB,OAAA;EAiEjD;;;EA7Db,OAAA,IAAW,IAAA,EAAM,WAAA,EAAa,KAAA,EAAO,SAAA,YAAqB,OAAA;AAAA;;;;KAMhD,WAAA;EAkDV;;;;;;;;;;;EAtCA,EAAA;EAGA,EAAA;EACA,EAAA;EACA,GAAA;EACA,OAAA;EAGA,OAAA;EACA,IAAA;EACA,IAAA;EACA,SAAA,GAAY,KAAA,CAAM,YAAA;EAGlB,MAAA,GAAS,kBAAA;EACT,MAAA;EACA,IAAA,GAAO,WAAA;EAGP,WAAA,GAAc,cAAA;EAGd,QAAA,GAAW,YAAA;EACX,OAAA,GAAU,MAAA;EACV,IAAA;EAGA,aAAA;AAAA,IACE,UAAA;;;;KAKQ,cAAA;EACV,EAAA;EACA,EAAA;EACA,GAAA;EACA,OAAA;EACA,IAAA,EAAM,WAAA;EACN,OAAA;EACA,IAAA;EACA,IAAA;EACA,WAAA,EAAa,cAAA;EACb,QAAA,EAAU,YAAA;EACV,OAAA,EAAS,MAAA;EACT,IAAA;EACA,aAAA;EACA,MAAA,EAAQ,kBAAA;AAAA;;;;;;;KASE,YAAA;EAesB,qEAbhC,EAAA;EACA,OAAA,EAAS,WAAA;EACT,UAAA,EAAY,cAAA;EACZ,SAAA,EAAW,IAAA;EACX,MAAA,GAAS,UAAA;EACT,KAAA,GAAQ,SAAA;AAAA;AAUsE;;;AAAA,cAJnE,SAAA,SAAkB,KAAA;EAAA,SACb,IAAA,EAAM,aAAA;EAAA,SACN,aAAA,GAAgB,KAAA;cAEb,OAAA,UAAiB,IAAA,EAAM,aAAA,EAAe,aAAA,GAAgB,KAAA;AAAA"}
|