@ultimat3/core 28.0.0 → 29.0.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/package.json +3 -3
- package/src/config-keys.ts +14 -0
- package/src/config-site.ts +28 -1
- package/src/config.ts +8 -0
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/core",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "29.0.0",
|
|
4
4
|
"description": "Ultimate's foundation: errors, context, env, config, clock, ids, logging, telemetry, lifecycle",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -30,13 +30,13 @@
|
|
|
30
30
|
"LICENSE"
|
|
31
31
|
],
|
|
32
32
|
"engines": {
|
|
33
|
-
"bun": ">=1.4.
|
|
33
|
+
"bun": ">=1.4.4"
|
|
34
34
|
},
|
|
35
35
|
"scripts": {
|
|
36
36
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
37
37
|
"test": "bun test"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@ultimat3/schema": "
|
|
40
|
+
"@ultimat3/schema": "29.0.0"
|
|
41
41
|
}
|
|
42
42
|
}
|
package/src/config-keys.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// at any depth and in any layer, is refused by path with the key it most likely meant. A key that
|
|
3
3
|
// is merged and read by nothing is the switch with no wire, and a typo (`drian`) is the same defect.
|
|
4
4
|
|
|
5
|
+
import type { JobsConfig } from './config';
|
|
5
6
|
import { BASE_FIX } from './config-fixes';
|
|
6
7
|
import type { MailRetainMimeConfig } from './config-mail';
|
|
7
8
|
import type { NavigationConfig, SpeculationConfig } from './config-navigation';
|
|
@@ -14,6 +15,7 @@ import type {
|
|
|
14
15
|
PwaShortcut,
|
|
15
16
|
PwaVapidConfig,
|
|
16
17
|
} from './config-pwa';
|
|
18
|
+
import type { SiteConfig } from './config-site';
|
|
17
19
|
import { ConfigInvalidError } from './errors';
|
|
18
20
|
import { isJsonObject } from './json-object';
|
|
19
21
|
import { nearestName } from './nearest-name';
|
|
@@ -60,6 +62,18 @@ export const SECTION_KEYS: Readonly<Record<string, readonly string[]>> = Object.
|
|
|
60
62
|
'pwa.shortcuts[].icons[]': keysOf<PwaImage>()(['src', 'sizes', 'type', 'purpose']),
|
|
61
63
|
'pwa.screenshots[]': keysOf<PwaScreenshot>()(['src', 'sizes', 'type', 'formFactor', 'label']),
|
|
62
64
|
'mail.retainMime': keysOf<MailRetainMimeConfig>()(['maxBytes']),
|
|
65
|
+
jobs: keysOf<JobsConfig>()([
|
|
66
|
+
'queues',
|
|
67
|
+
'concurrency',
|
|
68
|
+
'maxInFlight',
|
|
69
|
+
'maxAttempts',
|
|
70
|
+
'backoff',
|
|
71
|
+
'visibilityTimeoutMs',
|
|
72
|
+
'doneRetentionMs',
|
|
73
|
+
'deadRetentionMs',
|
|
74
|
+
'outboxRetentionMs',
|
|
75
|
+
]),
|
|
76
|
+
site: keysOf<SiteConfig>()(['origin', 'favicon']),
|
|
63
77
|
navigation: keysOf<NavigationConfig>()(['client', 'speculation']),
|
|
64
78
|
'navigation.speculation': keysOf<SpeculationConfig>()(['prefetch', 'exclude']),
|
|
65
79
|
});
|
package/src/config-site.ts
CHANGED
|
@@ -11,6 +11,8 @@ import {
|
|
|
11
11
|
} from './config-sitemap';
|
|
12
12
|
import { describeValue } from './error-render';
|
|
13
13
|
|
|
14
|
+
export type SiteFavicon = `assets/${string}.svg`;
|
|
15
|
+
|
|
14
16
|
export interface SiteConfig {
|
|
15
17
|
/**
|
|
16
18
|
* `https://www.example.com` — canonical, `og:url`, hreflang and the sitemap are absolute against
|
|
@@ -19,6 +21,11 @@ export interface SiteConfig {
|
|
|
19
21
|
* host or a preview domain.
|
|
20
22
|
*/
|
|
21
23
|
readonly origin: string | null;
|
|
24
|
+
/**
|
|
25
|
+
* The app-level SVG favicon, named as a site asset path (`assets/brand/icon.svg`). The renderer
|
|
26
|
+
* resolves it once at boot into the content-hashed URL every document carries.
|
|
27
|
+
*/
|
|
28
|
+
readonly favicon?: SiteFavicon | undefined;
|
|
22
29
|
}
|
|
23
30
|
|
|
24
31
|
/**
|
|
@@ -105,7 +112,7 @@ export interface SiteSectionsInput {
|
|
|
105
112
|
export function mergeSite(layers: readonly SiteSectionsInput[]): SiteSections {
|
|
106
113
|
return {
|
|
107
114
|
site: layered<SiteConfig>(
|
|
108
|
-
{ origin: null },
|
|
115
|
+
{ origin: null, favicon: undefined },
|
|
109
116
|
layers.map((layer) => layer.site),
|
|
110
117
|
),
|
|
111
118
|
seo: {
|
|
@@ -141,6 +148,21 @@ function originIssue(origin: string): string | undefined {
|
|
|
141
148
|
return undefined;
|
|
142
149
|
}
|
|
143
150
|
|
|
151
|
+
/** A favicon names one SVG under the app's site assets, never an arbitrary URL or path. */
|
|
152
|
+
function faviconIssue(favicon: string): string | undefined {
|
|
153
|
+
if (!favicon.startsWith('assets/')) {
|
|
154
|
+
return `site.favicon "${favicon}" must be a site asset path starting with assets/`;
|
|
155
|
+
}
|
|
156
|
+
if (!favicon.endsWith('.svg')) {
|
|
157
|
+
return `site.favicon "${favicon}" must name an SVG asset`;
|
|
158
|
+
}
|
|
159
|
+
const segments = favicon.split('/');
|
|
160
|
+
if (segments.some((segment) => segment === '' || segment === '.' || segment === '..')) {
|
|
161
|
+
return `site.favicon "${favicon}" has an empty, "." or ".." segment`;
|
|
162
|
+
}
|
|
163
|
+
return undefined;
|
|
164
|
+
}
|
|
165
|
+
|
|
144
166
|
/** Appends every refusal the two sections earn to `issues`, `config.ts`' one list. */
|
|
145
167
|
export function siteIssues(config: SiteSections, issues: string[]): void {
|
|
146
168
|
const origin = config.site.origin;
|
|
@@ -148,6 +170,11 @@ export function siteIssues(config: SiteSections, issues: string[]): void {
|
|
|
148
170
|
const issue = originIssue(origin);
|
|
149
171
|
if (issue !== undefined) issues.push(issue);
|
|
150
172
|
}
|
|
173
|
+
const favicon = config.site.favicon;
|
|
174
|
+
if (favicon !== undefined) {
|
|
175
|
+
const issue = faviconIssue(favicon);
|
|
176
|
+
if (issue !== undefined) issues.push(issue);
|
|
177
|
+
}
|
|
151
178
|
issues.push(...robotsPathIssues('seo.robots.disallow', config.seo.robots.disallow));
|
|
152
179
|
issues.push(...robotsGroupIssues(config.seo.robots.groups ?? []));
|
|
153
180
|
for (const path of config.seo.sitemap.extra as readonly unknown[]) {
|
package/src/config.ts
CHANGED
|
@@ -160,6 +160,11 @@ export interface JobsConfig {
|
|
|
160
160
|
* (`{ banks: 4, 'banks-long': 2 }`, a queue it does not name at `JOBS_CONCURRENCY_DEFAULT`).
|
|
161
161
|
*/
|
|
162
162
|
readonly concurrency: JobsConcurrency;
|
|
163
|
+
/**
|
|
164
|
+
* Runs one worker process holds at once across every queue it serves (`jobWorker`'s
|
|
165
|
+
* `maxInFlight`), the per-queue `concurrency` still bounding each under it. Unset: no process cap.
|
|
166
|
+
*/
|
|
167
|
+
readonly maxInFlight?: number | undefined;
|
|
163
168
|
readonly maxAttempts: number;
|
|
164
169
|
readonly backoff: (typeof JOB_BACKOFFS)[number];
|
|
165
170
|
readonly visibilityTimeoutMs: number;
|
|
@@ -283,6 +288,9 @@ function validate(config: AppConfig): void {
|
|
|
283
288
|
const perKey: readonly (string | undefined)[] = [
|
|
284
289
|
...config.roles.map((role) => oneOfIssue('roles', role, ROLES)),
|
|
285
290
|
...jobsConcurrencyIssues(config.jobs.concurrency),
|
|
291
|
+
config.jobs.maxInFlight === undefined
|
|
292
|
+
? undefined
|
|
293
|
+
: countIssue('jobs.maxInFlight', config.jobs.maxInFlight, 1),
|
|
286
294
|
countIssue('jobs.maxAttempts', config.jobs.maxAttempts, 1),
|
|
287
295
|
countIssue('jobs.visibilityTimeoutMs', config.jobs.visibilityTimeoutMs, 1),
|
|
288
296
|
...(['doneRetentionMs', 'deadRetentionMs', 'outboxRetentionMs'] as const).map((key) =>
|