Skip to content

If you use kebab-case in your configuration, the TypeScript emits kebab-case. #149

@howmanysmall

Description

@howmanysmall

I tried to make a pull request earlier (sorry!) about this, but yeah, it seems to create invalid variables. Easiest solution is just to sanitize with _ but I'd recommend a configuration value.

This image should show you what I mean, declare const image-ids is definitely NOT a valid variable name.

Image

Configuration

[codegen]
strip_extensions = true
style = "nested"
typescript = true

[creator]
id = 34745987
type = "group"

[inputs."image-ids"]
images = "bleed"
output_path = "src/shared/assets"
warn_each_duplicate = true
path = "./assets/Images/**/*"

Diff

Here is the diff of how it was done on my fork for reference.

diff --git a/src/config.rs b/src/config.rs
index 2a5682b..3050e60 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -85,12 +85,12 @@ impl Config {
     }
 }
 
-fn default_input_naming_convention() -> NamingConvention {
-    NamingConvention::CamelCase
+fn default_input_naming_convention() -> InputNamingConvention {
+    InputNamingConvention::CamelCase
 }
 
-fn default_asset_naming_convention() -> NamingConvention {
-    NamingConvention::Preserve
+fn default_asset_naming_convention() -> AssetNamingConvention {
+    AssetNamingConvention::Preserve
 }
 
 #[derive(Debug, Deserialize, Clone, Default, JsonSchema)]
@@ -109,12 +109,12 @@ pub struct Codegen {
     pub content: bool,
     #[serde(default = "default_input_naming_convention")]
     #[schemars(description = "Naming convention for input module names (default: camel_case)")]
-    pub input_naming_convention: NamingConvention,
+    pub input_naming_convention: InputNamingConvention,
     #[serde(default = "default_asset_naming_convention")]
     #[schemars(
         description = "Naming convention for asset keys in generated code (default: preserve)"
     )]
-    pub asset_naming_convention: NamingConvention,
+    pub asset_naming_convention: AssetNamingConvention,
 }
 
 #[derive(Debug, Deserialize, Clone, ValueEnum, JsonSchema)]
@@ -278,17 +278,38 @@ pub enum CodegenStyle {
 
 #[derive(Debug, Deserialize, Clone, Default, JsonSchema)]
 #[serde(rename_all = "snake_case")]
-#[schemars(description = "Naming convention for transforming names with hyphens, spaces, etc.")]
-pub enum NamingConvention {
-    #[schemars(description = "lowercase_with_underscores")]
+#[schemars(description = "Naming convention for input module names")]
+#[allow(clippy::enum_variant_names)]
+pub enum InputNamingConvention {
+    #[schemars(description = "lowercase_with_underscores (e.g., 'my_input')")]
     SnakeCase,
     #[default]
-    #[schemars(description = "firstWordLowerRestCapitalized (default for input names)")]
+    #[schemars(description = "firstWordLowerRestCapitalized (e.g., 'myInput') - default")]
     CamelCase,
-    #[schemars(description = "AllWordsCapitalized")]
+    #[schemars(description = "AllWordsCapitalized (e.g., 'MyInput')")]
     PascalCase,
-    #[schemars(description = "UPPERCASE_WITH_UNDERSCORES")]
+    #[schemars(description = "UPPERCASE_WITH_UNDERSCORES (e.g., 'MY_INPUT')")]
     ScreamingSnakeCase,
-    #[schemars(description = "Keep original formatting, quote if needed (default for asset names)")]
+}
+
+#[derive(Debug, Deserialize, Clone, Default, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+#[schemars(description = "Naming convention for asset keys in generated code")]
+#[allow(clippy::enum_variant_names)]
+pub enum AssetNamingConvention {
+    #[schemars(description = "lowercase_with_underscores (e.g., 'my_asset_name')")]
+    SnakeCase,
+    #[schemars(description = "firstWordLowerRestCapitalized (e.g., 'myAssetName')")]
+    CamelCase,
+    #[schemars(description = "AllWordsCapitalized (e.g., 'MyAssetName')")]
+    PascalCase,
+    #[schemars(description = "UPPERCASE_WITH_UNDERSCORES (e.g., 'MY_ASSET_NAME')")]
+    ScreamingSnakeCase,
+    #[schemars(description = "lowercase-with-hyphens (e.g., 'my-asset-name')")]
+    KebabCase,
+    #[default]
+    #[schemars(
+        description = "Preserve original name, quote if contains special characters - default"
+    )]
     Preserve,
 }
diff --git a/src/sync/codegen.rs b/src/sync/codegen.rs
index 198818c..3caf38a 100644
--- a/src/sync/codegen.rs
+++ b/src/sync/codegen.rs
@@ -67,7 +67,7 @@ pub fn create_node(source: &BTreeMap<PathBuf, Node>, config: &config::Codegen) -
 fn normalize_path_components(
     path: &Path,
     strip_extensions: bool,
-    convention: &config::NamingConvention,
+    convention: &config::AssetNamingConvention,
 ) -> Vec<String> {
     let mut components: Vec<String> = Vec::new();
     let total_components = path.iter().count();
@@ -84,7 +84,7 @@ fn normalize_path_components(
             comp.to_string_lossy().to_string()
         };
 
-        components.push(convert_name(&component_str, convention));
+        components.push(convert_asset_name(&component_str, convention));
     }
     components
 }
@@ -92,7 +92,7 @@ fn normalize_path_components(
 fn normalize_path_string(
     path: &Path,
     strip_extensions: bool,
-    convention: &config::NamingConvention,
+    convention: &config::AssetNamingConvention,
 ) -> String {
     let path_str = if strip_extensions
         && let (Some(file_name), Some(parent)) = (path.file_name(), path.parent())
@@ -108,7 +108,7 @@ fn normalize_path_string(
         path.to_string_lossy().into_owned()
     };
 
-    convert_name(&path_str, convention)
+    convert_asset_name(&path_str, convention)
 }
 
 fn insert_flat(node: &mut Node, key: &str, value: Node) {
@@ -157,13 +157,13 @@ pub fn generate_code(
     lang: Language,
     name: &str,
     node: &Node,
-    input_naming_convention: &config::NamingConvention,
+    input_naming_convention: &config::InputNamingConvention,
 ) -> anyhow::Result<String> {
     if !matches!(node, Node::Table(_)) {
         bail!("Root node must be a Table");
     }
 
-    let converted_name = convert_name(name, input_naming_convention);
+    let converted_name = convert_input_name(name, input_naming_convention);
 
     Ok(match lang {
         Language::TypeScript => generate_typescript(&converted_name, node),
@@ -301,13 +301,23 @@ fn is_valid_identifier(value: &str) -> bool {
     chars.all(is_valid_ident_char)
 }
 
-fn convert_name(value: &str, convention: &config::NamingConvention) -> String {
+fn convert_input_name(value: &str, convention: &config::InputNamingConvention) -> String {
     match convention {
-        config::NamingConvention::SnakeCase => to_snake_case(value),
-        config::NamingConvention::CamelCase => to_camel_case(value),
-        config::NamingConvention::PascalCase => to_pascal_case(value),
-        config::NamingConvention::ScreamingSnakeCase => to_screaming_snake_case(value),
-        config::NamingConvention::Preserve => to_preserve(value),
+        config::InputNamingConvention::SnakeCase => to_snake_case(value),
+        config::InputNamingConvention::CamelCase => to_camel_case(value),
+        config::InputNamingConvention::PascalCase => to_pascal_case(value),
+        config::InputNamingConvention::ScreamingSnakeCase => to_screaming_snake_case(value),
+    }
+}
+
+fn convert_asset_name(value: &str, convention: &config::AssetNamingConvention) -> String {
+    match convention {
+        config::AssetNamingConvention::SnakeCase => to_snake_case(value),
+        config::AssetNamingConvention::CamelCase => to_camel_case(value),
+        config::AssetNamingConvention::PascalCase => to_pascal_case(value),
+        config::AssetNamingConvention::ScreamingSnakeCase => to_screaming_snake_case(value),
+        config::AssetNamingConvention::KebabCase => to_kebab_case(value),
+        config::AssetNamingConvention::Preserve => to_preserve(value),
     }
 }
 
@@ -405,30 +415,13 @@ fn to_screaming_snake_case(value: &str) -> String {
     result
 }
 
-fn to_preserve(value: &str) -> String {
-    let mut result = String::with_capacity(value.len());
-    let mut chars = value.chars();
-
-    if let Some(first) = chars.next() {
-        if is_valid_ident_char_start(first) {
-            result.push(first);
-        } else if first.is_ascii_digit() {
-            result.push('_');
-            result.push(first);
-        } else {
-            result.push('_');
-        }
-    }
-
-    for ch in chars {
-        if is_valid_ident_char(ch) {
-            result.push(ch);
-        } else {
-            result.push('_');
-        }
-    }
+fn to_kebab_case(value: &str) -> String {
+    let words = split_into_words(value);
+    words.join("-").to_ascii_lowercase()
+}
 
-    result
+fn to_preserve(value: &str) -> String {
+    value.to_string()
 }
 
 #[cfg(test)]
@@ -485,7 +478,7 @@ mod tests {
             Language::TypeScript,
             "name",
             &root_node,
-            &config::NamingConvention::CamelCase,
+            &config::InputNamingConvention::CamelCase,
         )
         .unwrap();
         insta::assert_snapshot!(code);
@@ -498,7 +491,7 @@ mod tests {
             Language::Luau,
             "name",
             &root_node,
-            &config::NamingConvention::CamelCase,
+            &config::InputNamingConvention::CamelCase,
         )
         .unwrap();
         insta::assert_snapshot!(code);
@@ -513,7 +506,7 @@ mod tests {
             Language::Luau,
             "sprite",
             &root,
-            &config::NamingConvention::CamelCase,
+            &config::InputNamingConvention::CamelCase,
         )
         .unwrap();
         insta::assert_snapshot!(code);
@@ -528,7 +521,7 @@ mod tests {
             Language::Luau,
             "sprite",
             &root,
-            &config::NamingConvention::CamelCase,
+            &config::InputNamingConvention::CamelCase,
         )
         .unwrap();
         insta::assert_snapshot!(code);
@@ -543,7 +536,7 @@ mod tests {
             Language::TypeScript,
             "sprite",
             &root,
-            &config::NamingConvention::CamelCase,
+            &config::InputNamingConvention::CamelCase,
         )
         .unwrap();
         insta::assert_snapshot!(code);
@@ -558,7 +551,7 @@ mod tests {
             Language::TypeScript,
             "sprite",
             &root,
-            &config::NamingConvention::CamelCase,
+            &config::InputNamingConvention::CamelCase,
         )
         .unwrap();
         insta::assert_snapshot!(code);
@@ -571,7 +564,7 @@ mod tests {
             Language::Luau,
             "assets",
             &mixed_node,
-            &config::NamingConvention::CamelCase,
+            &config::InputNamingConvention::CamelCase,
         )
         .unwrap();
         insta::assert_snapshot!(code);
@@ -584,7 +577,7 @@ mod tests {
             Language::TypeScript,
             "assets",
             &mixed_node,
-            &config::NamingConvention::CamelCase,
+            &config::InputNamingConvention::CamelCase,
         )
         .unwrap();
         insta::assert_snapshot!(code);
@@ -606,7 +599,7 @@ mod tests {
             Language::Luau,
             "edge_case",
             &root,
-            &config::NamingConvention::CamelCase,
+            &config::InputNamingConvention::CamelCase,
         )
         .unwrap();
         insta::assert_snapshot!(code);
@@ -615,48 +608,90 @@ mod tests {
     #[test]
     fn test_naming_conventions() {
         assert_eq!(
-            convert_name("character-art", &config::NamingConvention::SnakeCase),
+            convert_asset_name("character-art", &config::AssetNamingConvention::SnakeCase),
             "character_art"
         );
         assert_eq!(
-            convert_name("character-art", &config::NamingConvention::CamelCase),
+            convert_asset_name("character-art", &config::AssetNamingConvention::CamelCase),
             "characterArt"
         );
         assert_eq!(
-            convert_name("character-art", &config::NamingConvention::PascalCase),
+            convert_asset_name("character-art", &config::AssetNamingConvention::PascalCase),
             "CharacterArt"
         );
         assert_eq!(
-            convert_name(
+            convert_asset_name(
                 "character-art",
-                &config::NamingConvention::ScreamingSnakeCase
+                &config::AssetNamingConvention::ScreamingSnakeCase
             ),
             "CHARACTER_ART"
         );
         assert_eq!(
-            convert_name("character-art", &config::NamingConvention::Preserve),
+            convert_asset_name("character-art", &config::AssetNamingConvention::KebabCase),
+            "character-art"
+        );
+        assert_eq!(
+            convert_asset_name("character-art", &config::AssetNamingConvention::Preserve),
+            "character-art"
+        );
+        assert_eq!(
+            convert_asset_name("Character Art", &config::AssetNamingConvention::KebabCase),
+            "character-art"
+        );
+        assert_eq!(
+            convert_asset_name("SOME_VALUE", &config::AssetNamingConvention::KebabCase),
+            "some-value"
+        );
+
+        assert_eq!(
+            convert_input_name("character-art", &config::InputNamingConvention::SnakeCase),
             "character_art"
         );
+        assert_eq!(
+            convert_input_name("character-art", &config::InputNamingConvention::CamelCase),
+            "characterArt"
+        );
+        assert_eq!(
+            convert_input_name("character-art", &config::InputNamingConvention::PascalCase),
+            "CharacterArt"
+        );
     }
 
     #[test]
     fn test_naming_edge_cases() {
         assert_eq!(
-            convert_name("123invalid", &config::NamingConvention::CamelCase),
+            convert_input_name("123invalid", &config::InputNamingConvention::CamelCase),
             "_123invalid"
         );
         assert_eq!(
-            convert_name("-starts-with-hyphen", &config::NamingConvention::CamelCase),
+            convert_input_name(
+                "-starts-with-hyphen",
+                &config::InputNamingConvention::CamelCase
+            ),
             "_startsWithHyphen"
         );
         assert_eq!(
-            convert_name("has spaces", &config::NamingConvention::PascalCase),
+            convert_input_name("has spaces", &config::InputNamingConvention::PascalCase),
             "HasSpaces"
         );
         assert_eq!(
-            convert_name("special!@#chars", &config::NamingConvention::SnakeCase),
+            convert_input_name("special!@#chars", &config::InputNamingConvention::SnakeCase),
             "special_chars"
         );
+        assert_eq!(
+            convert_asset_name(
+                "icons/sword-icon.png",
+                &config::AssetNamingConvention::Preserve
+            ),
+            "icons/sword-icon.png"
+        );
+        assert_eq!(
+            convert_asset_name(
+                "path with spaces/file.png",
+                &config::AssetNamingConvention::Preserve
+            ),
+            "path with spaces/file.png"
+        );
     }
 
     #[test]
@@ -672,7 +707,7 @@ mod tests {
             Language::Luau,
             "character-art",
             &root,
-            &config::NamingConvention::CamelCase,
+            &config::InputNamingConvention::CamelCase,
         )
         .unwrap();
         assert!(luau_code.contains("local characterArt ="));
@@ -682,7 +717,7 @@ mod tests {
             Language::TypeScript,
             "image-ids",
             &root,
-            &config::NamingConvention::CamelCase,
+            &config::InputNamingConvention::CamelCase,
         )
         .unwrap();
         assert!(ts_code.contains("declare const imageIds:"));

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions