@warlock.js/core 5.25.0 → 5.27.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 +53 -0
- 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/generate/generators/module.generator.mjs +1 -12
- package/esm/cli/commands/generate/generators/module.generator.mjs.map +1 -1
- package/esm/cli/commands/openapi-generator.action.mjs +43 -0
- package/esm/cli/commands/openapi-generator.action.mjs.map +1 -0
- package/esm/cli/commands/openapi-generator.command.mjs +44 -0
- package/esm/cli/commands/openapi-generator.command.mjs.map +1 -0
- package/esm/cli/commands/typings-generator.command.mjs +4 -1
- package/esm/cli/commands/typings-generator.command.mjs.map +1 -1
- package/esm/cli/framework-cli-commands.mjs +2 -0
- package/esm/cli/framework-cli-commands.mjs.map +1 -1
- package/esm/database/migrate-action.mjs +2 -2
- package/esm/database/migrate-action.mjs.map +1 -1
- package/esm/dev-server/build-development-openapi-document.mjs +36 -0
- package/esm/dev-server/build-development-openapi-document.mjs.map +1 -0
- package/esm/dev-server/collect-development-api-routes.mjs +27 -0
- package/esm/dev-server/collect-development-api-routes.mjs.map +1 -0
- package/esm/dev-server/dev-logger.mjs +9 -1
- package/esm/dev-server/dev-logger.mjs.map +1 -1
- package/esm/dev-server/development-server.mjs +6 -3
- package/esm/dev-server/development-server.mjs.map +1 -1
- package/esm/dev-server/files-orchestrator.mjs +17 -1
- package/esm/dev-server/files-orchestrator.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/model-resource-typings.mjs +246 -0
- package/esm/dev-server/model-resource-typings.mjs.map +1 -0
- package/esm/dev-server/module-loader.mjs +16 -0
- package/esm/dev-server/module-loader.mjs.map +1 -1
- package/esm/dev-server/special-file-patterns.mjs +2 -2
- package/esm/dev-server/special-file-patterns.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 +73 -6
- package/esm/dev-server/type-generator.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/bull-board.feature.mjs +2 -2
- package/esm/generations/features/bull-board.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/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 +1 -1
- 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.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 +9 -26
- 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.map +1 -1
- package/esm/http/response.mjs +4 -2
- package/esm/http/response.mjs.map +1 -1
- package/esm/http/serialized.d.mts +114 -0
- package/esm/http/serialized.d.mts.map +1 -0
- package/esm/index.d.mts +12 -5
- package/esm/index.mjs +8 -2
- 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/openapi/build-openapi-document.d.mts +17 -0
- package/esm/openapi/build-openapi-document.d.mts.map +1 -0
- package/esm/openapi/build-openapi-document.mjs +124 -0
- package/esm/openapi/build-openapi-document.mjs.map +1 -0
- package/esm/openapi/cast-schema.mjs +89 -0
- package/esm/openapi/cast-schema.mjs.map +1 -0
- package/esm/openapi/collect-openapi-document.mjs +40 -0
- package/esm/openapi/collect-openapi-document.mjs.map +1 -0
- package/esm/openapi/development-openapi-provider.d.mts +17 -0
- package/esm/openapi/development-openapi-provider.d.mts.map +1 -0
- package/esm/openapi/development-openapi-provider.mjs +21 -0
- package/esm/openapi/development-openapi-provider.mjs.map +1 -0
- package/esm/openapi/error-responses.mjs +67 -0
- package/esm/openapi/error-responses.mjs.map +1 -0
- package/esm/openapi/index.d.mts +3 -0
- package/esm/openapi/index.mjs +4 -0
- package/esm/openapi/openapi-child-output.mjs +57 -0
- package/esm/openapi/openapi-child-output.mjs.map +1 -0
- package/esm/openapi/openapi-protocol.mjs +30 -0
- package/esm/openapi/openapi-protocol.mjs.map +1 -0
- package/esm/openapi/paths.mjs +63 -0
- package/esm/openapi/paths.mjs.map +1 -0
- package/esm/openapi/request-schemas.mjs +138 -0
- package/esm/openapi/request-schemas.mjs.map +1 -0
- package/esm/openapi/response-schemas.mjs +85 -0
- package/esm/openapi/response-schemas.mjs.map +1 -0
- package/esm/openapi/schema-registry.mjs +143 -0
- package/esm/openapi/schema-registry.mjs.map +1 -0
- package/esm/openapi/security.mjs +98 -0
- package/esm/openapi/security.mjs.map +1 -0
- package/esm/openapi/types.d.mts +119 -0
- package/esm/openapi/types.d.mts.map +1 -0
- package/esm/production/production-builder.mjs +11 -2
- package/esm/production/production-builder.mjs.map +1 -1
- package/esm/production/registration-child-runner.mjs +139 -0
- package/esm/production/registration-child-runner.mjs.map +1 -0
- package/esm/production/route-registration-child.mjs +30 -2
- package/esm/production/route-registration-child.mjs.map +1 -1
- package/esm/production/route-registration-snapshot.mjs +22 -119
- package/esm/production/route-registration-snapshot.mjs.map +1 -1
- package/esm/resource/define-resource.d.mts +44 -14
- package/esm/resource/define-resource.d.mts.map +1 -1
- package/esm/resource/define-resource.mjs +0 -40
- package/esm/resource/define-resource.mjs.map +1 -1
- package/esm/resource/index.d.mts +1 -1
- package/esm/resource/resource.d.mts +4 -4
- package/esm/resource/resource.d.mts.map +1 -1
- package/esm/resource/resource.mjs.map +1 -1
- package/esm/resource/types.d.mts +89 -4
- package/esm/resource/types.d.mts.map +1 -1
- package/esm/restful/restful.d.mts +5 -5
- package/esm/restful/restful.d.mts.map +1 -1
- package/esm/router/index.d.mts +1 -1
- package/esm/router/named-api-routes-with-responses.mjs +59 -0
- package/esm/router/named-api-routes-with-responses.mjs.map +1 -0
- package/esm/router/response-schema-types.mjs +100 -0
- package/esm/router/response-schema-types.mjs.map +1 -0
- package/esm/router/router.d.mts +2 -2
- package/esm/router/router.d.mts.map +1 -1
- package/esm/router/router.mjs +38 -11
- package/esm/router/router.mjs.map +1 -1
- package/esm/router/types.d.mts +24 -1
- package/esm/router/types.d.mts.map +1 -1
- 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/validation/validateAll.mjs +48 -1
- package/esm/validation/validateAll.mjs.map +1 -1
- package/llms-full.txt +383 -80
- package/llms.txt +4 -3
- package/package.json +21 -21
- package/skills/README.md +4 -3
- package/skills/create-controller/SKILL.md +43 -2
- package/skills/define-resource/SKILL.md +100 -1
- package/skills/generate-openapi/SKILL.md +120 -0
- package/skills/index.md +3 -3
- package/skills/test-http/SKILL.md +9 -1
- package/skills/use-localization/SKILL.md +39 -26
- package/skills/validate-input/SKILL.md +2 -2
package/esm/index.mjs
CHANGED
|
@@ -24,7 +24,7 @@ import { DEFAULT_CSP_DIRECTIVES, InvalidCspDirectiveError, applyCspHeader, build
|
|
|
24
24
|
import { HttpErrorCodes } from "./http/error-codes.mjs";
|
|
25
25
|
import { isAllowedCsrfOrigin, normalizeOrigin, originOf, ownOrigin, resolveCsrfOriginVerdict } from "./http/csrf-origin-policy.mjs";
|
|
26
26
|
import { requiresDefaultCsrfGuard, runDefaultCsrfGuard } from "./http/csrf-default-guard.mjs";
|
|
27
|
-
import { BadRequestError, ConflictError, CookieJarUnavailableError, ForbiddenError, HttpError, NotAcceptableError, NotAllowedError,
|
|
27
|
+
import { BadRequestError, ConflictError, CookieJarUnavailableError, ForbiddenError, HttpError, NotAcceptableError, NotAllowedError, ResourceNotFoundError, ServerError, UnAuthorizedError } from "./http/errors/errors.mjs";
|
|
28
28
|
import { deriveTraceId, parseTraceparentTraceId } from "./http/tracing/trace-id.mjs";
|
|
29
29
|
import { buildTracingContext, dispatchPhase, dispatchRequestEnd, dispatchRequestStart, isTracingEnabled, registerTracingHooks, resetTracingConfigForTests, resolveTracingConfig } from "./http/tracing/tracing-dispatcher.mjs";
|
|
30
30
|
import { createRequestStore, t } from "./http/middleware/inject-request-context.mjs";
|
|
@@ -147,6 +147,12 @@ import "./database/index.mjs";
|
|
|
147
147
|
import { decrypt, encrypt } from "./encryption/encrypt.mjs";
|
|
148
148
|
import { hmacHash } from "./encryption/hash.mjs";
|
|
149
149
|
import "./encryption/index.mjs";
|
|
150
|
+
import { LocaleDictionaryError, parseLocaleDictionary } from "./localization/parse-locale-dictionary.mjs";
|
|
151
|
+
import { registerGroupedTranslationKeys, registerLocaleDictionary, unregisterLocaleDictionary } from "./localization/register-locale-dictionary.mjs";
|
|
152
|
+
import "./localization/index.mjs";
|
|
153
|
+
import { getDevelopmentOpenApiDocument, registerDevelopmentOpenApiProvider } from "./openapi/development-openapi-provider.mjs";
|
|
154
|
+
import { OPENAPI_JSON_SCHEMA_DIALECT, buildOpenApiDocument } from "./openapi/build-openapi-document.mjs";
|
|
155
|
+
import "./openapi/index.mjs";
|
|
150
156
|
import { CascadeQueryBuilder } from "./repositories/adapters/cascade/cascade-query-builder.mjs";
|
|
151
157
|
import { CascadeAdapter, UPDATE_MANY_BATCH_SIZE } from "./repositories/adapters/cascade/cascade-adapter.mjs";
|
|
152
158
|
import { RepositoryManager } from "./repositories/repository.manager.mjs";
|
|
@@ -175,4 +181,4 @@ import { colors } from "@mongez/copper";
|
|
|
175
181
|
|
|
176
182
|
export * from "@mongez/localization"
|
|
177
183
|
|
|
178
|
-
export { $registerUseCase, $unregisterUseCase, APP_ROLES, AccessConnector, AiConnector, Application, BOOT_SIGNAL_ENV_KEY, BOOT_SIGNAL_VERSION, BadRequestError, BadSchemaUseCaseError, BaseConnector, BenchmarkProfiler, BenchmarkSnapshots, CLICommand, CacheConnector, CascadeAdapter, CascadeQueryBuilder, CloudDriver, ConfigSpecialHandlers, ConflictError, ConnectorLifecyclePhase, ConnectorPriority, ConnectorsManager, ConsoleChannel, CookieJarUnavailableError, DEFAULT_CSP_DIRECTIVES, DOSpacesDriver, DatabaseCacheDriver, DatabaseConnector, DatabaseLog, DatabaseLogModel, FileValidator, ForbiddenError, HeraldConnector, HttpConnector, HttpError, HttpErrorCodes, Image, ImageVariantsConfigError, InvalidCspDirectiveError, LocalDriver, LoggerConnector, MAIL_EVENTS, Mail, MailError, MailerConnector, MimeTypes, NoopChannel, NotAcceptableError, NotAllowedError, NotificationsConnector, Path, PortInUseError, Queue, R2Driver, RegisterResource, RepositoryManager, Request, RequestController, RequestLog,
|
|
184
|
+
export { $registerUseCase, $unregisterUseCase, APP_ROLES, AccessConnector, AiConnector, Application, BOOT_SIGNAL_ENV_KEY, BOOT_SIGNAL_VERSION, BadRequestError, BadSchemaUseCaseError, BaseConnector, BenchmarkProfiler, BenchmarkSnapshots, CLICommand, CacheConnector, CascadeAdapter, CascadeQueryBuilder, CloudDriver, ConfigSpecialHandlers, ConflictError, ConnectorLifecyclePhase, ConnectorPriority, ConnectorsManager, ConsoleChannel, CookieJarUnavailableError, DEFAULT_CSP_DIRECTIVES, DOSpacesDriver, DatabaseCacheDriver, DatabaseConnector, DatabaseLog, DatabaseLogModel, FileValidator, ForbiddenError, HeraldConnector, HttpConnector, HttpError, HttpErrorCodes, Image, ImageVariantsConfigError, InvalidCspDirectiveError, LocalDriver, LocaleDictionaryError, LoggerConnector, MAIL_EVENTS, Mail, MailError, MailerConnector, MimeTypes, NoopChannel, NotAcceptableError, NotAllowedError, NotificationsConnector, OPENAPI_JSON_SCHEMA_DIALECT, Path, PortInUseError, Queue, R2Driver, RegisterResource, RepositoryManager, Request, RequestController, RequestLog, Resource, ResourceFieldBuilder, ResourceNotFoundError, Response, ResponseStatus, Restful, RouteRegistry, Router, S3Driver, ScopedStorage, SeederDependencyCycleError, ServerError, SocketConnector, Storage, StorageCapabilityError, StorageConnector, StorageError, StorageFile, StorageNotInitializedError, UPDATE_MANY_BATCH_SIZE, UPLOADS_DEFAULTS, UnAuthorizedError, UnknownSeederDependencyError, UploadedFile, WarlockConfigManager, addUseCaseHistory, anyMatch, app, appLog, appPath, applyCspHeader, assertConfiguredHttpPortIsFree, assertMailCount, assertMailSent, assertNoReservedConnectorNames, assertPortIsAvailable, assertSitesRequireWebRole, assertUniqueConnectorNames, assetsUrl, bootstrap, broadcastUseCaseResult, buildCspHeaderValue, buildIdempotencyCacheKey, buildOpenApiDocument, buildTracingContext, cachePath, captureMail, clearPositionalHandlerSuspects, clearTestMailbox, closeAllMailers, closeMailer, closeServerWithTimeout, colors, command, config, configPath, configSpecialHandlers, connectorsManager, container, createHttpApplication, createRequestStore, decrypt, defaultHttpConfigurations, defineConfig, defineResource, deriveTraceId, describePositionalHandlerSuspect, dispatchPhase, dispatchRequestEnd, dispatchRequestStart, displayEnvironmentMode, encrypt, ensureFatalIsVisible, env, environment, existsExceptCurrentIdRule, existsExceptCurrentUserRule, fetchLatestVersion, fileExtensionRule, fileRule, fileTypeRule, findMailsBySubject, findMailsTo, fireLifecycleEvent, forgetPositionalHandlerSuspects, generateImageVariants, generateMailId, getDefaultMailConfig, getDevelopmentOpenApiDocument, getHttpReadyReport, getHttpServer, getLastMail, getLocalized, getMailEventName, getMailMode, getMailboxSize, getMailer, getMailerConfig, getMimeType, getPoolStats, getPublicUrl, getRoles, getSites, getSocketServer, getTestMailbox, getUseCase, getUseCaseHistory, getUseCases, globalEventsCallbacksMap, globalUseCasesEvents, hasRole, hashBody, hashPassword, health, hmacHash, httpConfig, imageRule, increaseUseCaseFailedCalls, increaseUseCaseSuccessCalls, inspectHandlerSignature, ipMatches, isAllowedCsrfOrigin, isBootSignal, isDevelopmentMode, isNewerVersion, isPortAvailable, isPrivateOrReservedIp, isProductionMode, isTestMode, isTracingEnabled, isUnknownTsExtensionError, isValidIdempotencyKey, listPositionalHandlerSuspects, loadS3, logResponse, logsPath, looksLikePositionalHandler, mailEvents, matchesDerivedRouteName, measure, mergeCspDirectives, middleware, normalizeOrigin, normalizeRequestPath, normalizeRoutePath, notifyHttpServerRebuilt, onHttpServerRebuilt, originOf, ownOrigin, parseLocaleDictionary, parseRoles, parseSites, parseSize, parseTraceparentTraceId, paths, preflightConfiguredHttpPort, promiseAllObject, publicPath, publicUrl, registerAppConfig, registerConfiguredConnectors, registerDevelopmentOpenApiProvider, registerGroupedTranslationKeys, registerHttpPlugins, registerLocaleDictionary, registerTracingHooks, remedyLines, renderReact, renderReactMail, reportPositionalHandlerSuspects, requestContext, requestMemo, requiresDefaultCsrfGuard, resetHttpReadyReport, resetMailConfig, resetRolesCacheForTests, resetTracingConfigForTests, resolveCspConfig, resolveCsrfOriginVerdict, resolveMailConfig, resolveTracingConfig, resolveWithinRoot, rootPath, routeNameMethodSuffix, router, runDefaultCsrfGuard, runPipeline, safeFetchToBuffer, sanitizePath, seeder, sendBootSignal, sendMail, serializeCspDirectives, servesHttp, setBaseUrl, setConfig, setEnvironment, setHttpReadyReport, setLogConfigurations, setMailConfigurations, setMailMode, shouldPreflightHttpPort, sleep, sluggable, srcPath, startHttpServer, stopHttpApplication, storage, storageConfig, storageConfigurations, storageDriverContext, storagePath, streamReactResponse, t, tempPath, toJson, uniqueExceptCurrentIdRule, uniqueExceptCurrentUserRule, unregisterLocaleDictionary, uploadedFileController, uploadedFileMetadataSchema, uploadsConfig, uploadsPath, uploadsUrl, url, useCase, useComputedModel, useComputedSlug, useCurrentUser, useHashedPassword, useRequest, useRequestStore, validateCspConfigAtBoot, validateCspDirectives, verifyMailer, verifyPassword, warlockConfigManager, warlockPath, wasMailSentTo, wasMailSentWithSubject, wrapResponseInDataKey };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
//#region ../core/src/localization/parse-locale-dictionary.d.ts
|
|
2
|
+
type LocaleDictionaryEntries = Record<string, Record<string, string>>;
|
|
3
|
+
type ParsedLocaleDictionary = {
|
|
4
|
+
sourceFile: string;
|
|
5
|
+
group: string;
|
|
6
|
+
entries: LocaleDictionaryEntries;
|
|
7
|
+
};
|
|
8
|
+
/** A source error that always names both the locale file and the offending JSON key. */
|
|
9
|
+
declare class LocaleDictionaryError extends Error {
|
|
10
|
+
readonly sourceFile: string;
|
|
11
|
+
readonly key: string;
|
|
12
|
+
constructor(sourceFile: string, key: string, detail: string);
|
|
13
|
+
}
|
|
14
|
+
/** Parses one locale dictionary source without reading the filesystem or loading application code. */
|
|
15
|
+
declare function parseLocaleDictionary(input: {
|
|
16
|
+
sourceFile: string;
|
|
17
|
+
source: string;
|
|
18
|
+
defaultNamespace: string;
|
|
19
|
+
localeCodes?: readonly string[];
|
|
20
|
+
}): ParsedLocaleDictionary;
|
|
21
|
+
//#endregion
|
|
22
|
+
export { LocaleDictionaryEntries, LocaleDictionaryError, ParsedLocaleDictionary, parseLocaleDictionary };
|
|
23
|
+
//# sourceMappingURL=parse-locale-dictionary.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parse-locale-dictionary.d.mts","names":[],"sources":["../../../../../../../core/src/localization/parse-locale-dictionary.ts"],"mappings":";KAAY,uBAAA,GAA0B,MAAM,SAAS,MAAA;AAAA,KAEzC,sBAAA;EACV,UAAA;EACA,KAAA;EACA,OAAA,EAAS,uBAAuB;AAAA;AAHlC;AAAA,cAca,qBAAA,SAA8B,KAAK;EAAA,SAE5B,UAAA;EAAA,SACA,GAAA;cADA,UAAA,UACA,GAAA,UAChB,MAAA;AAAA;;iBAgOY,qBAAA,CAAsB,KAAA;EACpC,UAAA;EACA,MAAA;EACA,gBAAA;EACA,WAAA;AAAA,IACE,sBAAsB"}
|
|
@@ -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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { OpenApiBuildResult, OpenApiContext, OpenApiRouteInput } from "./types.mjs";
|
|
2
|
+
|
|
3
|
+
//#region ../core/src/openapi/build-openapi-document.d.ts
|
|
4
|
+
/** `jsonSchemaDialect` of every document: OpenAPI 3.1 schemas are JSON Schema 2020-12. */
|
|
5
|
+
declare const OPENAPI_JSON_SCHEMA_DIALECT = "https://json-schema.org/draft/2020-12/schema";
|
|
6
|
+
/**
|
|
7
|
+
* Build an OpenAPI 3.1 document from registered routes.
|
|
8
|
+
*
|
|
9
|
+
* Pure: it reads the routes it is given (`router.list()` in the app), converts Seal schemas
|
|
10
|
+
* with `toJsonSchema("draft-2020-12")` and returns plain JSON plus the gaps it found as
|
|
11
|
+
* `warnings`. It never throws for a single route it cannot document; that route is
|
|
12
|
+
* documented as far as possible and a warning explains what was left out.
|
|
13
|
+
*/
|
|
14
|
+
declare function buildOpenApiDocument(routes: readonly OpenApiRouteInput[], context: OpenApiContext): OpenApiBuildResult;
|
|
15
|
+
//#endregion
|
|
16
|
+
export { OPENAPI_JSON_SCHEMA_DIALECT, buildOpenApiDocument };
|
|
17
|
+
//# sourceMappingURL=build-openapi-document.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-openapi-document.d.mts","names":[],"sources":["../../../../../../../core/src/openapi/build-openapi-document.ts"],"mappings":";;;;cA2Ba,2BAAA;AAAb;;;;AAAwC;AA8BxC;;;AA9BA,iBA8BgB,oBAAA,CACd,MAAA,WAAiB,iBAAA,IACjB,OAAA,EAAS,cAAA,GACR,kBAAA"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { buildDeclaredResponses } from "./response-schemas.mjs";
|
|
2
|
+
import { buildUnauthorizedResponse, buildValidationFailedResponse, resolveValidationResponse } from "./error-responses.mjs";
|
|
3
|
+
import { buildOperationId, expandMethods, firstSegmentTag, toOpenApiPath } from "./paths.mjs";
|
|
4
|
+
import { buildRequestParts } from "./request-schemas.mjs";
|
|
5
|
+
import { SchemaRegistry } from "./schema-registry.mjs";
|
|
6
|
+
import { SecuritySchemes, readRouteAuth } from "./security.mjs";
|
|
7
|
+
|
|
8
|
+
//#region ../core/src/openapi/build-openapi-document.ts
|
|
9
|
+
/** `jsonSchemaDialect` of every document: OpenAPI 3.1 schemas are JSON Schema 2020-12. */
|
|
10
|
+
const OPENAPI_JSON_SCHEMA_DIALECT = "https://json-schema.org/draft/2020-12/schema";
|
|
11
|
+
const UNSAFE_METHODS = [
|
|
12
|
+
"post",
|
|
13
|
+
"put",
|
|
14
|
+
"patch",
|
|
15
|
+
"delete"
|
|
16
|
+
];
|
|
17
|
+
function describeAuth(auth, method) {
|
|
18
|
+
const lines = [];
|
|
19
|
+
if (auth.userTypes.length > 0) lines.push(`Requires an authenticated user of type: ${auth.userTypes.join(", ")}.`);
|
|
20
|
+
if (auth.sources.some((source) => typeof source !== "string") && UNSAFE_METHODS.includes(method)) lines.push("When authenticating with the cookie, the request must also carry a same-origin Origin or Referer header (CSRF guard).");
|
|
21
|
+
return lines;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Build an OpenAPI 3.1 document from registered routes.
|
|
25
|
+
*
|
|
26
|
+
* Pure: it reads the routes it is given (`router.list()` in the app), converts Seal schemas
|
|
27
|
+
* with `toJsonSchema("draft-2020-12")` and returns plain JSON plus the gaps it found as
|
|
28
|
+
* `warnings`. It never throws for a single route it cannot document; that route is
|
|
29
|
+
* documented as far as possible and a warning explains what was left out.
|
|
30
|
+
*/
|
|
31
|
+
function buildOpenApiDocument(routes, context) {
|
|
32
|
+
const warnings = [];
|
|
33
|
+
const registry = new SchemaRegistry(context.resolveResourceName, (message) => warnings.push(message));
|
|
34
|
+
const security = new SecuritySchemes();
|
|
35
|
+
const validationShape = resolveValidationResponse(context.validationResponse);
|
|
36
|
+
const paths = {};
|
|
37
|
+
const tags = /* @__PURE__ */ new Set();
|
|
38
|
+
const operationIds = /* @__PURE__ */ new Set();
|
|
39
|
+
for (const route of routes) {
|
|
40
|
+
if (route.isPage && !context.includePages) continue;
|
|
41
|
+
const methods = expandMethods(route.method);
|
|
42
|
+
const label = `${String(route.method).toUpperCase()} ${route.path}`;
|
|
43
|
+
const warn = (message) => warnings.push(`${label}: ${message}`);
|
|
44
|
+
if (methods.length === 0) {
|
|
45
|
+
warn(`unsupported method "${String(route.method)}", route skipped.`);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
const handler = route.handler;
|
|
49
|
+
const validation = handler?.validation;
|
|
50
|
+
const auth = readRouteAuth(route.middleware, warn);
|
|
51
|
+
const tag = firstSegmentTag(route.path);
|
|
52
|
+
const pathKey = toOpenApiPath(route.path);
|
|
53
|
+
if (validation?.validate) warn("validation.validate is custom middleware and is not documented.");
|
|
54
|
+
for (const method of methods) {
|
|
55
|
+
paths[pathKey] ??= {};
|
|
56
|
+
if (paths[pathKey][method]) {
|
|
57
|
+
warn(`${method.toUpperCase()} ${pathKey} is already documented by an earlier route; this one is skipped.`);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
let operationId = buildOperationId(route.name, method, route.path, methods.length > 1);
|
|
61
|
+
if (operationIds.has(operationId)) {
|
|
62
|
+
let suffix = 2;
|
|
63
|
+
while (operationIds.has(`${operationId}_${suffix}`)) suffix++;
|
|
64
|
+
warn(`operationId "${operationId}" is already used; renamed to "${operationId}_${suffix}".`);
|
|
65
|
+
operationId = `${operationId}_${suffix}`;
|
|
66
|
+
}
|
|
67
|
+
operationIds.add(operationId);
|
|
68
|
+
const description = [route.description ?? handler?.description, ...auth ? describeAuth(auth, method) : []].filter((line) => typeof line === "string" && line.length > 0);
|
|
69
|
+
const request = buildRequestParts({
|
|
70
|
+
method,
|
|
71
|
+
path: route.path,
|
|
72
|
+
validation,
|
|
73
|
+
warn
|
|
74
|
+
});
|
|
75
|
+
const responses = route.isPage ? { "200": {
|
|
76
|
+
description: "HTML page",
|
|
77
|
+
content: { "text/html": { schema: { type: "string" } } }
|
|
78
|
+
} } : buildDeclaredResponses(handler?.responseSchema, registry, warn);
|
|
79
|
+
if (!Object.keys(responses).some((status) => status.startsWith("2"))) responses["200"] = { description: "Successful response" };
|
|
80
|
+
if (validation?.schema) {
|
|
81
|
+
const failed = buildValidationFailedResponse(validationShape, registry);
|
|
82
|
+
responses[failed.status] ??= failed.response;
|
|
83
|
+
}
|
|
84
|
+
if (auth) responses["401"] ??= buildUnauthorizedResponse(registry);
|
|
85
|
+
if (tag) tags.add(tag);
|
|
86
|
+
const operation = {
|
|
87
|
+
operationId,
|
|
88
|
+
...route.label ? { summary: route.label } : {},
|
|
89
|
+
...description.length > 0 ? { description: description.join("\n\n") } : {},
|
|
90
|
+
...tag ? { tags: [tag] } : {},
|
|
91
|
+
...request.parameters.length > 0 ? { parameters: request.parameters } : {},
|
|
92
|
+
...request.requestBody ? { requestBody: request.requestBody } : {},
|
|
93
|
+
responses: sortResponses(responses),
|
|
94
|
+
...auth ? { security: security.requirementsFor(auth) } : {}
|
|
95
|
+
};
|
|
96
|
+
paths[pathKey][method] = operation;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const document = {
|
|
100
|
+
openapi: "3.1.0",
|
|
101
|
+
jsonSchemaDialect: OPENAPI_JSON_SCHEMA_DIALECT,
|
|
102
|
+
info: { ...context.info },
|
|
103
|
+
...context.servers && context.servers.length > 0 ? { servers: context.servers.map((url) => ({ url })) } : {},
|
|
104
|
+
...tags.size > 0 ? { tags: [...tags].sort().map((name) => ({ name })) } : {},
|
|
105
|
+
paths
|
|
106
|
+
};
|
|
107
|
+
const hasSchemas = Object.keys(registry.schemas).length > 0;
|
|
108
|
+
if (hasSchemas || security.hasSchemes()) {
|
|
109
|
+
document.components = {};
|
|
110
|
+
if (hasSchemas) document.components.schemas = registry.schemas;
|
|
111
|
+
if (security.hasSchemes()) document.components.securitySchemes = security.schemes;
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
document,
|
|
115
|
+
warnings: [...new Set(warnings)]
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function sortResponses(responses) {
|
|
119
|
+
return Object.fromEntries(Object.entries(responses).sort(([first], [second]) => first.localeCompare(second)));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
//#endregion
|
|
123
|
+
export { OPENAPI_JSON_SCHEMA_DIALECT, buildOpenApiDocument };
|
|
124
|
+
//# sourceMappingURL=build-openapi-document.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-openapi-document.mjs","names":[],"sources":["../../../../../../../core/src/openapi/build-openapi-document.ts"],"sourcesContent":["import {\n buildUnauthorizedResponse,\n buildValidationFailedResponse,\n resolveValidationResponse,\n} from \"./error-responses\";\nimport {\n buildOperationId,\n expandMethods,\n firstSegmentTag,\n toOpenApiPath,\n type OpenApiMethod,\n} from \"./paths\";\nimport { buildRequestParts } from \"./request-schemas\";\nimport { buildDeclaredResponses } from \"./response-schemas\";\nimport { SchemaRegistry } from \"./schema-registry\";\nimport { readRouteAuth, SecuritySchemes } from \"./security\";\nimport type {\n AuthDescriptor,\n OpenApiBuildResult,\n OpenApiContext,\n OpenApiDocument,\n OpenApiOperation,\n OpenApiResponse,\n OpenApiRouteInput,\n} from \"./types\";\n\n/** `jsonSchemaDialect` of every document: OpenAPI 3.1 schemas are JSON Schema 2020-12. */\nexport const OPENAPI_JSON_SCHEMA_DIALECT = \"https://json-schema.org/draft/2020-12/schema\";\n\nconst UNSAFE_METHODS: readonly OpenApiMethod[] = [\"post\", \"put\", \"patch\", \"delete\"];\n\nfunction describeAuth(auth: AuthDescriptor, method: OpenApiMethod): string[] {\n const lines: string[] = [];\n\n if (auth.userTypes.length > 0) {\n lines.push(`Requires an authenticated user of type: ${auth.userTypes.join(\", \")}.`);\n }\n\n const usesCookie = auth.sources.some((source) => typeof source !== \"string\");\n\n if (usesCookie && UNSAFE_METHODS.includes(method)) {\n lines.push(\n \"When authenticating with the cookie, the request must also carry a same-origin Origin or Referer header (CSRF guard).\",\n );\n }\n\n return lines;\n}\n\n/**\n * Build an OpenAPI 3.1 document from registered routes.\n *\n * Pure: it reads the routes it is given (`router.list()` in the app), converts Seal schemas\n * with `toJsonSchema(\"draft-2020-12\")` and returns plain JSON plus the gaps it found as\n * `warnings`. It never throws for a single route it cannot document; that route is\n * documented as far as possible and a warning explains what was left out.\n */\nexport function buildOpenApiDocument(\n routes: readonly OpenApiRouteInput[],\n context: OpenApiContext,\n): OpenApiBuildResult {\n const warnings: string[] = [];\n const registry = new SchemaRegistry(context.resolveResourceName, (message) => warnings.push(message));\n const security = new SecuritySchemes();\n const validationShape = resolveValidationResponse(context.validationResponse);\n const paths: OpenApiDocument[\"paths\"] = {};\n const tags = new Set<string>();\n const operationIds = new Set<string>();\n\n for (const route of routes) {\n if (route.isPage && !context.includePages) {\n continue;\n }\n\n const methods = expandMethods(route.method);\n const label = `${String(route.method).toUpperCase()} ${route.path}`;\n const warn = (message: string) => warnings.push(`${label}: ${message}`);\n\n if (methods.length === 0) {\n warn(`unsupported method \"${String(route.method)}\", route skipped.`);\n\n continue;\n }\n\n const handler = route.handler as OpenApiRouteInput[\"handler\"] | undefined;\n const validation = handler?.validation;\n const auth = readRouteAuth(route.middleware, warn);\n const tag = firstSegmentTag(route.path);\n const pathKey = toOpenApiPath(route.path);\n\n if (validation?.validate) {\n warn(\"validation.validate is custom middleware and is not documented.\");\n }\n\n for (const method of methods) {\n paths[pathKey] ??= {};\n\n if (paths[pathKey][method]) {\n warn(`${method.toUpperCase()} ${pathKey} is already documented by an earlier route; this one is skipped.`);\n\n continue;\n }\n\n let operationId = buildOperationId(route.name, method, route.path, methods.length > 1);\n\n if (operationIds.has(operationId)) {\n let suffix = 2;\n\n while (operationIds.has(`${operationId}_${suffix}`)) {\n suffix++;\n }\n\n warn(`operationId \"${operationId}\" is already used; renamed to \"${operationId}_${suffix}\".`);\n operationId = `${operationId}_${suffix}`;\n }\n\n operationIds.add(operationId);\n\n const description = [\n route.description ?? handler?.description,\n ...(auth ? describeAuth(auth, method) : []),\n ].filter((line): line is string => typeof line === \"string\" && line.length > 0);\n\n const request = buildRequestParts({ method, path: route.path, validation, warn });\n\n const responses: Record<string, OpenApiResponse> = route.isPage\n ? { \"200\": { description: \"HTML page\", content: { \"text/html\": { schema: { type: \"string\" } } } } }\n : buildDeclaredResponses(handler?.responseSchema, registry, warn);\n\n if (!Object.keys(responses).some((status) => status.startsWith(\"2\"))) {\n responses[\"200\"] = { description: \"Successful response\" };\n }\n\n if (validation?.schema) {\n const failed = buildValidationFailedResponse(validationShape, registry);\n\n responses[failed.status] ??= failed.response;\n }\n\n if (auth) {\n responses[\"401\"] ??= buildUnauthorizedResponse(registry);\n }\n\n if (tag) {\n tags.add(tag);\n }\n\n // Built in reading order: identity, prose, inputs, outputs, security.\n const operation: OpenApiOperation = {\n operationId,\n ...(route.label ? { summary: route.label } : {}),\n ...(description.length > 0 ? { description: description.join(\"\\n\\n\") } : {}),\n ...(tag ? { tags: [tag] } : {}),\n ...(request.parameters.length > 0 ? { parameters: request.parameters } : {}),\n ...(request.requestBody ? { requestBody: request.requestBody } : {}),\n responses: sortResponses(responses),\n ...(auth ? { security: security.requirementsFor(auth) } : {}),\n };\n\n paths[pathKey][method] = operation;\n }\n }\n\n // Keys in reading order: header, servers, tags, paths, components.\n const document: OpenApiDocument = {\n openapi: \"3.1.0\",\n jsonSchemaDialect: OPENAPI_JSON_SCHEMA_DIALECT,\n info: { ...context.info },\n ...(context.servers && context.servers.length > 0\n ? { servers: context.servers.map((url) => ({ url })) }\n : {}),\n ...(tags.size > 0 ? { tags: [...tags].sort().map((name) => ({ name })) } : {}),\n paths,\n };\n\n const hasSchemas = Object.keys(registry.schemas).length > 0;\n\n if (hasSchemas || security.hasSchemes()) {\n document.components = {};\n\n if (hasSchemas) {\n document.components.schemas = registry.schemas;\n }\n\n if (security.hasSchemes()) {\n document.components.securitySchemes = security.schemes;\n }\n }\n\n return { document, warnings: [...new Set(warnings)] };\n}\n\nfunction sortResponses(responses: Record<string, OpenApiResponse>): Record<string, OpenApiResponse> {\n return Object.fromEntries(\n Object.entries(responses).sort(([first], [second]) => first.localeCompare(second)),\n );\n}\n"],"mappings":"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"}
|