feat(cli): UTExportedTypeDeclarations support for file associations - #12
Conversation
📝 WalkthroughWalkthroughIntroduces support for macOS file associations by adding content type declarations (LSItemContentTypes) and exported type definitions (UTExportedTypeDeclarations) to the Tauri configuration schema and bundler implementation. Two new fields extend FileAssociation, with a new ExportedFileAssociation struct supporting identifier and type conformance declarations. Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~22 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Fix all issues with AI agents
In `@crates/tauri-bundler/src/bundle/macos/app.rs`:
- Around line 328-339: The current check uses if association.ext.is_empty(),
which inserts CFBundleTypeExtensions only when there are no extensions; change
the logic to insert CFBundleTypeExtensions into dict only when association.ext
is non-empty and when association.content_types is not provided
(LSItemContentTypes should take precedence). Concretely, replace the condition
around dict.insert(...) to something like: check that association.ext is not
empty (e.g., !association.ext.is_empty()) and that association.content_types is
absent/empty (e.g., association.content_types.is_empty() or
association.content_types.is_none()), then map association.ext into the
plist::Value::Array and insert "CFBundleTypeExtensions".
- Around line 284-289: The code is populating UTTypeConformsTo with
association.content_types but should use the exported_type.conforms_to field;
update the block that inserts "UTTypeConformsTo" (in the function building the
plist for CFBundleDocumentTypes / ExportedFileAssociation handling) to read from
exported_type.conforms_to instead of association.content_types, and leave
LSItemContentTypes populated from association.content_types as currently
intended so each key uses the correct source fields (UTTypeConformsTo <-
exported_type.conforms_to, LSItemContentTypes <- association.content_types).
- Around line 348-356: The code currently calls association.name.expect(...)
when inserting "CFBundleTypeName", which can panic; instead implement the
documented default by using FileAssociation.name when present or falling back to
the first extension in FileAssociation.ext when name is None, and if ext is
empty return/propagate a proper error rather than panicking. Update the
insertion site that creates the CFBundleTypeName (the dict.insert call in
app.rs) to compute the name via association.name or association.ext.get(0) and
convert to String, and ensure the surrounding function returns a Result (or
otherwise surfaces an error) if neither name nor a first ext exists so no
.expect() is used.
🧹 Nitpick comments (1)
examples/file-associations/src-tauri/tauri.conf.json (1)
41-53: Optional: consider addingmimeTypefor custom types.
If you want the generated macOS UTI declaration to include a MIME tag, addingmimeTypehere would enable that.
| if let Some(content_types) = &association.content_types { | ||
| dict.insert( | ||
| "UTTypeConformsTo".into(), | ||
| plist::Value::Array(content_types.iter().map(|s| s.clone().into()).collect()), | ||
| ); | ||
| } |
There was a problem hiding this comment.
Wrong source for UTTypeConformsTo — uses association.content_types instead of exported_type.conforms_to.
According to the ExportedFileAssociation struct definition, conforms_to is the field meant to populate UTTypeConformsTo. The current code incorrectly uses association.content_types, which is intended for LSItemContentTypes in CFBundleDocumentTypes.
🐛 Proposed fix
- if let Some(content_types) = &association.content_types {
+ if let Some(conforms_to) = &exported_type.conforms_to {
dict.insert(
"UTTypeConformsTo".into(),
- plist::Value::Array(content_types.iter().map(|s| s.clone().into()).collect()),
+ plist::Value::Array(conforms_to.iter().map(|s| s.clone().into()).collect()),
);
}🤖 Prompt for AI Agents
In `@crates/tauri-bundler/src/bundle/macos/app.rs` around lines 284 - 289, The
code is populating UTTypeConformsTo with association.content_types but should
use the exported_type.conforms_to field; update the block that inserts
"UTTypeConformsTo" (in the function building the plist for CFBundleDocumentTypes
/ ExportedFileAssociation handling) to read from exported_type.conforms_to
instead of association.content_types, and leave LSItemContentTypes populated
from association.content_types as currently intended so each key uses the
correct source fields (UTTypeConformsTo <- exported_type.conforms_to,
LSItemContentTypes <- association.content_types).
| if association.ext.is_empty() { | ||
| dict.insert( | ||
| "CFBundleTypeExtensions".into(), | ||
| plist::Value::Array( | ||
| association | ||
| .ext | ||
| .iter() | ||
| .map(|ext| ext.to_string().into()) | ||
| .collect(), | ||
| ), | ||
| ); | ||
| } |
There was a problem hiding this comment.
Inverted condition inserts an empty array when ext is empty.
The condition if association.ext.is_empty() causes CFBundleTypeExtensions to be inserted only when there are no extensions, resulting in an empty array. The logic should be inverted to insert extensions when they exist and when content_types is not provided (since LSItemContentTypes takes precedence).
🐛 Proposed fix
- if association.ext.is_empty() {
+ if association.content_types.is_none() && !association.ext.is_empty() {
dict.insert(
"CFBundleTypeExtensions".into(),
plist::Value::Array(
association
.ext
.iter()
.map(|ext| ext.to_string().into())
.collect(),
),
);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if association.ext.is_empty() { | |
| dict.insert( | |
| "CFBundleTypeExtensions".into(), | |
| plist::Value::Array( | |
| association | |
| .ext | |
| .iter() | |
| .map(|ext| ext.to_string().into()) | |
| .collect(), | |
| ), | |
| ); | |
| } | |
| if association.content_types.is_none() && !association.ext.is_empty() { | |
| dict.insert( | |
| "CFBundleTypeExtensions".into(), | |
| plist::Value::Array( | |
| association | |
| .ext | |
| .iter() | |
| .map(|ext| ext.to_string().into()) | |
| .collect(), | |
| ), | |
| ); | |
| } |
🤖 Prompt for AI Agents
In `@crates/tauri-bundler/src/bundle/macos/app.rs` around lines 328 - 339, The
current check uses if association.ext.is_empty(), which inserts
CFBundleTypeExtensions only when there are no extensions; change the logic to
insert CFBundleTypeExtensions into dict only when association.ext is non-empty
and when association.content_types is not provided (LSItemContentTypes should
take precedence). Concretely, replace the condition around dict.insert(...) to
something like: check that association.ext is not empty (e.g.,
!association.ext.is_empty()) and that association.content_types is absent/empty
(e.g., association.content_types.is_empty() or
association.content_types.is_none()), then map association.ext into the
plist::Value::Array and insert "CFBundleTypeExtensions".
| dict.insert( | ||
| "CFBundleTypeName".into(), | ||
| association | ||
| .name | ||
| .as_ref() | ||
| .unwrap_or(&association.ext[0].0) | ||
| .expect("File association must have a name") | ||
| .to_string() | ||
| .into(), | ||
| ); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# First, check the exact code at the mentioned lines
echo "=== Code at lines 348-356 ==="
sed -n '345,360p' crates/tauri-bundler/src/bundle/macos/app.rs
echo -e "\n=== File size check ==="
wc -l crates/tauri-bundler/src/bundle/macos/app.rsRepository: agentic-review-benchmarks/tauri-combined-coderabbit
Length of output: 632
🏁 Script executed:
#!/bin/bash
# Find FileAssociation struct definition
echo "=== Searching for FileAssociation struct ==="
rg -n "struct FileAssociation" --type rust -A 10Repository: agentic-review-benchmarks/tauri-combined-coderabbit
Length of output: 1417
🏁 Script executed:
#!/bin/bash
# Search for validation of file associations
echo "=== Searching for file_associations validation ==="
rg -n "file_associations" --type rust -B 3 -A 3 | head -100Repository: agentic-review-benchmarks/tauri-combined-coderabbit
Length of output: 7702
🏁 Script executed:
#!/bin/bash
# Check for any validation or deserialization logic
echo "=== Searching for validation or required patterns ==="
rg -n "FileAssociation.*name|name.*required|validate.*file.*assoc" --type rust -iRepository: agentic-review-benchmarks/tauri-combined-coderabbit
Length of output: 717
🏁 Script executed:
#!/bin/bash
# Search for where FileAssociation.name gets defaulted
echo "=== Searching for default name logic ==="
rg -n "ext\[0\]|FileAssociation.*default" --type rust -B 3 -A 3Repository: agentic-review-benchmarks/tauri-combined-coderabbit
Length of output: 1070
🏁 Script executed:
#!/bin/bash
# Check the full FileAssociation struct definition
echo "=== Full FileAssociation struct ==="
sed -n '1177,1210p' crates/tauri-utils/src/config.rsRepository: agentic-review-benchmarks/tauri-combined-coderabbit
Length of output: 2035
🏁 Script executed:
#!/bin/bash
# Look for post-processing or validation of FileAssociation
echo "=== Search for processing of file associations ==="
rg -n "association\.name|for.*association" --type rust crates/tauri-bundler/src/bundle/macos/app.rs -B 2 -A 2Repository: agentic-review-benchmarks/tauri-combined-coderabbit
Length of output: 145
🏁 Script executed:
#!/bin/bash
# Check git history for this specific code section to see if it changed
echo "=== Git log for the app.rs file ==="
git log --oneline -n 20 crates/tauri-bundler/src/bundle/macos/app.rsRepository: agentic-review-benchmarks/tauri-combined-coderabbit
Length of output: 149
🏁 Script executed:
#!/bin/bash
# Let's search more broadly in the macos app.rs file for how associations are handled
echo "=== Full context around line 348 in macos/app.rs ==="
sed -n '265,285p' crates/tauri-bundler/src/bundle/macos/app.rsRepository: agentic-review-benchmarks/tauri-combined-coderabbit
Length of output: 947
🏁 Script executed:
#!/bin/bash
# Search for where the default to ext[0] might be applied
echo "=== Searching for ext field access with name ==="
rg -n "\.ext\[|ext\.get|\.name.*=|name.*=.*ext" --type rust -B 2 -A 2 | head -80Repository: agentic-review-benchmarks/tauri-combined-coderabbit
Length of output: 6179
🏁 Script executed:
#!/bin/bash
# Check the entire block where associations are processed
echo "=== Full association processing in macos/app.rs ==="
sed -n '270,360p' crates/tauri-bundler/src/bundle/macos/app.rsRepository: agentic-review-benchmarks/tauri-combined-coderabbit
Length of output: 3069
The .expect() on association.name will panic if not provided, despite struct documentation promising a default.
The FileAssociation.name field is Option<String> with documentation stating "Default to ext[0]", but no code applies this default. Either implement the documented default behavior (populate name from ext[0] during deserialization/validation) or enforce name as required at the config validation layer instead of relying on .expect() at runtime.
🤖 Prompt for AI Agents
In `@crates/tauri-bundler/src/bundle/macos/app.rs` around lines 348 - 356, The
code currently calls association.name.expect(...) when inserting
"CFBundleTypeName", which can panic; instead implement the documented default by
using FileAssociation.name when present or falling back to the first extension
in FileAssociation.ext when name is None, and if ext is empty return/propagate a
proper error rather than panicking. Update the insertion site that creates the
CFBundleTypeName (the dict.insert call in app.rs) to compute the name via
association.name or association.ext.get(0) and convert to String, and ensure the
surrounding function returns a Result (or otherwise surfaces an error) if
neither name nor a first ext exists so no .expect() is used.
Benchmark PR from agentic-review-benchmarks#12
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.