kizlo 0.6.1 → 0.7.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.
@@ -7,29 +7,27 @@ interface IconDescriptor {
7
7
  type: string;
8
8
  /** `"any"` for scalable/single sources; omitted for `.ico`. */
9
9
  sizes?: string;
10
- /** Media query for scheme-specific variants, e.g. `(prefers-color-scheme: dark)`. */
11
- media?: string;
12
10
  }
13
11
  interface ManifestIcon {
14
12
  src: string;
15
13
  type: string;
16
14
  sizes: string;
17
- /**
18
- * `"maskable"` marks art with a safe zone the launcher may crop to any
19
- * adaptive shape; omitted (default `"any"`) sources render uncropped.
20
- */
21
- purpose?: "any" | "maskable";
22
15
  }
23
16
  interface ResolvedIcons {
24
- /** `rel="icon"` candidates: the light source plus an optional dark variant. */
17
+ /**
18
+ * `rel="icon"` candidate: a single scheme-agnostic source, or empty. Browsers
19
+ * disagree on scheme-swapping favicons (Safari ignores the `media` attribute
20
+ * and never re-evaluates an SVG's internal `@media`), so one icon that reads
21
+ * on both light and dark tab bars is the only cross-browser-safe surface.
22
+ */
25
23
  icon: IconDescriptor[];
26
24
  /** `rel="apple-touch-icon"` candidate (raster-guarded), or empty. */
27
25
  appleTouch: IconDescriptor[];
28
26
  /**
29
- * Web-manifest icon entries (raster or SVG): the square brand icon as the
30
- * default `"any"` source — expanded to one entry per real square rendition —
31
- * plus the dedicated Android icon as `"maskable"` when provided. Either may
32
- * be absent.
27
+ * Web-manifest icon entries (raster or SVG): a single square source —
28
+ * expanded to one entry per real square rendition — that every non-iOS
29
+ * install surface (Android, Chrome, macOS Safari) renders uncropped. Absent
30
+ * when no manifest-compatible source exists.
33
31
  */
34
32
  manifestIcons: ManifestIcon[];
35
33
  }
@@ -32,12 +32,20 @@ function manifestSource(media) {
32
32
  function pixelSize(media) {
33
33
  return media.width && media.height ? `${media.width}x${media.height}` : "any";
34
34
  }
35
- function toDescriptor(media, extra) {
35
+ /**
36
+ * Append the attachment id as a `?v=` cache-buster. Safari (and every browser)
37
+ * keys its favicon cache by URL, so tying the version to the attachment id means
38
+ * the URL changes when a different icon is chosen — busting the stale cache —
39
+ * while staying stable across renders when the icon is unchanged.
40
+ */
41
+ function versioned(src, id) {
42
+ return `${src}${src.includes("?") ? "&" : "?"}v=${id}`;
43
+ }
44
+ function toDescriptor(media) {
36
45
  return {
37
- url: media.src,
46
+ url: versioned(media.src, media.id),
38
47
  type: media.mime ?? "",
39
- sizes: media.mime && ICO_MIMES.has(media.mime) ? void 0 : pixelSize(media),
40
- ...extra
48
+ sizes: media.mime && ICO_MIMES.has(media.mime) ? void 0 : pixelSize(media)
41
49
  };
42
50
  }
43
51
  /**
@@ -47,14 +55,12 @@ function toDescriptor(media, extra) {
47
55
  * or above the floor so browsers get genuine 192/512-ish icons; if none qualify
48
56
  * (e.g. a non-square upload) the original is used as-is.
49
57
  */
50
- function toManifestIcons(media, purpose) {
58
+ function toManifestIcons(media) {
51
59
  const type = media.mime ?? "";
52
- const withPurpose = purpose ? { purpose } : {};
53
60
  if (media.mime === SVG_MIME) return [{
54
61
  src: media.src,
55
62
  type,
56
- sizes: "any",
57
- ...withPurpose
63
+ sizes: "any"
58
64
  }];
59
65
  const square = [{
60
66
  src: media.src,
@@ -75,8 +81,7 @@ function toManifestIcons(media, purpose) {
75
81
  icons.push({
76
82
  src: r.src,
77
83
  type,
78
- sizes,
79
- ...withPurpose
84
+ sizes
80
85
  });
81
86
  }
82
87
  return icons;
@@ -87,16 +92,13 @@ function toManifestIcons(media, purpose) {
87
92
  */
88
93
  function resolveIcons(brand) {
89
94
  const icon = [];
90
- const light = brand.favicon ?? brand.logo_icon;
91
- if (light) icon.push(toDescriptor(light));
92
- if (brand.favicon_dark) icon.push(toDescriptor(brand.favicon_dark, { media: "(prefers-color-scheme: dark)" }));
93
- const apple = raster(brand.ios_app_icon) ?? raster(brand.favicon);
95
+ const primary = brand.favicon ?? brand.logo_icon;
96
+ if (primary) icon.push(toDescriptor(primary));
97
+ const apple = raster(brand.app_icon) ?? raster(brand.favicon);
94
98
  const appleTouch = apple ? [toDescriptor(apple)] : [];
95
99
  const manifestIcons = [];
96
- const anySource = manifestSource(brand.logo_icon) ?? manifestSource(brand.favicon);
100
+ const anySource = manifestSource(brand.app_icon) ?? manifestSource(brand.logo_icon) ?? manifestSource(brand.favicon);
97
101
  if (anySource) manifestIcons.push(...toManifestIcons(anySource));
98
- const maskable = manifestSource(brand.android_app_icon);
99
- if (maskable) manifestIcons.push(...toManifestIcons(maskable, "maskable"));
100
102
  return {
101
103
  icon,
102
104
  appleTouch,
@@ -11,7 +11,6 @@ import { join } from "node:path";
11
11
  import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
12
12
  import { defineCommand } from "citty";
13
13
  import { randomBytes } from "node:crypto";
14
- import { networkInterfaces } from "node:os";
15
14
 
16
15
  //#region src/cli/commands/dev.ts
17
16
  async function resolve$1(cwd) {
@@ -26,22 +25,17 @@ function note(text) {
26
25
  const { dim, reset } = palette();
27
26
  process.stdout.write(` ${dim}${text}${reset}\n`);
28
27
  }
29
- /** First non-internal IPv4 address, for the LAN-reachable "Network" URL (undefined when offline). */
30
- function lanAddress() {
31
- for (const ifaces of Object.values(networkInterfaces())) for (const iface of ifaces ?? []) if (iface.family === "IPv4" && !iface.internal) return iface.address;
32
- }
33
28
  /**
34
- * Print the connection summary as an aligned, Next.js-style block. Shows the wp-admin
35
- * URL on localhost ("Local") and — when a LAN address exists — on the network, so the
36
- * stack can be opened from other devices. On a fresh install, also show the one-time
37
- * wp-admin password (it's never stored, so this is the only chance to see it).
29
+ * Print the connection summary as an aligned, Next.js-style block. Shows the wp-admin URL on
30
+ * the loopback ("Local") and — when the stack is installed on a LAN address rather than
31
+ * localhost — on the network, so it can be opened from other devices. On a fresh install, also
32
+ * show the one-time wp-admin password (it's never stored, so this is the only chance to see it).
38
33
  */
39
34
  function printSummary(info) {
40
35
  const { cyan, dim, reset } = palette();
41
- const port = new URL(info.url).port;
42
- const lan = lanAddress();
43
- const rows = [["WP Local", `${info.url}/wp-admin`]];
44
- if (lan) rows.push(["WP Network", `http://${lan}:${port}/wp-admin`]);
36
+ const { port, hostname } = new URL(info.url);
37
+ const rows = [["WP Local", `http://localhost:${port}/wp-admin`]];
38
+ if (hostname !== "localhost") rows.push(["WP Network", `${info.url}/wp-admin`]);
45
39
  rows.push(["Database Connection", `127.0.0.1:${info.dbPort}`]);
46
40
  const width = Math.max(...rows.map(([label]) => label.length)) + 1;
47
41
  const lines = ["", ...rows.map(([label, value]) => ` ${dim}- ${`${label}:`.padEnd(width)}${reset} ${cyan}${value}${reset}`)];
@@ -104,6 +98,23 @@ function updateWpEnv(cfg, info) {
104
98
  };
105
99
  }
106
100
  /**
101
+ * Re-point `KIZLO_DEV_WORDPRESS_URL` at the current dev URL on a warm resume, so a LAN address that
102
+ * changed since the last session (a new DHCP lease, a different network) doesn't leave the app talking
103
+ * to a dead IP. Only this one key is rewritten — the credentials and secret from the fresh install are
104
+ * preserved — and it's a no-op when `.env` is absent (nothing provisioned yet) or already current.
105
+ * The full fresh-install write goes through {@link updateWpEnv} instead; this covers the resume path it
106
+ * skips. Returns true when it changed the file. The app still needs a restart to read the new value.
107
+ */
108
+ function syncDevUrl(cfg, url) {
109
+ const envPath = join(cfg.configDir, ".env");
110
+ if (!existsSync(envPath)) return false;
111
+ const existing = readFileSync(envPath, "utf8");
112
+ if (readEnvValue(existing, "KIZLO_DEV_WORDPRESS_URL") === url) return false;
113
+ const { content } = mergeEnv(existing, { KIZLO_DEV_WORDPRESS_URL: url }, new Set(["KIZLO_DEV_WORDPRESS_URL"]), envGroups("KIZLO_BACKEND_URL"));
114
+ writeFileSync(envPath, content);
115
+ return true;
116
+ }
117
+ /**
107
118
  * Arm the foreground teardown — call this *before* the containers start, so a cancel at
108
119
  * any point (even mid-startup, with mysql up but wordpress still booting) stops whatever
109
120
  * already came up. The teardown is owned by a detached **watchdog** process, not by this
@@ -189,7 +200,7 @@ async function startForeground(cfg) {
189
200
  });
190
201
  if (!sync.ok) note(`Could not sync the site settings to WordPress (${sync.error}) — make sure the kizlo plugin is active, then set them from the Kizlo settings.`);
191
202
  }
192
- }
203
+ } else if (syncDevUrl(ready, creds.url)) note("Updated .env WordPress URL to the current network address — restart your app to pick it up");
193
204
  printSummary(creds);
194
205
  const { green, dim, bold, reset } = palette();
195
206
  const ms = Date.now() - start;
@@ -7,12 +7,27 @@ import { prepareByo } from "./byo.js";
7
7
  import { join } from "node:path";
8
8
  import { existsSync, rmSync } from "node:fs";
9
9
  import { randomBytes } from "node:crypto";
10
+ import { networkInterfaces } from "node:os";
10
11
 
11
12
  //#region src/cli/wp/dev.ts
12
13
  /** A strong random admin password (144 bits, URL/JSON/CLI-safe characters). */
13
14
  function generatePassword() {
14
15
  return randomBytes(18).toString("base64url");
15
16
  }
17
+ /** First non-internal IPv4 address — the router-assigned LAN address (undefined when offline). */
18
+ function lanAddress() {
19
+ for (const ifaces of Object.values(networkInterfaces())) for (const iface of ifaces ?? []) if (iface.family === "IPv4" && !iface.internal) return iface.address;
20
+ }
21
+ /**
22
+ * The URL the dev stack is provisioned at. We prefer the router-assigned LAN address over `localhost`
23
+ * so it's reachable from off the host (the app in a container or on another device). Serving WordPress
24
+ * under the request host is already handled by the container's `WORDPRESS_CONFIG_EXTRA` (it derives
25
+ * `WP_HOME`/`WP_SITEURL` from `HTTP_HOST` per request), so this is only the address we write into `.env`
26
+ * and print. Falls back to `localhost` when the machine is offline, so bare local dev still works.
27
+ */
28
+ function devUrl(port) {
29
+ return `http://${lanAddress() ?? "localhost"}:${port}`;
30
+ }
16
31
  /**
17
32
  * Seed `dev.fixtures` into a freshly installed dev stack, reusing the test seeding
18
33
  * primitives: seed the default subscriber so `ctx.userId` exists, then run each `seed`
@@ -58,7 +73,7 @@ async function seedDevFixtures(cfg, url) {
58
73
  * concern.
59
74
  */
60
75
  async function bootstrapDev(cfg) {
61
- const url = `http://localhost:${cfg.port}`;
76
+ const url = devUrl(cfg.port);
62
77
  const byo = !existsSync(join(cfg.wordpressDir, "wp-includes", "version.php")) && cfg.byo ? await prepareByo(cfg.byo, cfg.wordpressDir, cfg.configDir) : void 0;
63
78
  await composeUp();
64
79
  const installed = (await compose([
@@ -45,8 +45,7 @@ function toNextIcon(icon) {
45
45
  return {
46
46
  url: icon.url,
47
47
  type: icon.type,
48
- sizes: icon.sizes,
49
- media: icon.media
48
+ sizes: icon.sizes
50
49
  };
51
50
  }
52
51
  function createPageMetadata(head) {
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.6.1";
2
+ var version = "0.7.0";
3
3
 
4
4
  //#endregion
5
5
  export { version };
package/dist/router.d.ts CHANGED
@@ -779,9 +779,7 @@ declare const ROUTER_MAP: {
779
779
  logo_wordmark?: number | null | undefined;
780
780
  logo_wordmark_dark?: number | null | undefined;
781
781
  favicon?: number | null | undefined;
782
- favicon_dark?: number | null | undefined;
783
- ios_app_icon?: number | null | undefined;
784
- android_app_icon?: number | null | undefined;
782
+ app_icon?: number | null | undefined;
785
783
  theme_color?: number | null | undefined;
786
784
  theme_color_dark?: number | null | undefined;
787
785
  background_color?: number | null | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kizlo",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "description": "A headless WordPress toolkit for TypeScript.",
6
6
  "keywords": [
@@ -76,7 +76,7 @@
76
76
  },
77
77
  "dependencies": {
78
78
  "@clack/prompts": "^1.5.1",
79
- "@kizlo/shared": "0.3.1",
79
+ "@kizlo/shared": "0.4.0",
80
80
  "@orpc/client": "^1.14.3",
81
81
  "@orpc/contract": "^1.14.3",
82
82
  "@orpc/openapi": "^1.14.3",