@jxsuite/compiler 5.1.2 → 5.1.3
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/dist/cli.js +220 -351
- package/dist/compiler.js +126 -257
- package/package.json +4 -4
- package/src/cli.ts +12 -14
- package/src/compiler.ts +45 -23
- package/src/shadow.ts +9 -8
- package/src/shared.ts +350 -296
- package/src/site/asset-mounts.ts +22 -12
- package/src/site/bundler.ts +38 -12
- package/src/site/client-runtime.ts +31 -17
- package/src/site/csp.ts +20 -11
- package/src/site/db-push.ts +38 -24
- package/src/site/dev-command.ts +3 -3
- package/src/site/format-host.ts +56 -45
- package/src/site/headers-emitter.ts +7 -4
- package/src/site/i18n.ts +20 -19
- package/src/site/image-cache.ts +16 -3
- package/src/site/image-optimizer.ts +19 -5
- package/src/site/image-transform.ts +56 -60
- package/src/site/img-loading.ts +11 -5
- package/src/site/layout-resolver.ts +7 -6
- package/src/site/link-relations.ts +10 -6
- package/src/site/locale-negotiation.ts +1 -1
- package/src/site/pages-discovery.ts +65 -47
- package/src/site/preview-server.ts +22 -19
- package/src/site/project-sections.ts +12 -3
- package/src/site/prototype-resolver.ts +94 -42
- package/src/site/schema-command.ts +26 -13
- package/src/site/service-worker.ts +2 -2
- package/src/site/site-build.ts +379 -262
- package/src/site/site-loader.ts +6 -3
- package/src/site/static-host.ts +3 -3
- package/src/site/validate-command.ts +64 -34
- package/src/site/well-known.ts +57 -12
- package/src/targets/compile-class.ts +17 -5
- package/src/targets/compile-client.ts +141 -106
- package/src/targets/compile-element.ts +157 -140
- package/src/targets/compile-server.ts +11 -5
- package/src/targets/compile-static.ts +57 -63
- package/src/types.ts +5 -6
package/src/site/format-host.ts
CHANGED
|
@@ -12,8 +12,14 @@ import { pathToFileURL } from "node:url";
|
|
|
12
12
|
import { createRequire } from "node:module";
|
|
13
13
|
import { buildExtensionRegistry, EXTENSION_MANIFEST } from "@jxsuite/schema/extension-registry";
|
|
14
14
|
import type { ExtensionRegistry } from "@jxsuite/schema/extension-registry";
|
|
15
|
-
import type {
|
|
16
|
-
|
|
15
|
+
import type {
|
|
16
|
+
FormatHostIO,
|
|
17
|
+
ImplementationExport,
|
|
18
|
+
ProjectBlock,
|
|
19
|
+
StudioHints,
|
|
20
|
+
} from "@jxsuite/schema/format-registry";
|
|
21
|
+
import { isJsonObject } from "@jxsuite/schema/guards";
|
|
22
|
+
import type { JsonObject, JsonValue, ProjectConfig } from "@jxsuite/schema/types";
|
|
17
23
|
|
|
18
24
|
export type { ExtensionRegistry } from "@jxsuite/schema/extension-registry";
|
|
19
25
|
export type { FormatRegistry, FormatEntry } from "@jxsuite/schema/format-registry";
|
|
@@ -27,9 +33,10 @@ export type { FormatRegistry, FormatEntry } from "@jxsuite/schema/format-registr
|
|
|
27
33
|
* built module is JavaScript), and .js → .ts.
|
|
28
34
|
*
|
|
29
35
|
* @param {string} path - Absolute path to the implementation module
|
|
30
|
-
* @returns {Promise<
|
|
36
|
+
* @returns {Promise<ImplementationExport>} The module's namespace object, whose named exports (and
|
|
37
|
+
* `default`) are read by name
|
|
31
38
|
*/
|
|
32
|
-
export async function importImplementation(path: string): Promise<
|
|
39
|
+
export async function importImplementation(path: string): Promise<ImplementationExport> {
|
|
33
40
|
const candidates = [path];
|
|
34
41
|
const srcSeg = `${sep}src${sep}`;
|
|
35
42
|
const distSeg = `${sep}dist${sep}`;
|
|
@@ -131,7 +138,11 @@ export function probeExtension(projectRoot: string, name: string): ExtensionReso
|
|
|
131
138
|
// Not shipped by this host either.
|
|
132
139
|
}
|
|
133
140
|
|
|
134
|
-
|
|
141
|
+
const resolution: ExtensionResolution = { host, project };
|
|
142
|
+
if (manifestPath !== undefined) {
|
|
143
|
+
resolution.manifestPath = manifestPath;
|
|
144
|
+
}
|
|
145
|
+
return resolution;
|
|
135
146
|
}
|
|
136
147
|
|
|
137
148
|
/** Build the extension registry for a project from its project.json `extensions` array. */
|
|
@@ -146,20 +157,6 @@ export async function buildProjectExtensionRegistry(
|
|
|
146
157
|
);
|
|
147
158
|
}
|
|
148
159
|
|
|
149
|
-
/**
|
|
150
|
-
* Format-dispatch view of the project's extension registry.
|
|
151
|
-
*
|
|
152
|
-
* @deprecated Part-3 cleanup: the desktop app still imports this; use
|
|
153
|
-
* `buildProjectExtensionRegistry(...).then((r) => r.formats)` instead.
|
|
154
|
-
*/
|
|
155
|
-
export async function buildProjectFormatRegistry(
|
|
156
|
-
projectRoot: string,
|
|
157
|
-
projectConfig?: ProjectConfig,
|
|
158
|
-
): Promise<FormatRegistry> {
|
|
159
|
-
const registry = await buildProjectExtensionRegistry(projectRoot, projectConfig);
|
|
160
|
-
return registry.formats;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
160
|
// ─── Studio extensions payload ────────────────────────────────────────────────
|
|
164
161
|
|
|
165
162
|
/** One project-section contribution on the studio wire (specs/extensions.md §9/§9.1). */
|
|
@@ -169,12 +166,24 @@ export interface ExtensionsPayloadContribution {
|
|
|
169
166
|
/** The class descriptor's `project` admission block. */
|
|
170
167
|
project: ProjectBlock;
|
|
171
168
|
/** The class descriptor's `$studio` block (settings section vocabulary), when declared. */
|
|
172
|
-
studio:
|
|
169
|
+
studio: StudioHints | null;
|
|
173
170
|
/**
|
|
174
|
-
* The section's value schema — `properties[<key>]` of the extension's project fragment
|
|
175
|
-
*
|
|
171
|
+
* The section's value schema — `properties[<key>]` of the extension's project fragment, which
|
|
172
|
+
* JSON Schema lets be a boolean as well as an object — or null when the extension ships no
|
|
173
|
+
* project fragment (or it lacks the key).
|
|
176
174
|
*/
|
|
177
|
-
entrySchema:
|
|
175
|
+
entrySchema: JsonValue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** One manifest class of an extension on the studio wire. */
|
|
179
|
+
export interface ExtensionsPayloadClass {
|
|
180
|
+
name: string;
|
|
181
|
+
/** The class's resolved descriptor path. */
|
|
182
|
+
path: string;
|
|
183
|
+
/** Present on a plain state class: one with no admission blocks. */
|
|
184
|
+
state?: boolean;
|
|
185
|
+
/** The descriptor's `$studio.stateDefaults` hint (specs/extensions.md §10), when it is an object. */
|
|
186
|
+
stateDefaults?: JsonObject;
|
|
178
187
|
}
|
|
179
188
|
|
|
180
189
|
/** One extension package on the studio wire (the formats route's `extensions` sibling array). */
|
|
@@ -191,12 +200,7 @@ export interface ExtensionsPayloadEntry {
|
|
|
191
200
|
* carry `state: true` plus their `$studio.stateDefaults` hint (specs/extensions.md §10), so the
|
|
192
201
|
* studio can offer them in its add-state picker and seed new defs (e.g. `timing: "client"`).
|
|
193
202
|
*/
|
|
194
|
-
classes:
|
|
195
|
-
name: string;
|
|
196
|
-
path: string;
|
|
197
|
-
state?: boolean;
|
|
198
|
-
stateDefaults?: Record<string, unknown>;
|
|
199
|
-
}[];
|
|
203
|
+
classes: ExtensionsPayloadClass[];
|
|
200
204
|
}
|
|
201
205
|
|
|
202
206
|
/**
|
|
@@ -211,13 +215,13 @@ export interface ExtensionsPayloadEntry {
|
|
|
211
215
|
export function buildExtensionsPayload(registry: ExtensionRegistry): ExtensionsPayloadEntry[] {
|
|
212
216
|
const payload: ExtensionsPayloadEntry[] = [];
|
|
213
217
|
for (const ext of registry.extensions) {
|
|
214
|
-
let fragmentProperties:
|
|
218
|
+
let fragmentProperties: JsonObject = {};
|
|
215
219
|
if (ext.schemas.project) {
|
|
216
220
|
try {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
fragmentProperties =
|
|
221
|
+
// SAFETY: JSON.parse only ever yields JSON values; its `any` return type says nothing more.
|
|
222
|
+
const fragment = JSON.parse(readFileSync(ext.schemas.project, "utf8")) as JsonValue;
|
|
223
|
+
const properties = isJsonObject(fragment) ? fragment.properties : undefined;
|
|
224
|
+
fragmentProperties = isJsonObject(properties) ? properties : {};
|
|
221
225
|
} catch {
|
|
222
226
|
fragmentProperties = {};
|
|
223
227
|
}
|
|
@@ -234,27 +238,34 @@ export function buildExtensionsPayload(registry: ExtensionRegistry): ExtensionsP
|
|
|
234
238
|
studio: cls.studio,
|
|
235
239
|
});
|
|
236
240
|
}
|
|
237
|
-
|
|
241
|
+
const entry: ExtensionsPayloadEntry = {
|
|
238
242
|
classes: ext.classes.map((cls) => {
|
|
239
|
-
const stateDefaults = cls.studio?.stateDefaults
|
|
243
|
+
const stateDefaults = cls.studio?.stateDefaults;
|
|
240
244
|
const isState =
|
|
241
245
|
cls.project === null &&
|
|
242
246
|
cls.server === null &&
|
|
243
247
|
cls.connector === null &&
|
|
244
248
|
cls.extensions.length === 0;
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
249
|
+
const payloadClass: ExtensionsPayloadClass = { name: cls.name, path: cls.classPath };
|
|
250
|
+
if (isState) {
|
|
251
|
+
payloadClass.state = true;
|
|
252
|
+
}
|
|
253
|
+
if (isJsonObject(stateDefaults)) {
|
|
254
|
+
payloadClass.stateDefaults = stateDefaults;
|
|
255
|
+
}
|
|
256
|
+
return payloadClass;
|
|
251
257
|
}),
|
|
252
258
|
contributions,
|
|
253
259
|
name: ext.manifest.name,
|
|
254
260
|
specifier: ext.specifier,
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
261
|
+
};
|
|
262
|
+
if (ext.manifest.title !== undefined) {
|
|
263
|
+
entry.title = ext.manifest.title;
|
|
264
|
+
}
|
|
265
|
+
if (ext.manifest.description !== undefined) {
|
|
266
|
+
entry.description = ext.manifest.description;
|
|
267
|
+
}
|
|
268
|
+
payload.push(entry);
|
|
258
269
|
}
|
|
259
270
|
return payload;
|
|
260
271
|
}
|
|
@@ -24,6 +24,12 @@ export interface HeaderRule {
|
|
|
24
24
|
headers: Record<string, string>;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/** The generated `_headers` stanzas, and the configuration problems found while deriving them. */
|
|
28
|
+
export interface HeaderRuleSet {
|
|
29
|
+
rules: HeaderRule[];
|
|
30
|
+
errors: string[];
|
|
31
|
+
}
|
|
32
|
+
|
|
27
33
|
/**
|
|
28
34
|
* The only content-addressed output the build produces. `variantFilename()` embeds an 8-character
|
|
29
35
|
* digest of the source bytes, so a changed image is a changed URL and the old one can be cached
|
|
@@ -56,10 +62,7 @@ const AUTHOR_BANNER =
|
|
|
56
62
|
export function buildHeaderRules(
|
|
57
63
|
build: ProjectConfig["build"],
|
|
58
64
|
cspSources?: CspSources,
|
|
59
|
-
): {
|
|
60
|
-
rules: HeaderRule[];
|
|
61
|
-
errors: string[];
|
|
62
|
-
} {
|
|
65
|
+
): HeaderRuleSet {
|
|
63
66
|
const cfg: HeadersConfig = build?.headers ?? {};
|
|
64
67
|
const errors: string[] = [];
|
|
65
68
|
if (cfg.enabled === false) {
|
package/src/site/i18n.ts
CHANGED
|
@@ -13,12 +13,9 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { siteAbsoluteUrl } from "@jxsuite/schema/asset-paths";
|
|
16
|
-
import {
|
|
17
|
-
canonicalizeLocale,
|
|
18
|
-
localeDirection,
|
|
19
|
-
localeOfRoute as localeForRoute,
|
|
20
|
-
} from "@jxsuite/schema/locale";
|
|
16
|
+
import { canonicalizeLocale, localeDirection, localeOfRoute } from "@jxsuite/schema/locale";
|
|
21
17
|
import type { ResolvedI18n, TextDirection } from "@jxsuite/schema/locale";
|
|
18
|
+
import type { TranslationMember } from "@jxsuite/site/context";
|
|
22
19
|
|
|
23
20
|
import { servedRoute } from "./static-host.ts";
|
|
24
21
|
|
|
@@ -144,14 +141,6 @@ export interface LocaleAlternate {
|
|
|
144
141
|
href: string;
|
|
145
142
|
}
|
|
146
143
|
|
|
147
|
-
/** One route in a translation set: the locale it serves, and where it lives. */
|
|
148
|
-
export interface TranslationMember {
|
|
149
|
-
/** Canonical BCP 47 tag. */
|
|
150
|
-
locale: string;
|
|
151
|
-
/** Site-absolute route pattern, e.g. `/fr-ca/about/`. */
|
|
152
|
-
urlPattern: string;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
144
|
/** A route as translation grouping sees it: its URL, and the key its document declared. */
|
|
156
145
|
export interface TranslationRoute {
|
|
157
146
|
/** From the document's `$translationKey`; absent when the key is derived from the path. */
|
|
@@ -169,6 +158,19 @@ export interface TranslationConflict {
|
|
|
169
158
|
urlPatterns: string[];
|
|
170
159
|
}
|
|
171
160
|
|
|
161
|
+
/** Routes grouped into translation sets, and the collisions found while grouping them. */
|
|
162
|
+
export interface TranslationSets {
|
|
163
|
+
conflicts: TranslationConflict[];
|
|
164
|
+
/** Keyed by `urlPattern`; members of one set share one array. */
|
|
165
|
+
sets: Map<string, TranslationMember[]>;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** The `lang` a page ships with, and its `dir` when that is worth writing down. */
|
|
169
|
+
export interface PageLanguage {
|
|
170
|
+
lang: string;
|
|
171
|
+
dir?: string;
|
|
172
|
+
}
|
|
173
|
+
|
|
172
174
|
/** Everything a page needs to know about its own locale, gathered once per build. */
|
|
173
175
|
export interface PageLocaleContext {
|
|
174
176
|
/** Absolute alternates for `<head>` and the sitemap; empty without a site `url`. */
|
|
@@ -198,13 +200,12 @@ export interface PageLocaleContext {
|
|
|
198
200
|
*
|
|
199
201
|
* @param {readonly TranslationRoute[]} routes - Concrete routes only
|
|
200
202
|
* @param {ResolvedI18n | null} i18n
|
|
201
|
-
* @returns {
|
|
202
|
-
* keyed by `urlPattern`; members of one set share one array
|
|
203
|
+
* @returns {TranslationSets} Sets are keyed by `urlPattern`; members of one set share one array
|
|
203
204
|
*/
|
|
204
205
|
export function translationSets(
|
|
205
206
|
routes: readonly TranslationRoute[],
|
|
206
207
|
i18n: ResolvedI18n | null,
|
|
207
|
-
):
|
|
208
|
+
): TranslationSets {
|
|
208
209
|
const out = new Map<string, TranslationMember[]>();
|
|
209
210
|
const conflicts: TranslationConflict[] = [];
|
|
210
211
|
if (i18n === null) {
|
|
@@ -226,7 +227,7 @@ export function translationSets(
|
|
|
226
227
|
if (route.translationKey !== undefined) {
|
|
227
228
|
declared.add(route.urlPattern);
|
|
228
229
|
}
|
|
229
|
-
const locale =
|
|
230
|
+
const locale = localeOfRoute(route.urlPattern, i18n);
|
|
230
231
|
if (locale === null) {
|
|
231
232
|
continue;
|
|
232
233
|
}
|
|
@@ -332,7 +333,7 @@ export function localeAlternates(
|
|
|
332
333
|
* that says nothing.
|
|
333
334
|
*
|
|
334
335
|
* @param {object} args
|
|
335
|
-
* @returns {
|
|
336
|
+
* @returns {PageLanguage}
|
|
336
337
|
*/
|
|
337
338
|
export function pageLanguage({
|
|
338
339
|
pageLang,
|
|
@@ -344,7 +345,7 @@ export function pageLanguage({
|
|
|
344
345
|
pageDir?: string | undefined;
|
|
345
346
|
routeLocale?: string | null;
|
|
346
347
|
defaults?: { lang?: string | undefined; dir?: string | undefined } | undefined;
|
|
347
|
-
}):
|
|
348
|
+
}): PageLanguage {
|
|
348
349
|
const lang = pageLang ?? routeLocale ?? defaults?.lang ?? "en";
|
|
349
350
|
const explicit = pageDir ?? defaults?.dir;
|
|
350
351
|
if (explicit !== undefined) {
|
package/src/site/image-cache.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { execSync } from "node:child_process";
|
|
|
12
12
|
import { tmpdir } from "node:os";
|
|
13
13
|
import { configHash, contentHash } from "./image-optimizer.ts";
|
|
14
14
|
|
|
15
|
-
import type { ImageConfig, ImageManifest } from "./image-optimizer.ts";
|
|
15
|
+
import type { ImageConfig, ImageManifest, ImageVariant } from "./image-optimizer.ts";
|
|
16
16
|
|
|
17
17
|
interface CacheEntry {
|
|
18
18
|
source: string; // Relative path to source image
|
|
@@ -20,6 +20,15 @@ interface CacheEntry {
|
|
|
20
20
|
timestamp: number; // When the entry was cached
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* A cache entry as `manifest.json` may still hold it. A persisted cache outlives the build that
|
|
25
|
+
* wrote it, and an entry from before manifests carried `variants` has none, so pruning, which reads
|
|
26
|
+
* every entry rather than only the ones this build looked up, does not assume the list is there.
|
|
27
|
+
*/
|
|
28
|
+
interface PersistedEntry {
|
|
29
|
+
manifest: { variants?: readonly ImageVariant[] };
|
|
30
|
+
}
|
|
31
|
+
|
|
23
32
|
export interface CacheManifest {
|
|
24
33
|
version: number; // Cache format version
|
|
25
34
|
entries: Record<string, CacheEntry>; // Cached entries keyed by content+config hash
|
|
@@ -101,6 +110,9 @@ export function loadCache(projectRoot: string): CacheManifest {
|
|
|
101
110
|
return { entries: {}, touched: new Set(), version: 1 };
|
|
102
111
|
}
|
|
103
112
|
try {
|
|
113
|
+
/* SAFETY: manifest.json is written only by saveCache, from a CacheManifest minus its
|
|
114
|
+
runtime-only `touched`, which is restored below; bytes that do not parse throw into the
|
|
115
|
+
empty-cache fallback. */
|
|
104
116
|
const parsed = JSON.parse(readFileSync(manifestPath, "utf8")) as CacheManifest;
|
|
105
117
|
parsed.touched = new Set();
|
|
106
118
|
return parsed;
|
|
@@ -147,14 +159,15 @@ export function saveCache(
|
|
|
147
159
|
*/
|
|
148
160
|
function pruneStale(cache: CacheManifest, touched: Set<string>) {
|
|
149
161
|
const keep = new Set<string>();
|
|
150
|
-
|
|
162
|
+
const entries = Object.entries<PersistedEntry>(cache.entries);
|
|
163
|
+
for (const [key, entry] of entries) {
|
|
151
164
|
if (touched.has(key)) {
|
|
152
165
|
for (const variant of entry.manifest.variants ?? []) {
|
|
153
166
|
keep.add(variant.absolutePath);
|
|
154
167
|
}
|
|
155
168
|
}
|
|
156
169
|
}
|
|
157
|
-
for (const [key, entry] of
|
|
170
|
+
for (const [key, entry] of entries) {
|
|
158
171
|
if (touched.has(key)) {
|
|
159
172
|
continue;
|
|
160
173
|
}
|
|
@@ -68,7 +68,7 @@ async function getSharp() {
|
|
|
68
68
|
// Production installs where @img/sharp-* native packages are adjacent to cli.js.
|
|
69
69
|
try {
|
|
70
70
|
const sharpMod = await import("sharp");
|
|
71
|
-
_sharp = sharpMod.default
|
|
71
|
+
_sharp = sharpMod.default;
|
|
72
72
|
return _sharp;
|
|
73
73
|
} catch {
|
|
74
74
|
// Fall through to CJS fallback below.
|
|
@@ -79,6 +79,7 @@ async function getSharp() {
|
|
|
79
79
|
// Unreachable via the primary import path.
|
|
80
80
|
try {
|
|
81
81
|
const req = createRequire(resolve(process.cwd(), "package.json"));
|
|
82
|
+
// SAFETY: sharp's CommonJS entry is `module.exports = sharp`, the value its ESM default exports.
|
|
82
83
|
_sharp = req("sharp") as SharpModule;
|
|
83
84
|
return _sharp;
|
|
84
85
|
} catch (error) {
|
|
@@ -91,6 +92,17 @@ async function getSharp() {
|
|
|
91
92
|
|
|
92
93
|
const OPTIMIZED_DIR = "images/_optimized";
|
|
93
94
|
|
|
95
|
+
/**
|
|
96
|
+
* The part of sharp's metadata this module reads, with every member optional. The sharp these types
|
|
97
|
+
* describe always reports all three, but `getSharp` may load the project's own copy instead, and
|
|
98
|
+
* sharp before 0.34 declared each of them optional.
|
|
99
|
+
*/
|
|
100
|
+
interface ReportedMetadata {
|
|
101
|
+
format?: string;
|
|
102
|
+
height?: number;
|
|
103
|
+
width?: number;
|
|
104
|
+
}
|
|
105
|
+
|
|
94
106
|
/**
|
|
95
107
|
* Get image metadata (dimensions and format) via Sharp.
|
|
96
108
|
*
|
|
@@ -99,9 +111,9 @@ const OPTIMIZED_DIR = "images/_optimized";
|
|
|
99
111
|
*/
|
|
100
112
|
export async function getImageMetadata(srcPath: string) {
|
|
101
113
|
const sharp = await getSharp();
|
|
102
|
-
const meta = await sharp(srcPath).metadata();
|
|
114
|
+
const meta: ReportedMetadata = await sharp(srcPath).metadata();
|
|
103
115
|
return {
|
|
104
|
-
format:
|
|
116
|
+
format: meta.format ?? "unknown",
|
|
105
117
|
height: meta.height ?? 0,
|
|
106
118
|
width: meta.width ?? 0,
|
|
107
119
|
};
|
|
@@ -216,7 +228,10 @@ export async function processImage(srcPath: string, cacheImgDir: string, config:
|
|
|
216
228
|
continue;
|
|
217
229
|
}
|
|
218
230
|
|
|
231
|
+
/* SAFETY: `format` is a free string from project config; a name outside the four keyed
|
|
232
|
+
qualities reads `undefined` from the lookup and takes the default of 80. */
|
|
219
233
|
const quality = config.quality[format as keyof ImageConfig["quality"]] ?? 80;
|
|
234
|
+
// SAFETY: toFormat checks the name itself and throws for a format sharp cannot encode.
|
|
220
235
|
const task = sharp(srcPath)
|
|
221
236
|
.resize(width)
|
|
222
237
|
.toFormat(format as SharpFormat, { quality })
|
|
@@ -248,7 +263,6 @@ export async function processImage(srcPath: string, cacheImgDir: string, config:
|
|
|
248
263
|
*/
|
|
249
264
|
export function buildSrcset(variants: ImageVariant[], format: string) {
|
|
250
265
|
return variants
|
|
251
|
-
.
|
|
252
|
-
.map((v) => `${v.outputPath} ${v.width}w`)
|
|
266
|
+
.flatMap((v) => (v.format === format ? [`${v.outputPath} ${v.width}w`] : []))
|
|
253
267
|
.join(", ");
|
|
254
268
|
}
|