denoland/deno · error

icon ' ' must be .icns or .png

Error message

icon '{}' must be .icns or .png

What it means

Thrown by resolve_hmr_icon_path (macOS only) when the single icon file exists but its extension is neither .icns nor .png — the only two formats the LAUFEY backend accepts via LAUFEY_APP_ICON / `-[NSImage initWithContentsOfFile:]`. The check is on the file extension, not on decoded image content.

Solutions

  1. Convert the image to PNG (`magick input.svg output.png`) or build a proper .icns with iconutil/Icon Composer.
  2. If the file IS a png/icns but misnamed, rename it to the exact lowercase extension: `mv AppIcon.PNG AppIcon.png`.
  3. Use a real conversion — merely changing the extension of a JPG/ICO does not change the file format and will fail at render time.

Example fix

# before
deno desktop --hmr --icon assets/AppIcon.svg main.ts

# after
magick assets/AppIcon.svg assets/AppIcon.png
deno desktop --hmr --icon assets/AppIcon.png main.ts
Defensive patterns

Strategy: validation

Validate before calling

# bash: enforce the accepted extensions
ICON="$1"
case "$ICON" in *.icns|*.png) ;; *) echo "icon must be .icns or .png" >&2; exit 1;; esac
deno desktop --hmr --icon "$ICON" main.ts

Type guard

// TypeScript
function isAcceptedHmrIcon(p: string): boolean {
  return p.endsWith(".icns") || p.endsWith(".png");
}

Prevention

When it happens

Trigger: `--icon assets/AppIcon.svg`, `.jpg`, `.jpeg`, `.ico`, `.webp`, or `.bmp` on macOS with `deno desktop --hmr`; a file with no extension; upper-case extensions like `.PNG` are also rejected because the match is on the exact lowercase strings "icns"/"png".

Common situations: Designers hand over SVG or JPG source assets; a .ico reused from a Windows build; file named `AppIcon.icns.txt` after a bad download; uppercase extension from a case-renaming script.

Related errors


AI-assisted analysis of denoland/deno@f7822238ca (2026-08-20). Data as JSON: /api/errors/6a88fa802e9efbbf. Report an issue: GitHub.

Appendix: source

Thrown at cli/tools/desktop.rs:1173

/// laufey passes to `-[NSImage initWithContentsOfFile:]` (both formats are
/// accepted, so no conversion is needed).
#[cfg(target_os = "macos")]
fn resolve_hmr_icon_path(
  icon: &crate::args::IconConfig,
  initial_cwd: &Path,
) -> Result<PathBuf, AnyError> {
  let icon_path = match icon {
    crate::args::IconConfig::Single(p) => initial_cwd.join(p),
    crate::args::IconConfig::Set(_) => {
      deno_core::anyhow::bail!("icon sets are not supported in --hmr mode yet")
    }
  };
  if !icon_path.exists() {
    deno_core::anyhow::bail!("icon '{}' not found", icon_path.display());
  }
  match icon_path.extension().and_then(|e| e.to_str()) {
    Some("icns") | Some("png") => {}
    _ => deno_core::anyhow::bail!(
      "icon '{}' must be .icns or .png",
      icon_path.display()
    ),
  }
  Ok(crate::util::fs::canonicalize_path(&icon_path).unwrap_or(icon_path))
}

/// Extract the local URL from a line of dev server output.
fn parse_dev_server_url(line: &str) -> Option<String> {
  let line = console_static_text::ansi::strip_ansi_codes(line);
  // Vite prints `  ➜  Local:   http://localhost:5173/`
  regex::Regex::new(r"Local:\s+(https?://\S+)")
    .expect("regex to parse local url failed")
    .captures(&line)
    .and_then(|c| c.get(1))
    .map(|m| m.as_str().to_owned())
}

View on GitHub (pinned to f7822238ca)