feat(cli): UTExportedTypeDeclarations support for file associations#38
Conversation
WalkthroughThis PR introduces support for defining custom file types and content types in file associations on macOS. New configuration fields ( Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 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
Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Fix all issues with AI agents
In `@crates/tauri-bundler/src/bundle/macos/app.rs`:
- Around line 328-339: The condition around inserting CFBundleTypeExtensions is
inverted: currently it inserts an array when association.ext is empty and skips
when non-empty; change the guard to only insert when association.ext is
non-empty (i.e., replace if association.ext.is_empty() with if
!association.ext.is_empty()) so the dict.insert call for
"CFBundleTypeExtensions" uses the real extensions from
association.ext.iter().map(...).collect(); keep the existing dict.insert and
mapping logic but flip the boolean check to correctly register file extensions.
- Around line 349-355: The code currently calls association.name.expect(...)
which will panic for valid configs because name is optional; update the logic
around CFBundleTypeName construction in app.rs to avoid hard panics by using a
fallback (e.g., use the first extension from association.extensions when name is
None) or return a structured error; locate the block that sets
"CFBundleTypeName" and replace the expect usage on association.name with safe
handling (map/if-let or similar) that either uses the first extension as a
string fallback or propagates an error from the surrounding function instead of
panicking.
- Around line 281-289: Replace the current wiring of UTTypeConformsTo (which
uses association.content_types) to instead use the exported type's conforms_to
field when present: in the block around dict insertion for "UTTypeConformsTo"
(near where association.description and association.content_types are handled),
check the exported_type.conforms_to and insert those values into dict as an
Array for "UTTypeConformsTo"; only fall back to association.content_types if
exported_type.conforms_to is absent, so you don't duplicate LSItemContentTypes
semantics.
In `@crates/tauri-schema-generator/schemas/config.schema.json`:
- Around line 2436-2444: Update the description text for the "contentTypes"
schema to use the config-surface camelCase field names instead of Rust-style
snake_case: replace references to Self::exported_type with exportedType and
ExportedFileAssociation::conforms_to with ExportedFileAssociation::conformsTo
(or just conformsTo when referring to the config field) so the docs match the
JSON config names and avoid confusion.
🧹 Nitpick comments (1)
examples/file-associations/README.md (1)
11-13: Consider updating the build command.The
--features tauri/protocol-assetflag is now redundant since the feature is enabled by default inCargo.toml. Consider simplifying to:- cargo build --features tauri/protocol-asset + cargo build
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
.changes/file-association-content-type.md.changes/file-association-exported-type-cli.md.changes/file-association-exported-type.mdcrates/tauri-bundler/src/bundle/macos/app.rscrates/tauri-cli/config.schema.jsoncrates/tauri-schema-generator/schemas/config.schema.jsoncrates/tauri-utils/src/config.rsexamples/file-associations/README.mdexamples/file-associations/src-tauri/Cargo.tomlexamples/file-associations/src-tauri/tauri.conf.json
🔇 Additional comments (16)
examples/file-associations/src-tauri/Cargo.toml (1)
14-14: LGTM! Feature addition aligns with file associations functionality.The
protocol-assetfeature is correctly enabled for the file associations example. Note that the README (line 12) still instructs users to pass--features tauri/protocol-asset, which is now redundant since the feature is enabled by default inCargo.toml.crates/tauri-utils/src/config.rs (3)
1180-1185: LGTM! Well-documented content_types field.The field is properly documented with macOS-specific context, includes the appropriate serde alias for JSON kebab-case compatibility, and correctly references related types.
1199-1217: LGTM! Clean implementation of exported type declarations.The
exported_typefield andExportedFileAssociationstruct are well-designed:
- Required
identifierfield for the UTTypeIdentifier.- Optional
conforms_towith helpful examples in documentation.- Proper serde attributes following existing patterns in the codebase.
1383-1384: LGTM! Improved documentation clarity..changes/file-association-exported-type.md (1)
1-5: LGTM! Changeset accurately documents the feature addition..changes/file-association-content-type.md (1)
1-6: LGTM! Changeset correctly documents CLI feature for content type support..changes/file-association-exported-type-cli.md (1)
1-6: LGTM! Changeset correctly documents CLI feature for exported type declarations.examples/file-associations/README.md (1)
14-19: LGTM! Clear documentation of the new file associations.The new section clearly explains the supported file types and the macOS-specific
exportedTypehandling for custom extensions.examples/file-associations/src-tauri/tauri.conf.json (1)
2-53: LGTM for the updated example config.The new exportedType examples and assetProtocol enablement align with the intended macOS file-association enhancements.
crates/tauri-schema-generator/schemas/config.schema.json (3)
2162-2169: Clearer file-association description.Nice improvement to the wording here.
2485-2494: Schema shape forexportedTypelooks good.
2576-2599: ExportedFileAssociation definition is well-scoped.crates/tauri-cli/config.schema.json (4)
2161-2162: LGTM!The updated description "File types to associate with the application." is clearer and more accurate than the previous wording.
2436-2445: LGTM!The
contentTypesproperty is correctly defined as an optional array of strings, properly mapping to macOS'sLSItemContentTypes. The schema structure follows the established patterns in this file.
2484-2495: LGTM!The
exportedTypeproperty correctly uses theanyOfpattern with a$reftoExportedFileAssociationandnull, which is consistent with other optional object properties in this schema. The description clearly explains the macOSUTExportedTypeDeclarationsmapping.
2576-2599: LGTM!The
ExportedFileAssociationdefinition is well-structured:
identifieris correctly marked as required (mandatory forUTTypeIdentifier)conformsTois properly optional with valid example UTI types in the descriptionadditionalProperties: falseensures strict schema validationThe definition correctly models macOS's
UTExportedTypeDeclarationsentry structure.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
| if let Some(description) = &association.description { | ||
| dict.insert("UTTypeDescription".into(), description.clone().into()); | ||
| } | ||
| 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.
Use the exported type’s conforms_to for UTTypeConformsTo.
UTTypeConformsTo should reflect the exported type definition; wiring it to association.content_types ignores exported_type.conforms_to and duplicates LSItemContentTypes semantics.
🛠️ Suggested fix
- if let Some(content_types) = &association.content_types {
- dict.insert(
- "UTTypeConformsTo".into(),
- plist::Value::Array(content_types.iter().map(|s| s.clone().into()).collect()),
- );
- }
+ if let Some(conforms_to) = &exported_type.conforms_to {
+ dict.insert(
+ "UTTypeConformsTo".into(),
+ plist::Value::Array(conforms_to.iter().map(|s| s.clone().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 let Some(description) = &association.description { | |
| dict.insert("UTTypeDescription".into(), description.clone().into()); | |
| } | |
| if let Some(content_types) = &association.content_types { | |
| dict.insert( | |
| "UTTypeConformsTo".into(), | |
| plist::Value::Array(content_types.iter().map(|s| s.clone().into()).collect()), | |
| ); | |
| } | |
| if let Some(description) = &association.description { | |
| dict.insert("UTTypeDescription".into(), description.clone().into()); | |
| } | |
| if let Some(conforms_to) = &exported_type.conforms_to { | |
| dict.insert( | |
| "UTTypeConformsTo".into(), | |
| 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 281 - 289, Replace
the current wiring of UTTypeConformsTo (which uses association.content_types) to
instead use the exported type's conforms_to field when present: in the block
around dict insertion for "UTTypeConformsTo" (near where association.description
and association.content_types are handled), check the exported_type.conforms_to
and insert those values into dict as an Array for "UTTypeConformsTo"; only fall
back to association.content_types if exported_type.conforms_to is absent, so you
don't duplicate LSItemContentTypes semantics.
| 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.
CFBundleTypeExtensions insertion condition is inverted.
The current guard inserts an empty array when ext is empty and omits real extensions when present, which can break file association registration.
🛠️ Suggested fix
- if association.ext.is_empty() {
+ if !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.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
condition around inserting CFBundleTypeExtensions is inverted: currently it
inserts an array when association.ext is empty and skips when non-empty; change
the guard to only insert when association.ext is non-empty (i.e., replace if
association.ext.is_empty() with if !association.ext.is_empty()) so the
dict.insert call for "CFBundleTypeExtensions" uses the real extensions from
association.ext.iter().map(...).collect(); keep the existing dict.insert and
mapping logic but flip the boolean check to correctly register file extensions.
| "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.
Avoid a hard panic when name is omitted.
name is optional in the schema; expect will panic for valid configs. Consider falling back to the first extension (previous behavior) or returning a structured error.
🛠️ Suggested fix (fallback to extension)
- association
- .name
- .as_ref()
- .expect("File association must have a name")
- .to_string()
- .into(),
+ association
+ .name
+ .as_deref()
+ .or_else(|| association.ext.first().map(|s| s.as_str()))
+ .unwrap_or("unknown")
+ .to_string()
+ .into(),🤖 Prompt for AI Agents
In `@crates/tauri-bundler/src/bundle/macos/app.rs` around lines 349 - 355, The
code currently calls association.name.expect(...) which will panic for valid
configs because name is optional; update the logic around CFBundleTypeName
construction in app.rs to avoid hard panics by using a fallback (e.g., use the
first extension from association.extensions when name is None) or return a
structured error; locate the block that sets "CFBundleTypeName" and replace the
expect usage on association.name with safe handling (map/if-let or similar) that
either uses the first extension as a string fallback or propagates an error from
the surrounding function instead of panicking.
| "contentTypes": { | ||
| "description": "Declare support to a file with the given content type. Maps to `LSItemContentTypes` on macOS.\n\n This allows supporting any file format declared by another application that conforms to this type.\n Declaration of new types can be done with [`Self::exported_type`] and linking to certain content types are done via [`ExportedFileAssociation::conforms_to`].", | ||
| "type": [ | ||
| "array", | ||
| "null" | ||
| ], | ||
| "items": { | ||
| "type": "string" | ||
| } |
There was a problem hiding this comment.
Align docs with JSON field names.
The description references exported_type / conforms_to (Rust-style). Consider using exportedType / conformsTo to match the config surface and avoid confusion.
🤖 Prompt for AI Agents
In `@crates/tauri-schema-generator/schemas/config.schema.json` around lines 2436 -
2444, Update the description text for the "contentTypes" schema to use the
config-surface camelCase field names instead of Rust-style snake_case: replace
references to Self::exported_type with exportedType and
ExportedFileAssociation::conforms_to with ExportedFileAssociation::conformsTo
(or just conformsTo when referring to the config field) so the docs match the
JSON config names and avoid confusion.
Benchmark PR from agentic-review-benchmarks#12
Summary by CodeRabbit
Release Notes
New Features
Documentation
✏️ Tip: You can customize this high-level summary in your review settings.