@junejs/server 1.0.0-dev.27 → 1.0.0-dev.28
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/build.js +1 -0
- package/dist/favicon.d.ts +6 -2
- package/dist/favicon.js +18 -5
- package/dist/pipeline.js +2 -0
- package/dist/web-manifest.d.ts +6 -0
- package/dist/web-manifest.js +46 -0
- package/package.json +2 -2
- package/src/build.ts +1 -0
- package/src/favicon.ts +32 -5
- package/src/pipeline.ts +7 -0
- package/src/web-manifest.ts +42 -0
package/dist/build.js
CHANGED
package/dist/favicon.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { DocumentConfig } from "@junejs/core/document";
|
|
|
3
3
|
type Site = DocumentConfig["site"];
|
|
4
4
|
declare const ICON_BG = "#1d1d1f";
|
|
5
5
|
declare const ICON_FG = "#fbfbf8";
|
|
6
|
-
declare const GENERATED_ICONS: readonly ["apple-touch-icon.png", "icon.png", "favicon.ico"];
|
|
6
|
+
declare const GENERATED_ICONS: readonly ["apple-touch-icon.png", "icon.png", "favicon.ico", "icon-192.png", "icon-512.png"];
|
|
7
7
|
type GeneratedIconFile = (typeof GENERATED_ICONS)[number];
|
|
8
8
|
declare function publicFileCheck(publicDir: string): (file: string) => boolean;
|
|
9
9
|
declare function resolveIcons(site: Site, inPublic: (file: string) => boolean): {
|
|
@@ -17,11 +17,15 @@ declare function googleFontFetcher(cacheDir: string, opts?: {
|
|
|
17
17
|
fetch?: typeof fetch;
|
|
18
18
|
timeoutMs?: number;
|
|
19
19
|
}): FontFetcher;
|
|
20
|
+
declare const INK_H = 28;
|
|
21
|
+
declare const INK_W = 40;
|
|
20
22
|
declare function pngToIco(png: Uint8Array, size: number): Uint8Array<ArrayBuffer>;
|
|
21
23
|
type GeneratedIcons = {
|
|
22
24
|
"apple-touch-icon.png": Uint8Array<ArrayBuffer>;
|
|
23
25
|
"icon.png": Uint8Array<ArrayBuffer>;
|
|
24
26
|
"favicon.ico": Uint8Array<ArrayBuffer>;
|
|
27
|
+
"icon-192.png": Uint8Array<ArrayBuffer>;
|
|
28
|
+
"icon-512.png": Uint8Array<ArrayBuffer>;
|
|
25
29
|
letter: string;
|
|
26
30
|
family: string | null;
|
|
27
31
|
};
|
|
@@ -31,4 +35,4 @@ declare function renderIcons(opts: {
|
|
|
31
35
|
warn?: (msg: string) => void;
|
|
32
36
|
}): Promise<GeneratedIcons>;
|
|
33
37
|
//#endregion
|
|
34
|
-
export { FontFetcher, GENERATED_ICONS, GeneratedIconFile, GeneratedIcons, ICON_BG, ICON_FG, googleFontFetcher, hanFamily, iconFamily, pngToIco, publicFileCheck, renderIcons, resolveIcons };
|
|
38
|
+
export { FontFetcher, GENERATED_ICONS, GeneratedIconFile, GeneratedIcons, ICON_BG, ICON_FG, INK_H, INK_W, googleFontFetcher, hanFamily, iconFamily, pngToIco, publicFileCheck, renderIcons, resolveIcons };
|
package/dist/favicon.js
CHANGED
|
@@ -10,7 +10,9 @@ const ICON_FG = "#fbfbf8";
|
|
|
10
10
|
const GENERATED_ICONS = [
|
|
11
11
|
"apple-touch-icon.png",
|
|
12
12
|
"icon.png",
|
|
13
|
-
"favicon.ico"
|
|
13
|
+
"favicon.ico",
|
|
14
|
+
"icon-192.png",
|
|
15
|
+
"icon-512.png"
|
|
14
16
|
];
|
|
15
17
|
const PRIMARY_ICON_FILES = [
|
|
16
18
|
"favicon.svg",
|
|
@@ -20,6 +22,11 @@ const PRIMARY_ICON_FILES = [
|
|
|
20
22
|
"favicon.ico"
|
|
21
23
|
];
|
|
22
24
|
const CUSTOM_ICON_FILES = [...PRIMARY_ICON_FILES, "apple-touch-icon.png"];
|
|
25
|
+
const MANIFEST_FILES = [
|
|
26
|
+
"manifest.webmanifest",
|
|
27
|
+
"manifest.json",
|
|
28
|
+
"site.webmanifest"
|
|
29
|
+
];
|
|
23
30
|
function publicFileCheck(publicDir) {
|
|
24
31
|
let real = false;
|
|
25
32
|
try {
|
|
@@ -28,11 +35,14 @@ function publicFileCheck(publicDir) {
|
|
|
28
35
|
return (file) => real && existsSync(join(publicDir, file));
|
|
29
36
|
}
|
|
30
37
|
function resolveIcons(site, inPublic) {
|
|
38
|
+
const ownManifest = MANIFEST_FILES.find(inPublic);
|
|
31
39
|
if (!(Boolean(site.icon) || CUSTOM_ICON_FILES.some(inPublic))) return {
|
|
32
40
|
generate: true,
|
|
33
41
|
icons: {
|
|
34
42
|
png: "/icon.png",
|
|
35
|
-
appleTouch: "/apple-touch-icon.png"
|
|
43
|
+
appleTouch: "/apple-touch-icon.png",
|
|
44
|
+
manifest: ownManifest ? `/${ownManifest}` : "/manifest.webmanifest",
|
|
45
|
+
generated: !ownManifest
|
|
36
46
|
}
|
|
37
47
|
};
|
|
38
48
|
const primary = PRIMARY_ICON_FILES.find(inPublic);
|
|
@@ -41,7 +51,8 @@ function resolveIcons(site, inPublic) {
|
|
|
41
51
|
icons: {
|
|
42
52
|
primary: primary ? `/${primary}` : void 0,
|
|
43
53
|
png: inPublic("icon.png") ? "/icon.png" : void 0,
|
|
44
|
-
appleTouch: inPublic("apple-touch-icon.png") ? "/apple-touch-icon.png" : void 0
|
|
54
|
+
appleTouch: inPublic("apple-touch-icon.png") ? "/apple-touch-icon.png" : void 0,
|
|
55
|
+
manifest: ownManifest ? `/${ownManifest}` : void 0
|
|
45
56
|
}
|
|
46
57
|
};
|
|
47
58
|
}
|
|
@@ -163,7 +174,7 @@ function place(resvg, ch, family, font) {
|
|
|
163
174
|
if (!bbox) return null;
|
|
164
175
|
const notdef = inkBox(resvg, NOTDEF_PROBE, family, font);
|
|
165
176
|
if (notdef && notdef.x === bbox.x && notdef.y === bbox.y && notdef.width === bbox.width && notdef.height === bbox.height) return null;
|
|
166
|
-
const scale = Math.min(
|
|
177
|
+
const scale = Math.min(28 / bbox.height, 40 / bbox.width);
|
|
167
178
|
const cx = bbox.x + bbox.width / 2 - PROBE / 2;
|
|
168
179
|
const cy = bbox.y + bbox.height / 2 - PROBE * .7;
|
|
169
180
|
return {
|
|
@@ -227,9 +238,11 @@ async function renderIcons(opts) {
|
|
|
227
238
|
"apple-touch-icon.png": render(180, false),
|
|
228
239
|
"icon.png": icon32,
|
|
229
240
|
"favicon.ico": pngToIco(icon32, 32),
|
|
241
|
+
"icon-192.png": render(192, false),
|
|
242
|
+
"icon-512.png": render(512, false),
|
|
230
243
|
letter: ch,
|
|
231
244
|
family: placement?.family ?? null
|
|
232
245
|
};
|
|
233
246
|
}
|
|
234
247
|
//#endregion
|
|
235
|
-
export { GENERATED_ICONS, ICON_BG, ICON_FG, googleFontFetcher, hanFamily, iconFamily, pngToIco, publicFileCheck, renderIcons, resolveIcons };
|
|
248
|
+
export { GENERATED_ICONS, ICON_BG, ICON_FG, INK_H, INK_W, googleFontFetcher, hanFamily, iconFamily, pngToIco, publicFileCheck, renderIcons, resolveIcons };
|
package/dist/pipeline.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { iconLetter } from "./icon-letter.js";
|
|
2
|
+
import { webManifest } from "./web-manifest.js";
|
|
2
3
|
import { SEGMENT_HEADER, TITLE_HEADER, encodeTitle, negotiate } from "./negotiate.js";
|
|
3
4
|
import { ensureScope, runInScope, setRequestLocale } from "@junejs/db";
|
|
4
5
|
import { LoaderDataContext } from "@junejs/core/route";
|
|
@@ -201,6 +202,7 @@ function createPipeline(cfg) {
|
|
|
201
202
|
if (out) return out;
|
|
202
203
|
}
|
|
203
204
|
if (request.method === "GET" && !docConfig.site.icon && (url.pathname === "/favicon.svg" || url.pathname === "/favicon.ico")) return letterFavicon(docConfig.site.name);
|
|
205
|
+
if (request.method === "GET" && docConfig.icons?.generated && url.pathname === "/manifest.webmanifest") return text(webManifest(docConfig), "application/manifest+json", { headers: { "cache-control": "public, max-age=86400" } });
|
|
204
206
|
let locale;
|
|
205
207
|
let routeBase = url.pathname;
|
|
206
208
|
if (cfg.i18n) {
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { DocumentConfig } from "@junejs/core/document";
|
|
2
|
+
//#region src/web-manifest.d.ts
|
|
3
|
+
declare function shortName(site: DocumentConfig["site"]): string | undefined;
|
|
4
|
+
declare function webManifest(config: DocumentConfig): string;
|
|
5
|
+
//#endregion
|
|
6
|
+
export { shortName, webManifest };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { resolveThemeColor } from "@junejs/core/document";
|
|
2
|
+
//#region src/web-manifest.ts
|
|
3
|
+
function shortName(site) {
|
|
4
|
+
if (site.shortName) return site.shortName;
|
|
5
|
+
const name = site.name?.trim();
|
|
6
|
+
if (!name) return void 0;
|
|
7
|
+
return name.split(/\s+[—–|-]\s+|:\s+/)[0].trim() || name;
|
|
8
|
+
}
|
|
9
|
+
function webManifest(config) {
|
|
10
|
+
const base = config.basePath ?? "";
|
|
11
|
+
const theme = resolveThemeColor(config);
|
|
12
|
+
const color = typeof theme === "string" ? theme : void 0;
|
|
13
|
+
const manifest = {
|
|
14
|
+
name: config.site.name,
|
|
15
|
+
short_name: shortName(config.site),
|
|
16
|
+
description: config.site.description,
|
|
17
|
+
start_url: `${base}/`,
|
|
18
|
+
scope: `${base}/`,
|
|
19
|
+
display: "browser",
|
|
20
|
+
...color ? {
|
|
21
|
+
theme_color: color,
|
|
22
|
+
background_color: color
|
|
23
|
+
} : {},
|
|
24
|
+
icons: [
|
|
25
|
+
{
|
|
26
|
+
src: `${base}/icon-192.png`,
|
|
27
|
+
sizes: "192x192",
|
|
28
|
+
type: "image/png"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
src: `${base}/icon-512.png`,
|
|
32
|
+
sizes: "512x512",
|
|
33
|
+
type: "image/png"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
src: `${base}/icon-512.png`,
|
|
37
|
+
sizes: "512x512",
|
|
38
|
+
type: "image/png",
|
|
39
|
+
purpose: "maskable"
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
};
|
|
43
|
+
return JSON.stringify(manifest, null, 2) + "\n";
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
export { shortName, webManifest };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@junejs/server",
|
|
3
|
-
"version": "1.0.0-dev.
|
|
3
|
+
"version": "1.0.0-dev.28",
|
|
4
4
|
"description": "June host adapters + dev server. Host-coupled; depends on the pure @junejs/core contract layer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -140,7 +140,7 @@
|
|
|
140
140
|
"rolldown": "^1.1.0"
|
|
141
141
|
},
|
|
142
142
|
"peerDependencies": {
|
|
143
|
-
"@junejs/core": ">=0.2.0-dev.
|
|
143
|
+
"@junejs/core": ">=0.2.0-dev.48",
|
|
144
144
|
"react": "^19.2.0",
|
|
145
145
|
"react-dom": "^19.2.0",
|
|
146
146
|
"react-server-dom-webpack": "19.2.7",
|
package/src/build.ts
CHANGED
|
@@ -631,6 +631,7 @@ ${doClass}`;
|
|
|
631
631
|
// skips its file rather than failing the build.
|
|
632
632
|
const extra: Array<[string, string]> = [
|
|
633
633
|
["/favicon.svg", "favicon.svg"],
|
|
634
|
+
["/manifest.webmanifest", "manifest.webmanifest"],
|
|
634
635
|
["/llms.txt", "llms.txt"],
|
|
635
636
|
["/sitemap.xml", "sitemap.xml"],
|
|
636
637
|
];
|
package/src/favicon.ts
CHANGED
|
@@ -32,12 +32,13 @@ export const ICON_FG = "#fbfbf8";
|
|
|
32
32
|
|
|
33
33
|
// Filenames the icons are served at (from the app root), and the public/ files
|
|
34
34
|
// that mean the app brings its own icon — then June generates none.
|
|
35
|
-
export const GENERATED_ICONS = ["apple-touch-icon.png", "icon.png", "favicon.ico"] as const;
|
|
35
|
+
export const GENERATED_ICONS = ["apple-touch-icon.png", "icon.png", "favicon.ico", "icon-192.png", "icon-512.png"] as const;
|
|
36
36
|
export type GeneratedIconFile = (typeof GENERATED_ICONS)[number];
|
|
37
37
|
// The app's own main icon, in preference order: the first one present becomes
|
|
38
38
|
// the page's favicon link (so none of them is ever silently unreferenced).
|
|
39
39
|
const PRIMARY_ICON_FILES = ["favicon.svg", "icon.svg", "favicon.png", "icon.png", "favicon.ico"];
|
|
40
40
|
const CUSTOM_ICON_FILES = [...PRIMARY_ICON_FILES, "apple-touch-icon.png"];
|
|
41
|
+
const MANIFEST_FILES = ["manifest.webmanifest", "manifest.json", "site.webmanifest"];
|
|
41
42
|
|
|
42
43
|
// `inPublic` for resolveIcons over an app's public/ dir. A symlinked public/ is
|
|
43
44
|
// ignored, as the build and dev server ignore it.
|
|
@@ -56,12 +57,28 @@ export function publicFileCheck(publicDir: string): (file: string) => boolean {
|
|
|
56
57
|
// its own. Otherwise every icon file it has is linked: the first of
|
|
57
58
|
// PRIMARY_ICON_FILES as the favicon (site.icon still wins in the document),
|
|
58
59
|
// plus icon.png and apple-touch-icon.png when present.
|
|
60
|
+
//
|
|
61
|
+
// The web manifest: the app's own public/manifest.webmanifest (or
|
|
62
|
+
// manifest.json) is linked whenever present. Otherwise June generates one only
|
|
63
|
+
// alongside its own icons — an app with custom icons may have no 192/512 PNGs
|
|
64
|
+
// for a manifest to point at.
|
|
59
65
|
export function resolveIcons(
|
|
60
66
|
site: Site,
|
|
61
67
|
inPublic: (file: string) => boolean,
|
|
62
68
|
): { generate: boolean; icons: NonNullable<DocumentConfig["icons"]> } {
|
|
69
|
+
const ownManifest = MANIFEST_FILES.find(inPublic);
|
|
63
70
|
const custom = Boolean(site.icon) || CUSTOM_ICON_FILES.some(inPublic);
|
|
64
|
-
if (!custom)
|
|
71
|
+
if (!custom) {
|
|
72
|
+
return {
|
|
73
|
+
generate: true,
|
|
74
|
+
icons: {
|
|
75
|
+
png: "/icon.png",
|
|
76
|
+
appleTouch: "/apple-touch-icon.png",
|
|
77
|
+
manifest: ownManifest ? `/${ownManifest}` : "/manifest.webmanifest",
|
|
78
|
+
generated: !ownManifest,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
65
82
|
const primary = PRIMARY_ICON_FILES.find(inPublic);
|
|
66
83
|
return {
|
|
67
84
|
generate: false,
|
|
@@ -69,6 +86,7 @@ export function resolveIcons(
|
|
|
69
86
|
primary: primary ? `/${primary}` : undefined,
|
|
70
87
|
png: inPublic("icon.png") ? "/icon.png" : undefined,
|
|
71
88
|
appleTouch: inPublic("apple-touch-icon.png") ? "/apple-touch-icon.png" : undefined,
|
|
89
|
+
manifest: ownManifest ? `/${ownManifest}` : undefined,
|
|
72
90
|
},
|
|
73
91
|
};
|
|
74
92
|
}
|
|
@@ -179,10 +197,12 @@ const escapeXml = (s: string) => s.replace(/[<>&"']/g, (c) => `&#${c.charCodeAt(
|
|
|
179
197
|
// Where the glyph goes on the 64×64 square: font size + origin, from its INK
|
|
180
198
|
// box — not its em box — so every script sits optically centred at one size.
|
|
181
199
|
// A probe render measures the ink; cap height and a CJK ideograph both come out
|
|
182
|
-
// INK_H tall, and a wide glyph is held to INK_W.
|
|
200
|
+
// INK_H tall, and a wide glyph is held to INK_W. The ink box's corners stay
|
|
201
|
+
// within 0.4 of the size from the centre, which is the maskable-icon safe zone,
|
|
202
|
+
// so the manifest can mark the full-bleed art "maskable" (pinned in a test).
|
|
183
203
|
const PROBE = 1000;
|
|
184
|
-
const INK_H = 28;
|
|
185
|
-
const INK_W = 40;
|
|
204
|
+
export const INK_H = 28;
|
|
205
|
+
export const INK_W = 40;
|
|
186
206
|
type Placement = { family: string; font: Uint8Array; size: number; x: number; y: number };
|
|
187
207
|
|
|
188
208
|
function inkBox(resvg: ResvgModule, ch: string, family: string, font: Uint8Array) {
|
|
@@ -248,6 +268,11 @@ export type GeneratedIcons = {
|
|
|
248
268
|
"apple-touch-icon.png": Uint8Array<ArrayBuffer>;
|
|
249
269
|
"icon.png": Uint8Array<ArrayBuffer>;
|
|
250
270
|
"favicon.ico": Uint8Array<ArrayBuffer>;
|
|
271
|
+
// The web manifest's icons (Android home screen). Full-bleed like the apple
|
|
272
|
+
// icon: the glyph's ink stays inside the maskable safe zone (a centred circle
|
|
273
|
+
// of radius 0.4), so one image serves "any" and "maskable".
|
|
274
|
+
"icon-192.png": Uint8Array<ArrayBuffer>;
|
|
275
|
+
"icon-512.png": Uint8Array<ArrayBuffer>;
|
|
251
276
|
// What was drawn, for the build log and tests: the character and its font
|
|
252
277
|
// family, or family null when the square went out without one.
|
|
253
278
|
letter: string;
|
|
@@ -292,6 +317,8 @@ export async function renderIcons(opts: {
|
|
|
292
317
|
"apple-touch-icon.png": render(180, false),
|
|
293
318
|
"icon.png": icon32,
|
|
294
319
|
"favicon.ico": pngToIco(icon32, 32),
|
|
320
|
+
"icon-192.png": render(192, false),
|
|
321
|
+
"icon-512.png": render(512, false),
|
|
295
322
|
letter: ch,
|
|
296
323
|
family: placement?.family ?? null,
|
|
297
324
|
};
|
package/src/pipeline.ts
CHANGED
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
import type { Resources } from "@junejs/core/resources";
|
|
51
51
|
|
|
52
52
|
import { iconLetter } from "./icon-letter";
|
|
53
|
+
import { webManifest } from "./web-manifest";
|
|
53
54
|
import { negotiate, TITLE_HEADER, SEGMENT_HEADER, encodeTitle } from "./negotiate";
|
|
54
55
|
|
|
55
56
|
// Minimal Cookie-header read for the locale negotiation chain (no dependency on
|
|
@@ -543,6 +544,12 @@ export function createPipeline(cfg: PipelineConfig): Pipeline {
|
|
|
543
544
|
) {
|
|
544
545
|
return letterFavicon(docConfig.site.name);
|
|
545
546
|
}
|
|
547
|
+
// --- generated web manifest (only beside June's generated icons) ------
|
|
548
|
+
if (request.method === "GET" && docConfig.icons?.generated && url.pathname === "/manifest.webmanifest") {
|
|
549
|
+
return text(webManifest(docConfig), "application/manifest+json", {
|
|
550
|
+
headers: { "cache-control": "public, max-age=86400" },
|
|
551
|
+
});
|
|
552
|
+
}
|
|
546
553
|
|
|
547
554
|
// --- locale resolution (before routing) ------------------------------
|
|
548
555
|
// host/path → locale, stripping the locale prefix off the front so the
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// The generated web app manifest (/manifest.webmanifest) — Android's "Add to
|
|
2
|
+
// Home Screen" name, icons, and colours, built from the same config the
|
|
3
|
+
// document renders with. Worker-safe (no node:*): the pipeline serves it like
|
|
4
|
+
// the SVG favicon, so dev, the worker, and the static build all agree.
|
|
5
|
+
import { resolveThemeColor, type DocumentConfig } from "@junejs/core/document";
|
|
6
|
+
|
|
7
|
+
// The home-screen label: site.shortName, else the site name's leading segment
|
|
8
|
+
// ("June — build agents into real apps" → "June"). Launchers truncate past
|
|
9
|
+
// ~12 characters, and a tagline is never the label anyone wants.
|
|
10
|
+
export function shortName(site: DocumentConfig["site"]): string | undefined {
|
|
11
|
+
if (site.shortName) return site.shortName;
|
|
12
|
+
const name = site.name?.trim();
|
|
13
|
+
if (!name) return undefined;
|
|
14
|
+
return name.split(/\s+[—–|-]\s+|:\s+/)[0]!.trim() || name;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function webManifest(config: DocumentConfig): string {
|
|
18
|
+
const base = config.basePath ?? "";
|
|
19
|
+
const theme = resolveThemeColor(config);
|
|
20
|
+
// A manifest has ONE theme colour; a { light, dark } pair has no honest single
|
|
21
|
+
// value, so both colour fields are left to the browser then.
|
|
22
|
+
const color = typeof theme === "string" ? theme : undefined;
|
|
23
|
+
const manifest = {
|
|
24
|
+
name: config.site.name,
|
|
25
|
+
short_name: shortName(config.site),
|
|
26
|
+
description: config.site.description,
|
|
27
|
+
start_url: `${base}/`,
|
|
28
|
+
scope: `${base}/`,
|
|
29
|
+
// "browser": a home-screen shortcut that opens a normal tab. standalone
|
|
30
|
+
// (app-like, no URL bar) changes how a site behaves, so it stays opt-in via
|
|
31
|
+
// your own public/manifest.webmanifest.
|
|
32
|
+
display: "browser",
|
|
33
|
+
...(color ? { theme_color: color, background_color: color } : {}),
|
|
34
|
+
icons: [
|
|
35
|
+
{ src: `${base}/icon-192.png`, sizes: "192x192", type: "image/png" },
|
|
36
|
+
{ src: `${base}/icon-512.png`, sizes: "512x512", type: "image/png" },
|
|
37
|
+
// The same full-bleed art: its glyph sits inside the maskable safe zone.
|
|
38
|
+
{ src: `${base}/icon-512.png`, sizes: "512x512", type: "image/png", purpose: "maskable" },
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
return JSON.stringify(manifest, null, 2) + "\n";
|
|
42
|
+
}
|