This is an automated email from the ASF dual-hosted git repository.

villebro pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new 40f609fdce2 fix(extensions): enforce correct naming conventions 
(#38167)
40f609fdce2 is described below

commit 40f609fdce2c4dd9f73c487a2f1c96bbf77ef56a
Author: Ville Brofeldt <[email protected]>
AuthorDate: Mon Feb 23 08:21:35 2026 -0800

    fix(extensions): enforce correct naming conventions (#38167)
---
 docs/developer_portal/extensions/development.md    |  44 ++-
 docs/developer_portal/extensions/quick-start.md    |  56 +--
 .../src/superset_core/extensions/types.py          |   5 +
 .../src/superset_extensions_cli/cli.py             | 174 +++++++--
 .../{utils.py => exceptions.py}                    |  26 +-
 .../templates/backend/pyproject.toml.j2            |   2 +-
 .../templates/backend/src/package/entrypoint.py.j2 |   2 +-
 .../templates/extension.json.j2                    |   5 +-
 .../templates/frontend/webpack.config.js.j2        |   2 +-
 .../superset_extensions_cli/{utils.py => types.py} |  34 +-
 .../src/superset_extensions_cli/utils.py           | 262 ++++++++++++++
 superset-extensions-cli/tests/conftest.py          |   8 +-
 superset-extensions-cli/tests/test_cli_init.py     | 139 +++----
 .../tests/test_name_transformations.py             | 400 +++++++++++++++++++++
 superset-extensions-cli/tests/test_templates.py    |  73 +++-
 .../src/extensions/ExtensionsManager.ts            |   4 +-
 superset/extensions/utils.py                       |  29 ++
 17 files changed, 1066 insertions(+), 199 deletions(-)

diff --git a/docs/developer_portal/extensions/development.md 
b/docs/developer_portal/extensions/development.md
index 12fe9351ce0..a6c143a3796 100644
--- a/docs/developer_portal/extensions/development.md
+++ b/docs/developer_portal/extensions/development.md
@@ -40,10 +40,10 @@ superset-extensions bundle: Packages the extension into a 
.supx file.
 superset-extensions dev: Automatically rebuilds the extension as files change.
 ```
 
-When creating a new extension with `superset-extensions init 
<extension-name>`, the CLI generates a standardized folder structure:
+When creating a new extension with `superset-extensions init`, the CLI 
generates a standardized folder structure:
 
 ```
-dataset_references/
+dataset-references/
 ├── extension.json
 ├── frontend/
 │   ├── src/
@@ -52,25 +52,33 @@ dataset_references/
 │   └── package.json
 ├── backend/
 │   ├── src/
-│        └── dataset_references/
+│   │    └── superset_extensions/
+│   │         └── dataset_references/
 │   ├── tests/
 │   ├── pyproject.toml
 │   └── requirements.txt
 ├── dist/
 │   ├── manifest.json
 │   ├── frontend
-│        └── dist/
-│             ├── remoteEntry.d7a9225d042e4ccb6354.js
-│             └── 900.038b20cdff6d49cfa8d9.js
+│   │    └── dist/
+│   │         ├── remoteEntry.d7a9225d042e4ccb6354.js
+│   │         └── 900.038b20cdff6d49cfa8d9.js
 │   └── backend
-│        └── dataset_references/
-│             ├── __init__.py
-│             ├── api.py
-│             └── entrypoint.py
-├── dataset_references-1.0.0.supx
+│        └── superset_extensions/
+│             └── dataset_references/
+│                  ├── __init__.py
+│                  ├── api.py
+│                  └── entrypoint.py
+├── dataset-references-1.0.0.supx
 └── README.md
 ```
 
+**Note**: The extension ID (`dataset-references`) serves as the basis for all 
technical names:
+- Directory name: `dataset-references` (kebab-case)
+- Backend Python package: `dataset_references` (snake_case)
+- Frontend package name: `dataset-references` (kebab-case)
+- Module Federation name: `datasetReferences` (camelCase)
+
 The `extension.json` file serves as the declared metadata for the extension, 
containing the extension's name, version, author, description, and a list of 
capabilities. This file is essential for the host application to understand how 
to load and manage the extension.
 
 The `frontend` directory contains the source code for the frontend components 
of the extension, including React components, styles, and assets. The 
`webpack.config.js` file is used to configure Webpack for building the frontend 
code, while the `tsconfig.json` file defines the TypeScript configuration for 
the project. The `package.json` file specifies the dependencies and scripts for 
building and testing the frontend code.
@@ -87,7 +95,8 @@ The `extension.json` file contains all metadata necessary for 
the host applicati
 
 ```json
 {
-  "name": "dataset_references",
+  "id": "dataset-references",
+  "name": "Dataset References",
   "version": "1.0.0",
   "frontend": {
     "contributions": {
@@ -95,20 +104,21 @@ The `extension.json` file contains all metadata necessary 
for the host applicati
         "sqllab": {
           "panels": [
             {
-              "id": "dataset_references.main",
-              "name": "Dataset references"
+              "id": "dataset-references.main",
+              "name": "Dataset References"
             }
           ]
         }
       }
     },
     "moduleFederation": {
-      "exposes": ["./index"]
+      "exposes": ["./index"],
+      "name": "datasetReferences"
     }
   },
   "backend": {
-    "entryPoints": ["dataset_references.entrypoint"],
-    "files": ["backend/src/dataset_references/**/*.py"]
+    "entryPoints": ["superset_extensions.dataset_references.entrypoint"],
+    "files": ["backend/src/superset_extensions/dataset_references/**/*.py"]
   }
 }
 ```
diff --git a/docs/developer_portal/extensions/quick-start.md 
b/docs/developer_portal/extensions/quick-start.md
index 60cf23acf9a..a3a08566f1b 100644
--- a/docs/developer_portal/extensions/quick-start.md
+++ b/docs/developer_portal/extensions/quick-start.md
@@ -54,24 +54,33 @@ superset-extensions init
 The CLI will prompt you for information:
 
 ```
-Extension ID (unique identifier, alphanumeric only): hello_world
-Extension name (human-readable display name): Hello World
+Extension name (e.g. Hello World): Hello World
+Extension ID [hello-world]: hello-world
 Initial version [0.1.0]: 0.1.0
 License [Apache-2.0]: Apache-2.0
 Include frontend? [Y/n]: Y
 Include backend? [Y/n]: Y
 ```
 
+**Important**: The extension ID must be **globally unique** across all 
Superset extensions and serves as the basis for all technical identifiers:
+- **Frontend package name**: `hello-world` (same as ID, used in package.json)
+- **Webpack Module Federation name**: `helloWorld` (camelCase from ID)
+- **Backend package name**: `hello_world` (snake_case from ID, used in 
project.toml)
+- **Python namespace**: `superset_extensions.hello_world`
+
+This ensures consistent naming across all technical components, even when the 
display name differs significantly from the ID. Since all technical names 
derive from the extension ID, choosing a unique ID automatically ensures all 
generated names are also unique, preventing conflicts between extensions.
+
 This creates a complete project structure:
 
 ```
-hello_world/
+hello-world/
 ├── extension.json           # Extension metadata and configuration
 ├── backend/                 # Backend Python code
 │   ├── src/
-│   │   └── hello_world/
-│   │       ├── __init__.py
-│   │       └── entrypoint.py  # Backend registration
+│   │   └── superset_extensions/
+│   │       └── hello_world/
+│   │           ├── __init__.py
+│   │           └── entrypoint.py  # Backend registration
 │   └── pyproject.toml
 └── frontend/                # Frontend TypeScript/React code
     ├── src/
@@ -87,7 +96,7 @@ The generated `extension.json` contains basic metadata. 
Update it to register yo
 
 ```json
 {
-  "id": "hello_world",
+  "id": "hello-world",
   "name": "Hello World",
   "version": "0.1.0",
   "license": "Apache-2.0",
@@ -97,7 +106,7 @@ The generated `extension.json` contains basic metadata. 
Update it to register yo
         "sqllab": {
           "panels": [
             {
-              "id": "hello_world.main",
+              "id": "hello-world.main",
               "name": "Hello World"
             }
           ]
@@ -105,17 +114,20 @@ The generated `extension.json` contains basic metadata. 
Update it to register yo
       }
     },
     "moduleFederation": {
-      "exposes": ["./index"]
+      "exposes": ["./index"],
+      "name": "helloWorld"
     }
   },
   "backend": {
-    "entryPoints": ["hello_world.entrypoint"],
-    "files": ["backend/src/hello_world/**/*.py"]
+    "entryPoints": ["superset_extensions.hello_world.entrypoint"],
+    "files": ["backend/src/superset_extensions/hello_world/**/*.py"]
   },
   "permissions": ["can_read"]
 }
 ```
 
+**Note**: The `moduleFederation.name` is automatically derived from the 
extension ID (`hello-world` → `helloWorld`), and backend entry points use the 
full Python namespace (`superset_extensions.hello_world`).
+
 **Key fields:**
 
 - `frontend.contributions.views.sqllab.panels`: Registers your panel in SQL Lab
@@ -123,9 +135,9 @@ The generated `extension.json` contains basic metadata. 
Update it to register yo
 
 ## Step 4: Create Backend API
 
-The CLI generated a basic `backend/src/hello_world/entrypoint.py`. We'll 
create an API endpoint.
+The CLI generated a basic 
`backend/src/superset_extensions/hello_world/entrypoint.py`. We'll create an 
API endpoint.
 
-**Create `backend/src/hello_world/api.py`**
+**Create `backend/src/superset_extensions/hello_world/api.py`**
 
 ```python
 from flask import Response
@@ -174,10 +186,10 @@ class HelloWorldAPI(RestApi):
 - Extends `RestApi` from `superset_core.api.types.rest_api`
 - Uses Flask-AppBuilder decorators (`@expose`, `@protect`, `@safe`)
 - Returns responses using `self.response(status_code, result=data)`
-- The endpoint will be accessible at `/extensions/hello_world/message`
+- The endpoint will be accessible at `/extensions/hello-world/message`
 - OpenAPI docstrings are crucial - Flask-AppBuilder uses them to automatically 
generate interactive API documentation at `/swagger/v1`, allowing developers to 
explore endpoints, understand schemas, and test the API directly from the 
browser
 
-**Update `backend/src/hello_world/entrypoint.py`**
+**Update `backend/src/superset_extensions/hello_world/entrypoint.py`**
 
 Replace the generated print statement with API registration:
 
@@ -201,7 +213,7 @@ The `@apache-superset/core` package must be listed in both 
`peerDependencies` (t
 
 ```json
 {
-  "name": "hello_world",
+  "name": "hello-world",
   "version": "0.1.0",
   "private": true,
   "license": "Apache-2.0",
@@ -330,7 +342,7 @@ const HelloWorldPanel: React.FC = () => {
     const fetchMessage = async () => {
       try {
         const csrfToken = await authentication.getCSRFToken();
-        const response = await fetch('/extensions/hello_world/message', {
+        const response = await fetch('/extensions/hello-world/message', {
           method: 'GET',
           headers: {
             'Content-Type': 'application/json',
@@ -403,7 +415,7 @@ import HelloWorldPanel from './HelloWorldPanel';
 
 export const activate = (context: core.ExtensionContext) => {
   context.disposables.push(
-    core.registerViewProvider('hello_world.main', () => <HelloWorldPanel />),
+    core.registerViewProvider('hello-world.main', () => <HelloWorldPanel />),
   );
 };
 
@@ -413,7 +425,7 @@ export const deactivate = () => {};
 **Key patterns:**
 
 - `activate` function is called when the extension loads
-- `core.registerViewProvider` registers the component with ID 
`hello_world.main` (matching `extension.json`)
+- `core.registerViewProvider` registers the component with ID 
`hello-world.main` (matching `extension.json`)
 - `authentication.getCSRFToken()` retrieves the CSRF token for API calls
 - Fetch calls to `/extensions/{extension_id}/{endpoint}` reach your backend API
 - `context.disposables.push()` ensures proper cleanup
@@ -444,7 +456,7 @@ This command automatically:
   - `manifest.json` - Build metadata and asset references
   - `frontend/dist/` - Built frontend assets (remoteEntry.js, chunks)
   - `backend/` - Python source files
-- Packages everything into `hello_world-0.1.0.supx` - a zip archive with the 
specific structure required by Superset
+- Packages everything into `hello-world-0.1.0.supx` - a zip archive with the 
specific structure required by Superset
 
 ## Step 8: Deploy to Superset
 
@@ -469,7 +481,7 @@ EXTENSIONS_PATH = "/path/to/extensions/folder"
 Copy your `.supx` file to the configured extensions path:
 
 ```bash
-cp hello_world-0.1.0.supx /path/to/extensions/folder/
+cp hello-world-0.1.0.supx /path/to/extensions/folder/
 ```
 
 **Restart Superset**
@@ -500,7 +512,7 @@ Here's what happens when your extension loads:
 4. **Module Federation**: Webpack loads your extension code and resolves 
`@apache-superset/core` to `window.superset`
 5. **Activation**: `activate()` is called, registering your view provider
 6. **Rendering**: When the user opens your panel, React renders 
`<HelloWorldPanel />`
-7. **API call**: Component fetches data from `/extensions/hello_world/message`
+7. **API call**: Component fetches data from `/extensions/hello-world/message`
 8. **Backend response**: Your Flask API returns the hello world message
 9. **Display**: Component shows the message to the user
 
diff --git a/superset-core/src/superset_core/extensions/types.py 
b/superset-core/src/superset_core/extensions/types.py
index 82f4d5a85eb..41ab83d40c9 100644
--- a/superset-core/src/superset_core/extensions/types.py
+++ b/superset-core/src/superset_core/extensions/types.py
@@ -37,6 +37,11 @@ from pydantic import BaseModel, Field  # noqa: I001
 class ModuleFederationConfig(BaseModel):
     """Configuration for Webpack Module Federation."""
 
+    name: str | None = Field(
+        default=None,
+        description="Module Federation container name "
+        "(must be valid JavaScript identifier)",
+    )
     exposes: list[str] = Field(
         default_factory=list,
         description="Modules exposed by this extension",
diff --git a/superset-extensions-cli/src/superset_extensions_cli/cli.py 
b/superset-extensions-cli/src/superset_extensions_cli/cli.py
index 8f15df6da64..6f333ec6948 100644
--- a/superset-extensions-cli/src/superset_extensions_cli/cli.py
+++ b/superset-extensions-cli/src/superset_extensions_cli/cli.py
@@ -38,7 +38,20 @@ from watchdog.events import FileSystemEventHandler
 from watchdog.observers import Observer
 
 from superset_extensions_cli.constants import MIN_NPM_VERSION
-from superset_extensions_cli.utils import read_json, read_toml
+from superset_extensions_cli.exceptions import ExtensionNameError
+from superset_extensions_cli.types import ExtensionNames
+from superset_extensions_cli.utils import (
+    generate_extension_names,
+    kebab_to_camel_case,
+    kebab_to_snake_case,
+    read_json,
+    read_toml,
+    to_kebab_case,
+    to_snake_case,
+    validate_extension_id,
+    validate_npm_package_name,
+    validate_python_package_name,
+)
 
 REMOTE_ENTRY_REGEX = re.compile(r"^remoteEntry\..+\.js$")
 FRONTEND_DIST_REGEX = re.compile(r"/frontend/dist")
@@ -403,14 +416,127 @@ def dev(ctx: click.Context) -> None:
         click.secho("❌ No directories to watch. Exiting.", fg="red")
 
 
+def prompt_for_extension_name(
+    display_name_opt: str | None, id_opt: str | None
+) -> ExtensionNames:
+    """
+    Prompt for extension name with graceful validation and re-prompting.
+
+    Args:
+        display_name_opt: Display name provided via CLI option (if any)
+        id_opt: Extension ID provided via CLI option (if any)
+
+    Returns:
+        ExtensionNames: Validated extension name variants
+    """
+
+    # Case 1: Both provided via CLI - validate they work together
+    if display_name_opt and id_opt:
+        try:
+            # Generate all names from display name for consistency
+            temp_names = generate_extension_names(display_name_opt)
+            # Check if the provided ID matches what we'd generate
+            if temp_names["id"] == id_opt:
+                return temp_names
+            else:
+                # If IDs don't match, use the provided ID but validate it
+                validate_extension_id(id_opt)
+                validate_python_package_name(to_snake_case(id_opt))
+                validate_npm_package_name(id_opt)
+                # Create names with the provided ID (derive technical names 
from ID)
+                return ExtensionNames(
+                    name=display_name_opt,
+                    id=id_opt,
+                    mf_name=kebab_to_camel_case(id_opt),
+                    backend_name=kebab_to_snake_case(id_opt),
+                    
backend_package=f"superset_extensions.{kebab_to_snake_case(id_opt)}",
+                    
backend_entry=f"superset_extensions.{kebab_to_snake_case(id_opt)}.entrypoint",
+                )
+        except ExtensionNameError as e:
+            click.secho(f"❌ {e}", fg="red")
+            sys.exit(1)
+
+    # Case 2: Only display name provided - suggest ID
+    if display_name_opt and not id_opt:
+        display_name = display_name_opt
+        try:
+            suggested_names = generate_extension_names(display_name)
+            suggested_id = suggested_names["id"]
+        except ExtensionNameError:
+            suggested_id = to_kebab_case(display_name)
+
+        extension_id = click.prompt("Extension ID", default=suggested_id, 
type=str)
+
+    # Case 3: Only ID provided - ask for display name
+    elif id_opt and not display_name_opt:
+        extension_id = id_opt
+        # Validate the provided ID first
+        try:
+            validate_extension_id(id_opt)
+        except ExtensionNameError as e:
+            click.secho(f"❌ {e}", fg="red")
+            sys.exit(1)
+
+        # Suggest display name from kebab ID
+        suggested_display = " ".join(word.capitalize() for word in 
id_opt.split("-"))
+        display_name = click.prompt(
+            "Extension name", default=suggested_display, type=str
+        )
+
+    # Case 4: Neither provided - ask for both
+    else:
+        display_name = click.prompt("Extension name (e.g. Hello World)", 
type=str)
+        try:
+            suggested_names = generate_extension_names(display_name)
+            suggested_id = suggested_names["id"]
+        except ExtensionNameError:
+            suggested_id = to_kebab_case(display_name)
+
+        extension_id = click.prompt("Extension ID", default=suggested_id, 
type=str)
+
+    # Final validation loop - try to use generate_extension_names for 
consistent results
+    display_name_failed = False  # Track if display name validation failed
+    while True:
+        try:
+            # First try to generate from display name if possible and it 
hasn't failed before
+            if display_name and not display_name_failed:
+                temp_names = generate_extension_names(display_name)
+                if temp_names["id"] == extension_id:
+                    # Perfect match - use generated names
+                    return temp_names
+
+            # If no match or display name failed, validate manually and 
construct
+            validate_extension_id(extension_id)
+            validate_python_package_name(to_snake_case(extension_id))
+            validate_npm_package_name(extension_id)
+
+            return ExtensionNames(
+                name=display_name,
+                id=extension_id,
+                mf_name=kebab_to_camel_case(extension_id),
+                backend_name=kebab_to_snake_case(extension_id),
+                
backend_package=f"superset_extensions.{kebab_to_snake_case(extension_id)}",
+                
backend_entry=f"superset_extensions.{kebab_to_snake_case(extension_id)}.entrypoint",
+            )
+
+        except ExtensionNameError as e:
+            click.secho(f"❌ {e}", fg="red")
+            # If the error came from generate_extension_names, stop trying it
+            if "display_name" in str(e) or not display_name_failed:
+                display_name_failed = True
+            extension_id = click.prompt("Extension ID", type=str)
+
+
 @app.command()
 @click.option(
     "--id",
     "id_opt",
     default=None,
-    help="Extension ID (alphanumeric and underscores only)",
+    help="Extension ID (kebab-case, e.g. hello-world)",
+)
[email protected](
+    "--name", "name_opt", default=None, help="Extension display name (e.g. 
Hello World)"
 )
[email protected]("--name", "name_opt", default=None, help="Extension display 
name")
 @click.option(
     "--version", "version_opt", default=None, help="Initial version (default: 
0.1.0)"
 )
@@ -431,18 +557,9 @@ def init(
     frontend_opt: bool | None,
     backend_opt: bool | None,
 ) -> None:
-    id_ = id_opt or click.prompt(
-        "Extension ID (unique identifier, alphanumeric only)", type=str
-    )
-    if not re.match(r"^[a-zA-Z0-9_]+$", id_):
-        click.secho(
-            "❌ ID must be alphanumeric (letters, digits, underscore).", 
fg="red"
-        )
-        sys.exit(1)
+    # Get extension names with graceful validation
+    names = prompt_for_extension_name(name_opt, id_opt)
 
-    name = name_opt or click.prompt(
-        "Extension name (human-readable display name)", type=str
-    )
     version = version_opt or click.prompt("Initial version", default="0.1.0")
     license_ = license_opt or click.prompt("License", default="Apache-2.0")
     include_frontend = (
@@ -456,7 +573,7 @@ def init(
         else click.confirm("Include backend?", default=True)
     )
 
-    target_dir = Path.cwd() / id_
+    target_dir = Path.cwd() / names["id"]
     if target_dir.exists():
         click.secho(f"❌ Directory {target_dir} already exists.", fg="red")
         sys.exit(1)
@@ -465,8 +582,7 @@ def init(
     templates_dir = Path(__file__).parent / "templates"
     env = Environment(loader=FileSystemLoader(templates_dir))  # noqa: S701
     ctx = {
-        "id": id_,
-        "name": name,
+        **names,  # Include all name variants
         "include_frontend": include_frontend,
         "include_backend": include_backend,
         "license": license_,
@@ -502,29 +618,41 @@ def init(
         (frontend_src_dir / "index.tsx").write_text(index_tsx)
         click.secho("✅ Created frontend folder structure", fg="green")
 
-    # Initialize backend files
+    # Initialize backend files with superset_extensions namespace
     if include_backend:
         backend_dir = target_dir / "backend"
         backend_dir.mkdir()
         backend_src_dir = backend_dir / "src"
         backend_src_dir.mkdir()
-        backend_src_package_dir = backend_src_dir / id_
-        backend_src_package_dir.mkdir()
+
+        # Create superset_extensions namespace directory
+        namespace_dir = backend_src_dir / "superset_extensions"
+        namespace_dir.mkdir()
+
+        # Create extension package directory
+        extension_package_dir = namespace_dir / names["backend_name"]
+        extension_package_dir.mkdir()
 
         # backend files
         pyproject_toml = 
env.get_template("backend/pyproject.toml.j2").render(ctx)
         (backend_dir / "pyproject.toml").write_text(pyproject_toml)
+
+        # Namespace package __init__.py (empty for namespace)
+        (namespace_dir / "__init__.py").write_text("")
+
+        # Extension package files
         init_py = 
env.get_template("backend/src/package/__init__.py.j2").render(ctx)
-        (backend_src_package_dir / "__init__.py").write_text(init_py)
+        (extension_package_dir / "__init__.py").write_text(init_py)
         entrypoint_py = 
env.get_template("backend/src/package/entrypoint.py.j2").render(
             ctx
         )
-        (backend_src_package_dir / "entrypoint.py").write_text(entrypoint_py)
+        (extension_package_dir / "entrypoint.py").write_text(entrypoint_py)
 
         click.secho("✅ Created backend folder structure", fg="green")
 
     click.secho(
-        f"🎉 Extension {name} (ID: {id_}) initialized at {target_dir}", 
fg="cyan"
+        f"🎉 Extension {names['name']} (ID: {names['id']}) initialized at 
{target_dir}",
+        fg="cyan",
     )
 
 
diff --git a/superset-extensions-cli/src/superset_extensions_cli/utils.py 
b/superset-extensions-cli/src/superset_extensions_cli/exceptions.py
similarity index 61%
copy from superset-extensions-cli/src/superset_extensions_cli/utils.py
copy to superset-extensions-cli/src/superset_extensions_cli/exceptions.py
index 7dc739d9dba..8d03d129f4f 100644
--- a/superset-extensions-cli/src/superset_extensions_cli/utils.py
+++ b/superset-extensions-cli/src/superset_extensions_cli/exceptions.py
@@ -15,28 +15,8 @@
 # specific language governing permissions and limitations
 # under the License.
 
-import json  # noqa: TID251
-import sys
-from pathlib import Path
-from typing import Any
 
-if sys.version_info >= (3, 11):
-    import tomllib
-else:
-    import tomli as tomllib
+class ExtensionNameError(Exception):
+    """Raised when extension name validation fails."""
 
-
-def read_toml(path: Path) -> dict[str, Any] | None:
-    if not path.is_file():
-        return None
-
-    with path.open("rb") as f:
-        return tomllib.load(f)
-
-
-def read_json(path: Path) -> dict[str, Any] | None:
-    path = Path(path)
-    if not path.is_file():
-        return None
-
-    return json.loads(path.read_text())
+    pass
diff --git 
a/superset-extensions-cli/src/superset_extensions_cli/templates/backend/pyproject.toml.j2
 
b/superset-extensions-cli/src/superset_extensions_cli/templates/backend/pyproject.toml.j2
index cbe78bd8b29..135f45e28a9 100644
--- 
a/superset-extensions-cli/src/superset_extensions_cli/templates/backend/pyproject.toml.j2
+++ 
b/superset-extensions-cli/src/superset_extensions_cli/templates/backend/pyproject.toml.j2
@@ -1,4 +1,4 @@
 [project]
-name = "{{ id }}"
+name = "{{ backend_package }}"
 version = "{{ version }}"
 license = "{{ license }}"
diff --git 
a/superset-extensions-cli/src/superset_extensions_cli/templates/backend/src/package/entrypoint.py.j2
 
b/superset-extensions-cli/src/superset_extensions_cli/templates/backend/src/package/entrypoint.py.j2
index 2ff158bf997..2adca9ab7a9 100644
--- 
a/superset-extensions-cli/src/superset_extensions_cli/templates/backend/src/package/entrypoint.py.j2
+++ 
b/superset-extensions-cli/src/superset_extensions_cli/templates/backend/src/package/entrypoint.py.j2
@@ -1 +1 @@
-print("{{ name }} extension registered")
+print("{{ display_name }} extension registered")
diff --git 
a/superset-extensions-cli/src/superset_extensions_cli/templates/extension.json.j2
 
b/superset-extensions-cli/src/superset_extensions_cli/templates/extension.json.j2
index fa3e1f93b2a..4f4fb32e169 100644
--- 
a/superset-extensions-cli/src/superset_extensions_cli/templates/extension.json.j2
+++ 
b/superset-extensions-cli/src/superset_extensions_cli/templates/extension.json.j2
@@ -11,14 +11,15 @@
       "menus": {}
     },
     "moduleFederation": {
+      "name": "{{ mf_name }}",
       "exposes": ["./index"]
     }
   },
   {% endif -%}
   {% if include_backend -%}
   "backend": {
-    "entryPoints": ["{{ id }}.entrypoint"],
-    "files": ["backend/src/{{ id }}/**/*.py"]
+    "entryPoints": ["{{ backend_entry }}"],
+    "files": ["backend/src/superset_extensions/{{ backend_name }}/**/*.py"]
   },
   {% endif -%}
   "permissions": []
diff --git 
a/superset-extensions-cli/src/superset_extensions_cli/templates/frontend/webpack.config.js.j2
 
b/superset-extensions-cli/src/superset_extensions_cli/templates/frontend/webpack.config.js.j2
index 25f1b5c0be6..5a2342ed69f 100644
--- 
a/superset-extensions-cli/src/superset_extensions_cli/templates/frontend/webpack.config.js.j2
+++ 
b/superset-extensions-cli/src/superset_extensions_cli/templates/frontend/webpack.config.js.j2
@@ -39,7 +39,7 @@ module.exports = (env, argv) => {
     },
     plugins: [
       new ModuleFederationPlugin({
-        name: "{{ id }}",
+        name: "{{ mf_name }}",
         filename: "remoteEntry.[contenthash].js",
         exposes: {
           "./index": "./src/index.tsx",
diff --git a/superset-extensions-cli/src/superset_extensions_cli/utils.py 
b/superset-extensions-cli/src/superset_extensions_cli/types.py
similarity index 53%
copy from superset-extensions-cli/src/superset_extensions_cli/utils.py
copy to superset-extensions-cli/src/superset_extensions_cli/types.py
index 7dc739d9dba..c7b774ab59c 100644
--- a/superset-extensions-cli/src/superset_extensions_cli/utils.py
+++ b/superset-extensions-cli/src/superset_extensions_cli/types.py
@@ -15,28 +15,26 @@
 # specific language governing permissions and limitations
 # under the License.
 
-import json  # noqa: TID251
-import sys
-from pathlib import Path
-from typing import Any
+from typing import TypedDict
 
-if sys.version_info >= (3, 11):
-    import tomllib
-else:
-    import tomli as tomllib
 
+class ExtensionNames(TypedDict):
+    """Type definition for extension name variants following platform 
conventions."""
 
-def read_toml(path: Path) -> dict[str, Any] | None:
-    if not path.is_file():
-        return None
+    # Extension name (e.g., "Hello World")
+    name: str
 
-    with path.open("rb") as f:
-        return tomllib.load(f)
+    # Extension ID - kebab-case primary identifier and npm package name (e.g., 
"hello-world")
+    id: str
 
+    # Module Federation library - camelCase JS identifier (e.g., "helloWorld")
+    mf_name: str
 
-def read_json(path: Path) -> dict[str, Any] | None:
-    path = Path(path)
-    if not path.is_file():
-        return None
+    # Backend package name - snake_case (e.g., "hello_world")
+    backend_name: str
 
-    return json.loads(path.read_text())
+    # Full backend package (e.g., "superset_extensions.hello_world")
+    backend_package: str
+
+    # Backend entry point (e.g., "superset_extensions.hello_world.entrypoint")
+    backend_entry: str
diff --git a/superset-extensions-cli/src/superset_extensions_cli/utils.py 
b/superset-extensions-cli/src/superset_extensions_cli/utils.py
index 7dc739d9dba..9aa17e09365 100644
--- a/superset-extensions-cli/src/superset_extensions_cli/utils.py
+++ b/superset-extensions-cli/src/superset_extensions_cli/utils.py
@@ -16,15 +16,75 @@
 # under the License.
 
 import json  # noqa: TID251
+import re
 import sys
 from pathlib import Path
 from typing import Any
 
+from superset_extensions_cli.exceptions import ExtensionNameError
+from superset_extensions_cli.types import ExtensionNames
+
 if sys.version_info >= (3, 11):
     import tomllib
 else:
     import tomli as tomllib
 
+# Python reserved keywords to avoid in package names
+PYTHON_KEYWORDS = {
+    "and",
+    "as",
+    "assert",
+    "break",
+    "class",
+    "continue",
+    "def",
+    "del",
+    "elif",
+    "else",
+    "except",
+    "exec",
+    "finally",
+    "for",
+    "from",
+    "global",
+    "if",
+    "import",
+    "in",
+    "is",
+    "lambda",
+    "not",
+    "or",
+    "pass",
+    "print",
+    "raise",
+    "return",
+    "try",
+    "while",
+    "with",
+    "yield",
+    "False",
+    "None",
+    "True",
+}
+
+# npm reserved names to avoid
+NPM_RESERVED = {
+    "node_modules",
+    "favicon.ico",
+    "www",
+    "http",
+    "https",
+    "ftp",
+    "localhost",
+    "package.json",
+    "npm",
+    "yarn",
+    "bower_components",
+}
+
+# Extension name pattern: lowercase, start with letter or number, alphanumeric 
+ hyphens
+EXTENSION_NAME_PATTERN = re.compile(r"^[a-z0-9][a-z0-9]*(?:-[a-z0-9]+)*$")
+
 
 def read_toml(path: Path) -> dict[str, Any] | None:
     if not path.is_file():
@@ -40,3 +100,205 @@ def read_json(path: Path) -> dict[str, Any] | None:
         return None
 
     return json.loads(path.read_text())
+
+
+def _normalize_for_identifiers(name: str) -> str:
+    """
+    Normalize display name to clean lowercase words.
+
+    Args:
+        name: Raw display name (e.g., "Hello World!")
+
+    Returns:
+        Normalized string (e.g., "hello world")
+    """
+    # Convert to lowercase
+    normalized = name.lower().strip()
+
+    # Convert underscores and existing hyphens to spaces for consistent 
processing
+    normalized = normalized.replace("_", " ").replace("-", " ")
+
+    # Remove any non-alphanumeric characters except spaces
+    normalized = re.sub(r"[^a-z0-9\s]", "", normalized)
+
+    # Normalize whitespace (collapse multiple spaces, strip)
+    normalized = " ".join(normalized.split())
+
+    return normalized
+
+
+def _normalized_to_kebab(normalized: str) -> str:
+    """Convert normalized string to kebab-case."""
+    return normalized.replace(" ", "-")
+
+
+def _normalized_to_snake(normalized: str) -> str:
+    """Convert normalized string to snake_case."""
+    return normalized.replace(" ", "_")
+
+
+def _normalized_to_camel(normalized: str) -> str:
+    """Convert normalized string to camelCase."""
+    parts = normalized.split()
+    if not parts:
+        return ""
+    # First part lowercase, subsequent parts capitalized
+    return parts[0] + "".join(word.capitalize() for word in parts[1:])
+
+
+def kebab_to_camel_case(kebab_name: str) -> str:
+    """Convert kebab-case to camelCase (e.g., 'hello-world' -> 
'helloWorld')."""
+    parts = kebab_name.split("-")
+    if not parts:
+        return ""
+    # First part lowercase, subsequent parts capitalized
+    return parts[0] + "".join(word.capitalize() for word in parts[1:])
+
+
+def kebab_to_snake_case(kebab_name: str) -> str:
+    """Convert kebab-case to snake_case (e.g., 'hello-world' -> 
'hello_world')."""
+    return kebab_name.replace("-", "_")
+
+
+def name_to_kebab_case(name: str) -> str:
+    """Convert display name directly to kebab-case (e.g., 'Hello World' -> 
'hello-world')."""
+    normalized = _normalize_for_identifiers(name)
+    return _normalized_to_kebab(normalized)
+
+
+# Legacy functions for backward compatibility
+def to_kebab_case(name: str) -> str:
+    """Convert display name to kebab-case. For new code, use 
name_to_kebab_case."""
+    return name_to_kebab_case(name)
+
+
+def to_snake_case(kebab_name: str) -> str:
+    """Convert kebab-case to snake_case. For new code, use 
kebab_to_snake_case."""
+    return kebab_to_snake_case(kebab_name)
+
+
+def validate_extension_id(extension_id: str) -> None:
+    """
+    Validate extension ID format (kebab-case).
+
+    Raises:
+        ExtensionNameError: If ID is invalid
+    """
+    if not extension_id:
+        raise ExtensionNameError("Extension ID cannot be empty")
+
+    # Check for leading/trailing hyphens first
+    if extension_id.startswith("-"):
+        raise ExtensionNameError("Extension ID cannot start with hyphens")
+
+    if extension_id.endswith("-"):
+        raise ExtensionNameError("Extension ID cannot end with hyphens")
+
+    # Check for consecutive hyphens
+    if "--" in extension_id:
+        raise ExtensionNameError("Extension ID cannot have consecutive 
hyphens")
+
+    # Check overall pattern
+    if not EXTENSION_NAME_PATTERN.match(extension_id):
+        raise ExtensionNameError(
+            "Use lowercase letters, numbers, and hyphens only (e.g. 
hello-world)"
+        )
+
+
+def validate_extension_name(name: str) -> str:
+    """
+    Validate and normalize extension name (human-readable).
+
+    Args:
+        extension_name: Raw extension name input
+
+    Returns:
+        Cleaned extension name
+
+    Raises:
+        ExtensionNameError: If extension name is invalid
+    """
+    if not name or not name.strip():
+        raise ExtensionNameError("Extension name cannot be empty")
+
+    # Normalize whitespace: strip and collapse multiple spaces
+    normalized = " ".join(name.strip().split())
+
+    # Check for only whitespace/special chars after normalization
+    if not any(c.isalnum() for c in normalized):
+        raise ExtensionNameError(
+            "Extension name must contain at least one letter or number"
+        )
+
+    return normalized
+
+
+def validate_python_package_name(name: str) -> None:
+    """
+    Validate Python package name (snake_case format).
+
+    Raises:
+        ExtensionNameError: If name is invalid
+    """
+    # Check if it starts with a number (invalid for Python identifiers)
+    if name[0].isdigit():
+        raise ExtensionNameError(f"Package name '{name}' cannot start with a 
number")
+
+    # Check if the first part (before any underscore) is a Python keyword
+    if (first_part := name.split("_")[0]) in PYTHON_KEYWORDS:
+        raise ExtensionNameError(
+            f"Package name cannot start with Python keyword '{first_part}'"
+        )
+
+    # Check if it's a valid Python identifier
+    if not name.replace("_", "a").isalnum():
+        raise ExtensionNameError(f"'{name}' is not a valid Python package 
name")
+
+
+def validate_npm_package_name(name: str) -> None:
+    """
+    Validate npm package name (kebab-case format).
+
+    Raises:
+        ExtensionNameError: If name is invalid
+    """
+    if name.lower() in NPM_RESERVED:
+        raise ExtensionNameError(f"'{name}' is a reserved npm package name")
+
+
+def generate_extension_names(name: str) -> ExtensionNames:
+    """
+    Generate all extension name variants from display name input.
+
+    Flow: Display Name -> Generate ID -> Derive Technical Names from ID
+    Example: "Hello World" -> "hello-world" -> "helloWorld"/"hello_world" 
(from ID)
+
+    Returns:
+        ExtensionNames: Dictionary with all name variants
+
+    Raises:
+        ExtensionNameError: If any generated name is invalid
+    """
+    # Validate and normalize the extension name
+    name = validate_extension_name(name)
+
+    # Generate ID from display name
+    kebab_name = name_to_kebab_case(name)
+
+    # Derive all technical names from the generated ID (not display name)
+    snake_name = kebab_to_snake_case(kebab_name)
+    camel_name = kebab_to_camel_case(kebab_name)
+
+    # Validate the generated names
+    validate_extension_id(kebab_name)
+    validate_python_package_name(snake_name)
+    validate_npm_package_name(kebab_name)
+
+    return ExtensionNames(
+        name=name,
+        id=kebab_name,
+        mf_name=camel_name,
+        backend_name=snake_name,
+        backend_package=f"superset_extensions.{snake_name}",
+        backend_entry=f"superset_extensions.{snake_name}.entrypoint",
+    )
diff --git a/superset-extensions-cli/tests/conftest.py 
b/superset-extensions-cli/tests/conftest.py
index fdb4cd17b90..7292428c4ee 100644
--- a/superset-extensions-cli/tests/conftest.py
+++ b/superset-extensions-cli/tests/conftest.py
@@ -58,25 +58,25 @@ def extension_params():
 @pytest.fixture
 def cli_input_both():
     """CLI input for creating extension with both frontend and backend."""
-    return "test_extension\nTest Extension\n0.1.0\nApache-2.0\ny\ny\n"
+    return "Test Extension\n\n0.1.0\nApache-2.0\ny\ny\n"
 
 
 @pytest.fixture
 def cli_input_frontend_only():
     """CLI input for creating extension with frontend only."""
-    return "test_extension\nTest Extension\n0.1.0\nApache-2.0\ny\nn\n"
+    return "Test Extension\n\n0.1.0\nApache-2.0\ny\nn\n"
 
 
 @pytest.fixture
 def cli_input_backend_only():
     """CLI input for creating extension with backend only."""
-    return "test_extension\nTest Extension\n0.1.0\nApache-2.0\nn\ny\n"
+    return "Test Extension\n\n0.1.0\nApache-2.0\nn\ny\n"
 
 
 @pytest.fixture
 def cli_input_neither():
     """CLI input for creating extension with neither frontend nor backend."""
-    return "test_extension\nTest Extension\n0.1.0\nApache-2.0\nn\nn\n"
+    return "Test Extension\n\n0.1.0\nApache-2.0\nn\nn\n"
 
 
 @pytest.fixture
diff --git a/superset-extensions-cli/tests/test_cli_init.py 
b/superset-extensions-cli/tests/test_cli_init.py
index 8bf00d3c778..78e0d6888c7 100644
--- a/superset-extensions-cli/tests/test_cli_init.py
+++ b/superset-extensions-cli/tests/test_cli_init.py
@@ -43,16 +43,16 @@ def 
test_init_creates_extension_with_both_frontend_and_backend(
 
     assert result.exit_code == 0, f"Command failed with output: 
{result.output}"
     assert (
-        "🎉 Extension Test Extension (ID: test_extension) initialized" in 
result.output
+        "🎉 Extension Test Extension (ID: test-extension) initialized" in 
result.output
     )
 
     # Verify directory structure
-    extension_path = isolated_filesystem / "test_extension"
+    extension_path = isolated_filesystem / "test-extension"
     assert_directory_exists(extension_path, "main extension directory")
 
     expected_structure = create_test_extension_structure(
         isolated_filesystem,
-        "test_extension",
+        "test-extension",
         include_frontend=True,
         include_backend=True,
     )
@@ -73,7 +73,7 @@ def test_init_creates_extension_with_frontend_only(
 
     assert result.exit_code == 0, f"Command failed with output: 
{result.output}"
 
-    extension_path = isolated_filesystem / "test_extension"
+    extension_path = isolated_filesystem / "test-extension"
     assert_directory_exists(extension_path)
 
     # Should have frontend directory and package.json
@@ -96,7 +96,7 @@ def test_init_creates_extension_with_backend_only(
 
     assert result.exit_code == 0, f"Command failed with output: 
{result.output}"
 
-    extension_path = isolated_filesystem / "test_extension"
+    extension_path = isolated_filesystem / "test-extension"
     assert_directory_exists(extension_path)
 
     # Should have backend directory and pyproject.toml
@@ -119,7 +119,7 @@ def 
test_init_creates_extension_with_neither_frontend_nor_backend(
 
     assert result.exit_code == 0, f"Command failed with output: 
{result.output}"
 
-    extension_path = isolated_filesystem / "test_extension"
+    extension_path = isolated_filesystem / "test-extension"
     assert_directory_exists(extension_path)
 
     # Should only have extension.json
@@ -131,53 +131,51 @@ def 
test_init_creates_extension_with_neither_frontend_nor_backend(
 
 
 @pytest.mark.cli
[email protected](
-    "invalid_name,expected_error",
-    [
-        ("test-extension", "must be alphanumeric"),
-        ("test extension", "must be alphanumeric"),
-        ("test.extension", "must be alphanumeric"),
-        ("test@extension", "must be alphanumeric"),
-        ("", "must be alphanumeric"),
-    ],
-)
-def test_init_validates_extension_name(
-    cli_runner, isolated_filesystem, invalid_name, expected_error
-):
-    """Test that init validates extension names according to regex pattern."""
-    cli_input = f"{invalid_name}\n0.1.0\nApache-2.0\ny\ny\n"
+def test_init_accepts_any_display_name(cli_runner, isolated_filesystem):
+    """Test that init accepts any display name and generates proper ID."""
+    cli_input = "My Awesome Extension!\n\n0.1.0\nApache-2.0\ny\ny\n"
     result = cli_runner.invoke(app, ["init"], input=cli_input)
 
-    assert result.exit_code == 1, (
-        f"Expected command to fail for invalid name '{invalid_name}'"
+    assert result.exit_code == 0, f"Should accept display name: 
{result.output}"
+    assert Path("my-awesome-extension").exists(), (
+        "Directory for generated ID should be created"
     )
-    assert expected_error in result.output
 
 
 @pytest.mark.cli
-def test_init_accepts_numeric_extension_name(cli_runner, isolated_filesystem):
-    """Test that init accepts numeric extension ids like '123'."""
-    cli_input = "123\n123\n0.1.0\nApache-2.0\ny\ny\n"
+def test_init_accepts_mixed_alphanumeric_name(cli_runner, isolated_filesystem):
+    """Test that init accepts mixed alphanumeric display names."""
+    cli_input = "Tool 123\n\n0.1.0\nApache-2.0\ny\ny\n"
     result = cli_runner.invoke(app, ["init"], input=cli_input)
 
-    assert result.exit_code == 0, f"Numeric id '123' should be valid: 
{result.output}"
-    assert Path("123").exists(), "Directory for '123' should be created"
+    assert result.exit_code == 0, (
+        f"Mixed alphanumeric display name should be valid: {result.output}"
+    )
+    assert Path("tool-123").exists(), "Directory for 'tool-123' should be 
created"
 
 
 @pytest.mark.cli
 @pytest.mark.parametrize(
-    "valid_id", ["test123", "TestExtension", "test_extension_123", "MyExt_1"]
+    "display_name,expected_id",
+    [
+        ("Test Extension", "test-extension"),
+        ("My Tool v2", "my-tool-v2"),
+        ("Dashboard Helper", "dashboard-helper"),
+        ("Chart Builder Pro", "chart-builder-pro"),
+    ],
 )
-def test_init_with_valid_alphanumeric_names(cli_runner, valid_id):
-    """Test that init accepts various valid alphanumeric names."""
+def test_init_with_various_display_names(cli_runner, display_name, 
expected_id):
+    """Test that init accepts various display names and generates proper 
IDs."""
     with cli_runner.isolated_filesystem():
-        cli_input = f"{valid_id}\nTest Extension\n0.1.0\nApache-2.0\ny\ny\n"
+        cli_input = f"{display_name}\n\n0.1.0\nApache-2.0\ny\ny\n"
         result = cli_runner.invoke(app, ["init"], input=cli_input)
 
         assert result.exit_code == 0, (
-            f"Valid name '{valid_id}' was rejected: {result.output}"
+            f"Valid display name '{display_name}' was rejected: 
{result.output}"
+        )
+        assert Path(expected_id).exists(), (
+            f"Directory for '{expected_id}' was not created"
         )
-        assert Path(valid_id).exists(), f"Directory for '{valid_id}' was not 
created"
 
 
 @pytest.mark.cli
@@ -186,7 +184,7 @@ def test_init_fails_when_directory_already_exists(
 ):
     """Test that init fails gracefully when target directory already exists."""
     # Create the directory first
-    existing_dir = isolated_filesystem / "test_extension"
+    existing_dir = isolated_filesystem / "test-extension"
     existing_dir.mkdir()
 
     result = cli_runner.invoke(app, ["init"], input=cli_input_both)
@@ -203,14 +201,14 @@ def test_extension_json_content_is_correct(
     result = cli_runner.invoke(app, ["init"], input=cli_input_both)
     assert result.exit_code == 0
 
-    extension_path = isolated_filesystem / "test_extension"
+    extension_path = isolated_filesystem / "test-extension"
     extension_json_path = extension_path / "extension.json"
 
     # Verify the JSON structure and values
     assert_json_content(
         extension_json_path,
         {
-            "id": "test_extension",
+            "id": "test-extension",
             "name": "Test Extension",
             "version": "0.1.0",
             "license": "Apache-2.0",
@@ -227,15 +225,20 @@ def test_extension_json_content_is_correct(
     assert "contributions" in frontend
     assert "moduleFederation" in frontend
     assert frontend["contributions"] == {"commands": [], "views": {}, "menus": 
{}}
-    assert frontend["moduleFederation"] == {"exposes": ["./index"]}
+    assert frontend["moduleFederation"] == {
+        "exposes": ["./index"],
+        "name": "testExtension",
+    }
 
     # Verify backend section exists and has correct structure
     assert "backend" in content
     backend = content["backend"]
     assert "entryPoints" in backend
     assert "files" in backend
-    assert backend["entryPoints"] == ["test_extension.entrypoint"]
-    assert backend["files"] == ["backend/src/test_extension/**/*.py"]
+    assert backend["entryPoints"] == 
["superset_extensions.test_extension.entrypoint"]
+    assert backend["files"] == [
+        "backend/src/superset_extensions/test_extension/**/*.py"
+    ]
 
 
 @pytest.mark.cli
@@ -246,14 +249,14 @@ def test_frontend_package_json_content_is_correct(
     result = cli_runner.invoke(app, ["init"], input=cli_input_both)
     assert result.exit_code == 0
 
-    extension_path = isolated_filesystem / "test_extension"
+    extension_path = isolated_filesystem / "test-extension"
     package_json_path = extension_path / "frontend" / "package.json"
 
     # Verify the package.json structure and values
     assert_json_content(
         package_json_path,
         {
-            "name": "test_extension",
+            "name": "test-extension",
             "version": "0.1.0",
             "license": "Apache-2.0",
         },
@@ -275,14 +278,14 @@ def test_backend_pyproject_toml_is_created(
     result = cli_runner.invoke(app, ["init"], input=cli_input_both)
     assert result.exit_code == 0
 
-    extension_path = isolated_filesystem / "test_extension"
+    extension_path = isolated_filesystem / "test-extension"
     pyproject_path = extension_path / "backend" / "pyproject.toml"
 
     assert_file_exists(pyproject_path, "backend pyproject.toml")
 
     # Basic content verification (without parsing TOML for now)
     content = pyproject_path.read_text()
-    assert "test_extension" in content
+    assert "superset_extensions.test_extension" in content
     assert "0.1.0" in content
     assert "Apache-2.0" in content
 
@@ -300,7 +303,7 @@ def test_init_command_output_messages(cli_runner, 
isolated_filesystem, cli_input
     assert "Created .gitignore" in output
     assert "Created frontend folder structure" in output
     assert "Created backend folder structure" in output
-    assert "Extension Test Extension (ID: test_extension) initialized" in 
output
+    assert "Extension Test Extension (ID: test-extension) initialized" in 
output
 
 
 @pytest.mark.cli
@@ -309,7 +312,7 @@ def test_gitignore_content_is_correct(cli_runner, 
isolated_filesystem, cli_input
     result = cli_runner.invoke(app, ["init"], input=cli_input_both)
     assert result.exit_code == 0
 
-    extension_path = isolated_filesystem / "test_extension"
+    extension_path = isolated_filesystem / "test-extension"
     gitignore_path = extension_path / ".gitignore"
 
     assert_file_exists(gitignore_path, ".gitignore")
@@ -329,18 +332,18 @@ def test_gitignore_content_is_correct(cli_runner, 
isolated_filesystem, cli_input
 @pytest.mark.cli
 def test_init_with_custom_version_and_license(cli_runner, isolated_filesystem):
     """Test init with custom version and license parameters."""
-    cli_input = "my_extension\nMy Extension\n2.1.0\nMIT\ny\nn\n"
+    cli_input = "My Extension\n\n2.1.0\nMIT\ny\nn\n"
     result = cli_runner.invoke(app, ["init"], input=cli_input)
 
     assert result.exit_code == 0
 
-    extension_path = isolated_filesystem / "my_extension"
+    extension_path = isolated_filesystem / "my-extension"
     extension_json_path = extension_path / "extension.json"
 
     assert_json_content(
         extension_json_path,
         {
-            "id": "my_extension",
+            "id": "my-extension",
             "name": "My Extension",
             "version": "2.1.0",
             "license": "MIT",
@@ -353,17 +356,17 @@ def test_init_with_custom_version_and_license(cli_runner, 
isolated_filesystem):
 def test_full_init_workflow_integration(cli_runner, isolated_filesystem):
     """Integration test for the complete init workflow."""
     # Test the complete flow with realistic user input
-    cli_input = "awesome_charts\nAwesome Charts\n1.0.0\nApache-2.0\ny\ny\n"
+    cli_input = "Awesome Charts\n\n1.0.0\nApache-2.0\ny\ny\n"
     result = cli_runner.invoke(app, ["init"], input=cli_input)
 
     # Verify success
     assert result.exit_code == 0
 
     # Verify complete directory structure
-    extension_path = isolated_filesystem / "awesome_charts"
+    extension_path = isolated_filesystem / "awesome-charts"
     expected_structure = create_test_extension_structure(
         isolated_filesystem,
-        "awesome_charts",
+        "awesome-charts",
         include_frontend=True,
         include_backend=True,
     )
@@ -374,16 +377,16 @@ def test_full_init_workflow_integration(cli_runner, 
isolated_filesystem):
 
     # Verify all generated files have correct content
     extension_json = load_json_file(extension_path / "extension.json")
-    assert extension_json["id"] == "awesome_charts"
+    assert extension_json["id"] == "awesome-charts"
     assert extension_json["name"] == "Awesome Charts"
     assert extension_json["version"] == "1.0.0"
     assert extension_json["license"] == "Apache-2.0"
 
     package_json = load_json_file(extension_path / "frontend" / "package.json")
-    assert package_json["name"] == "awesome_charts"
+    assert package_json["name"] == "awesome-charts"
 
     pyproject_content = (extension_path / "backend" / 
"pyproject.toml").read_text()
-    assert "awesome_charts" in pyproject_content
+    assert "superset_extensions.awesome_charts" in pyproject_content
 
 
 # Non-interactive mode tests
@@ -395,7 +398,7 @@ def test_init_non_interactive_with_all_options(cli_runner, 
isolated_filesystem):
         [
             "init",
             "--id",
-            "my_ext",
+            "my-ext",
             "--name",
             "My Extension",
             "--version",
@@ -408,15 +411,15 @@ def 
test_init_non_interactive_with_all_options(cli_runner, isolated_filesystem):
     )
 
     assert result.exit_code == 0, f"Command failed with output: 
{result.output}"
-    assert "🎉 Extension My Extension (ID: my_ext) initialized" in result.output
+    assert "🎉 Extension My Extension (ID: my-ext) initialized" in result.output
 
-    extension_path = isolated_filesystem / "my_ext"
+    extension_path = isolated_filesystem / "my-ext"
     assert_directory_exists(extension_path)
     assert_directory_exists(extension_path / "frontend")
     assert_directory_exists(extension_path / "backend")
 
     extension_json = load_json_file(extension_path / "extension.json")
-    assert extension_json["id"] == "my_ext"
+    assert extension_json["id"] == "my-ext"
     assert extension_json["name"] == "My Extension"
     assert extension_json["version"] == "1.0.0"
     assert extension_json["license"] == "MIT"
@@ -430,7 +433,7 @@ def test_init_frontend_only_with_cli_options(cli_runner, 
isolated_filesystem):
         [
             "init",
             "--id",
-            "frontend_ext",
+            "frontend-ext",
             "--name",
             "Frontend Extension",
             "--version",
@@ -444,7 +447,7 @@ def test_init_frontend_only_with_cli_options(cli_runner, 
isolated_filesystem):
 
     assert result.exit_code == 0, f"Command failed with output: 
{result.output}"
 
-    extension_path = isolated_filesystem / "frontend_ext"
+    extension_path = isolated_filesystem / "frontend-ext"
     assert_directory_exists(extension_path / "frontend")
     assert not (extension_path / "backend").exists()
 
@@ -457,7 +460,7 @@ def test_init_backend_only_with_cli_options(cli_runner, 
isolated_filesystem):
         [
             "init",
             "--id",
-            "backend_ext",
+            "backend-ext",
             "--name",
             "Backend Extension",
             "--version",
@@ -471,7 +474,7 @@ def test_init_backend_only_with_cli_options(cli_runner, 
isolated_filesystem):
 
     assert result.exit_code == 0, f"Command failed with output: 
{result.output}"
 
-    extension_path = isolated_filesystem / "backend_ext"
+    extension_path = isolated_filesystem / "backend-ext"
     assert not (extension_path / "frontend").exists()
     assert_directory_exists(extension_path / "backend")
 
@@ -485,7 +488,7 @@ def test_init_prompts_for_missing_options(cli_runner, 
isolated_filesystem):
         [
             "init",
             "--id",
-            "default_ext",
+            "default-ext",
             "--name",
             "Default Extension",
             "--frontend",
@@ -496,7 +499,7 @@ def test_init_prompts_for_missing_options(cli_runner, 
isolated_filesystem):
 
     assert result.exit_code == 0, f"Command failed with output: 
{result.output}"
 
-    extension_path = isolated_filesystem / "default_ext"
+    extension_path = isolated_filesystem / "default-ext"
     extension_json = load_json_file(extension_path / "extension.json")
     assert extension_json["version"] == "0.1.0"
     assert extension_json["license"] == "Apache-2.0"
@@ -510,7 +513,7 @@ def test_init_non_interactive_validates_id(cli_runner, 
isolated_filesystem):
         [
             "init",
             "--id",
-            "invalid-id",
+            "invalid_name",
             "--name",
             "Invalid Extension",
             "--frontend",
@@ -519,4 +522,4 @@ def test_init_non_interactive_validates_id(cli_runner, 
isolated_filesystem):
     )
 
     assert result.exit_code == 1
-    assert "must be alphanumeric" in result.output
+    assert "Use lowercase letters, numbers, and hyphens only" in result.output
diff --git a/superset-extensions-cli/tests/test_name_transformations.py 
b/superset-extensions-cli/tests/test_name_transformations.py
new file mode 100644
index 00000000000..d376c04e170
--- /dev/null
+++ b/superset-extensions-cli/tests/test_name_transformations.py
@@ -0,0 +1,400 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+import pytest
+
+from superset_extensions_cli.exceptions import ExtensionNameError
+from superset_extensions_cli.utils import (
+    generate_extension_names,
+    kebab_to_camel_case,
+    kebab_to_snake_case,
+    name_to_kebab_case,
+    to_snake_case,  # Keep this for backward compatibility testing only
+    validate_extension_name,
+    validate_extension_id,
+    validate_npm_package_name,
+    validate_python_package_name,
+)
+
+
+class TestNameTransformations:
+    """Test name transformation functions."""
+
+    @pytest.mark.parametrize(
+        "display_name,expected",
+        [
+            ("Hello World", "hello-world"),
+            ("Data Explorer", "data-explorer"),
+            ("My Extension", "my-extension"),
+            ("hello-world", "hello-world"),  # Already normalized
+            ("Hello@World!", "helloworld"),  # Special chars removed
+            (
+                "Data_Explorer",
+                "data-explorer",
+            ),  # Underscores become spaces then hyphens
+            ("My   Extension", "my-extension"),  # Multiple spaces normalized
+            ("  Hello World  ", "hello-world"),  # Trimmed
+            ("API v2 Client", "api-v2-client"),  # Numbers preserved
+            ("Simple", "simple"),  # Single word
+        ],
+    )
+    def test_name_to_kebab_case(self, display_name, expected):
+        """Test direct kebab case conversion from display names."""
+        assert name_to_kebab_case(display_name) == expected
+
+    @pytest.mark.parametrize(
+        "kebab_name,expected",
+        [
+            ("hello-world", "helloWorld"),
+            ("data-explorer", "dataExplorer"),
+            ("my-extension", "myExtension"),
+            ("api-v2-client", "apiV2Client"),
+            ("simple", "simple"),  # Single word
+            ("chart-tool", "chartTool"),
+            ("dashboard-helper", "dashboardHelper"),
+        ],
+    )
+    def test_kebab_to_camel_case(self, kebab_name, expected):
+        """Test kebab-case to camelCase conversion."""
+        assert kebab_to_camel_case(kebab_name) == expected
+
+    @pytest.mark.parametrize(
+        "kebab_name,expected",
+        [
+            ("hello-world", "hello_world"),
+            ("data-explorer", "data_explorer"),
+            ("my-extension", "my_extension"),
+            ("api-v2-client", "api_v2_client"),
+            ("simple", "simple"),  # Single word
+            ("chart-tool", "chart_tool"),
+            ("dashboard-helper", "dashboard_helper"),
+        ],
+    )
+    def test_kebab_to_snake_case(self, kebab_name, expected):
+        """Test kebab-case to snake_case conversion."""
+        assert kebab_to_snake_case(kebab_name) == expected
+
+    # Backward compatibility test for remaining legacy function
+    @pytest.mark.parametrize(
+        "input_name,expected",
+        [
+            ("hello-world", "hello_world"),
+            ("data-explorer", "data_explorer"),
+            ("my-extension-name", "my_extension_name"),
+        ],
+    )
+    def test_to_snake_case_legacy(self, input_name, expected):
+        """Test legacy kebab-to-snake conversion function."""
+        assert to_snake_case(input_name) == expected
+
+
+class TestValidation:
+    """Test validation functions."""
+
+    @pytest.mark.parametrize(
+        "valid_display",
+        [
+            "Hello World",
+            "Data Explorer",
+            "My Extension",
+            "Simple",
+            "   Extra   Spaces   ",  # Gets normalized
+        ],
+    )
+    def test_validate_extension_name_valid(self, valid_display):
+        """Test valid display names."""
+        result = validate_extension_name(valid_display)
+        assert result  # Should return normalized name
+        assert "  " not in result  # No double spaces
+
+    @pytest.mark.parametrize(
+        "invalid_display,error_match",
+        [
+            ("", "cannot be empty"),
+            ("   ", "cannot be empty"),
+            ("@#$%", "must contain at least one letter or number"),
+        ],
+    )
+    def test_validate_extension_name_invalid(self, invalid_display, 
error_match):
+        """Test invalid extension names."""
+        with pytest.raises(ExtensionNameError, match=error_match):
+            validate_extension_name(invalid_display)
+
+    @pytest.mark.parametrize(
+        "valid_id",
+        [
+            "hello-world",
+            "data-explorer",
+            "myext",
+            "chart123",
+            "my-tool-v2",
+            "a",  # Single character
+            "extension-with-many-parts",
+        ],
+    )
+    def test_validate_extension_id_valid(self, valid_id):
+        """Test valid extension IDs."""
+        # Should not raise exceptions
+        validate_extension_id(valid_id)
+
+    @pytest.mark.parametrize(
+        "invalid_id,error_match",
+        [
+            ("", "cannot be empty"),
+            ("Hello-World", "Use lowercase"),
+            ("-hello", "cannot start with hyphens"),
+            ("hello-", "cannot end with hyphens"),
+            ("hello--world", "consecutive hyphens"),
+        ],
+    )
+    def test_validate_extension_id_invalid(self, invalid_id, error_match):
+        """Test invalid extension IDs."""
+        with pytest.raises(ExtensionNameError, match=error_match):
+            validate_extension_id(invalid_id)
+
+    @pytest.mark.parametrize(
+        "valid_package",
+        [
+            "hello_world",
+            "data_explorer",
+            "myext",
+            "test123",
+            "package_with_many_parts",
+        ],
+    )
+    def test_validate_python_package_name_valid(self, valid_package):
+        """Test valid Python package names."""
+        # Should not raise exceptions
+        validate_python_package_name(valid_package)
+
+    @pytest.mark.parametrize(
+        "keyword",
+        [
+            "class",
+            "import",
+            "def",
+            "return",
+            "if",
+            "else",
+            "for",
+            "while",
+            "try",
+            "except",
+            "finally",
+            "with",
+            "as",
+            "lambda",
+            "yield",
+            "False",
+            "None",
+            "True",
+        ],
+    )
+    def test_validate_python_package_name_keywords(self, keyword):
+        """Test that Python reserved keywords are rejected."""
+        with pytest.raises(
+            ExtensionNameError, match="Package name cannot start with Python 
keyword"
+        ):
+            validate_python_package_name(keyword)
+
+    @pytest.mark.parametrize(
+        "invalid_package",
+        [
+            "hello-world",  # Hyphens not allowed in Python identifiers
+        ],
+    )
+    def test_validate_python_package_name_invalid(self, invalid_package):
+        """Test invalid Python package names."""
+        with pytest.raises(ExtensionNameError, match="not a valid Python 
package"):
+            validate_python_package_name(invalid_package)
+
+    @pytest.mark.parametrize(
+        "valid_npm",
+        [
+            "hello-world",
+            "data-explorer",
+            "myext",
+            "package-with-many-parts",
+        ],
+    )
+    def test_validate_npm_package_name_valid(self, valid_npm):
+        """Test valid npm package names."""
+        # Should not raise exceptions
+        validate_npm_package_name(valid_npm)
+
+    @pytest.mark.parametrize(
+        "reserved_name",
+        ["node_modules", "npm", "yarn", "package.json", "localhost", 
"favicon.ico"],
+    )
+    def test_validate_npm_package_name_reserved(self, reserved_name):
+        """Test that npm reserved names are rejected."""
+        with pytest.raises(ExtensionNameError, match="reserved npm package 
name"):
+            validate_npm_package_name(reserved_name)
+
+
+class TestNameGeneration:
+    """Test complete name generation."""
+
+    @pytest.mark.parametrize(
+        "display_name,expected_kebab,expected_snake,expected_camel",
+        [
+            ("Hello World", "hello-world", "hello_world", "helloWorld"),
+            ("Data Explorer", "data-explorer", "data_explorer", 
"dataExplorer"),
+            ("My Extension v2", "my-extension-v2", "my_extension_v2", 
"myExtensionV2"),
+            ("Chart Tool", "chart-tool", "chart_tool", "chartTool"),
+            ("Simple", "simple", "simple", "simple"),
+            ("API v2 Client", "api-v2-client", "api_v2_client", "apiV2Client"),
+            (
+                "Dashboard Helper",
+                "dashboard-helper",
+                "dashboard_helper",
+                "dashboardHelper",
+            ),
+        ],
+    )
+    def test_generate_extension_names_complete_flow(
+        self, display_name, expected_kebab, expected_snake, expected_camel
+    ):
+        """Test complete name generation flow from display name to all 
variants."""
+        names = generate_extension_names(display_name)
+
+        # Test all transformations from single source
+        assert names["name"] == display_name
+        assert names["id"] == expected_kebab  # Extension ID (kebab-case)
+        assert names["mf_name"] == expected_camel  # Module Federation 
(camelCase)
+        assert names["backend_name"] == expected_snake  # Python package 
(snake_case)
+        assert names["backend_package"] == 
f"superset_extensions.{expected_snake}"
+        assert (
+            names["backend_entry"] == 
f"superset_extensions.{expected_snake}.entrypoint"
+        )
+
+    @pytest.mark.parametrize(
+        "invalid_display",
+        [
+            "Class Helper",  # Would create 'class_helper' - reserved keyword
+            "Import Tool",  # Would create 'import_tool' - reserved keyword
+            "@#$%",  # All special chars - becomes empty
+            "123 Tool",  # Starts with number after kebab conversion
+        ],
+    )
+    def test_generate_extension_names_invalid(self, invalid_display):
+        """Test invalid name generation scenarios."""
+        with pytest.raises(ExtensionNameError):
+            generate_extension_names(invalid_display)
+
+    def test_generate_extension_names_unicode(self):
+        """Test handling of unicode characters."""
+        names = generate_extension_names("Café Extension")
+        assert "é" not in names["id"]
+        assert names["id"] == "caf-extension"
+        assert names["name"] == "Café Extension"  # Original preserved
+
+    def test_generate_extension_names_special_chars(self):
+        """Test name generation with special characters."""
+        names = generate_extension_names("My@Extension!")
+
+        assert names["name"] == "My@Extension!"
+        assert names["id"] == "myextension"
+        assert names["backend_name"] == "myextension"
+
+    def test_generate_extension_names_case_preservation(self):
+        """Test that display name case is preserved."""
+        names = generate_extension_names("CamelCase Extension")
+        assert names["name"] == "CamelCase Extension"
+        assert names["id"] == "camelcase-extension"
+
+
+class TestEdgeCases:
+    """Test edge cases and boundary conditions."""
+
+    @pytest.mark.parametrize(
+        "edge_case",
+        [
+            "",  # Empty string
+            "   ",  # Only spaces
+            "---",  # Only hyphens
+            "___",  # Only underscores
+        ],
+    )
+    def test_empty_or_invalid_inputs(self, edge_case):
+        """Test inputs that become empty or invalid after processing."""
+        with pytest.raises(ExtensionNameError):
+            generate_extension_names(edge_case)
+
+    def test_minimal_valid_input(self):
+        """Test minimal valid input."""
+        names = generate_extension_names("A Extension")
+        assert names["id"] == "a-extension"
+        assert names["backend_name"] == "a_extension"
+
+    def test_numbers_handling(self):
+        """Test handling of numbers in names."""
+        names = generate_extension_names("Tool 123 v2")
+        assert names["id"] == "tool-123-v2"
+        assert names["backend_name"] == "tool_123_v2"
+
+    def test_id_based_name_generation(self):
+        """Test that technical names are derived from ID, not display name."""
+        # Simulate manual ExtensionNames construction with custom ID
+        display_name = "My Awesome Chart Builder Pro"
+        extension_id = "chart-builder"  # Much shorter than display name
+
+        # Create names using ID-based generation (new behavior)
+        from superset_extensions_cli.types import ExtensionNames
+
+        names = ExtensionNames(
+            name=display_name,
+            id=extension_id,
+            mf_name=kebab_to_camel_case(extension_id),  # From ID: 
"chartBuilder"
+            backend_name=kebab_to_snake_case(extension_id),  # From ID: 
"chart_builder"
+            
backend_package=f"superset_extensions.{kebab_to_snake_case(extension_id)}",
+            
backend_entry=f"superset_extensions.{kebab_to_snake_case(extension_id)}.entrypoint",
+        )
+
+        # Verify technical names come from ID, not display name
+        assert names["name"] == "My Awesome Chart Builder Pro"  # Display name 
preserved
+        assert names["id"] == "chart-builder"  # Extension ID
+        assert (
+            names["mf_name"] == "chartBuilder"
+        )  # From ID, not "myAwesomeChartBuilderPro"
+        assert (
+            names["backend_name"] == "chart_builder"
+        )  # From ID, not "my_awesome_chart_builder_pro"
+        assert names["backend_package"] == "superset_extensions.chart_builder"
+        assert names["backend_entry"] == 
"superset_extensions.chart_builder.entrypoint"
+
+    def test_generate_names_uses_id_based_technical_names(self):
+        """Test that generate_extension_names uses ID-based generation for 
technical names."""
+        display_name = "Hello World"
+
+        # Generated names should use ID-based technical name generation
+        names = generate_extension_names(display_name)
+
+        # Verify the ID was generated from display name
+        assert names["id"] == "hello-world"
+
+        # Verify technical names were generated from the ID, not original 
display name
+        assert names["mf_name"] == kebab_to_camel_case("hello-world")  # 
"helloWorld"
+        assert names["backend_name"] == kebab_to_snake_case(
+            "hello-world"
+        )  # "hello_world"
+
+        # For this simple case, the results are the same as before, but the 
path is different:
+        # Old path: Display Name -> camelCase directly
+        # New path: Display Name -> ID -> camelCase from ID
+        assert names["mf_name"] == "helloWorld"
+        assert names["backend_name"] == "hello_world"
diff --git a/superset-extensions-cli/tests/test_templates.py 
b/superset-extensions-cli/tests/test_templates.py
index 249d36034e7..30d2115be74 100644
--- a/superset-extensions-cli/tests/test_templates.py
+++ b/superset-extensions-cli/tests/test_templates.py
@@ -42,8 +42,12 @@ def jinja_env(templates_dir):
 def template_context():
     """Default template context for testing."""
     return {
-        "id": "test_extension",
         "name": "Test Extension",
+        "id": "test-extension",
+        "mf_name": "testExtension",
+        "backend_name": "test_extension",
+        "backend_package": "superset_extensions.test_extension",
+        "backend_entry": "superset_extensions.test_extension.entrypoint",
         "version": "0.1.0",
         "license": "Apache-2.0",
         "include_frontend": True,
@@ -64,7 +68,7 @@ def 
test_extension_json_template_renders_with_both_frontend_and_backend(
     parsed = json.loads(rendered)
 
     # Verify basic fields
-    assert parsed["id"] == "test_extension"
+    assert parsed["id"] == "test-extension"
     assert parsed["name"] == "Test Extension"
     assert parsed["version"] == "0.1.0"
     assert parsed["license"] == "Apache-2.0"
@@ -76,13 +80,18 @@ def 
test_extension_json_template_renders_with_both_frontend_and_backend(
     assert "contributions" in frontend
     assert "moduleFederation" in frontend
     assert frontend["contributions"] == {"commands": [], "views": {}, "menus": 
{}}
-    assert frontend["moduleFederation"] == {"exposes": ["./index"]}
+    assert frontend["moduleFederation"] == {
+        "exposes": ["./index"],
+        "name": "testExtension",
+    }
 
     # Verify backend section exists
     assert "backend" in parsed
     backend = parsed["backend"]
-    assert backend["entryPoints"] == ["test_extension.entrypoint"]
-    assert backend["files"] == ["backend/src/test_extension/**/*.py"]
+    assert backend["entryPoints"] == 
["superset_extensions.test_extension.entrypoint"]
+    assert backend["files"] == [
+        "backend/src/superset_extensions/test_extension/**/*.py"
+    ]
 
 
 @pytest.mark.unit
@@ -127,7 +136,7 @@ def 
test_frontend_package_json_template_renders_correctly(jinja_env, template_co
     parsed = json.loads(rendered)
 
     # Verify basic package info
-    assert parsed["name"] == "test_extension"
+    assert parsed["name"] == "test-extension"
     assert parsed["version"] == "0.1.0"
     assert parsed["license"] == "Apache-2.0"
     assert parsed["private"] is True
@@ -169,19 +178,34 @@ def 
test_backend_pyproject_toml_template_renders_correctly(jinja_env, template_c
 # Template Rendering with Different Parameters Tests
 @pytest.mark.unit
 @pytest.mark.parametrize(
-    "id_,name",
+    "extension_id,name,backend_name",
     [
-        ("simple_extension", "Simple Extension"),
-        ("MyExtension123", "My Extension 123"),
-        ("complex_extension_name_123", "Complex Extension Name 123"),
-        ("ext", "Ext"),
+        ("simple-extension", "Simple Extension", "simple_extension"),
+        ("my-extension-123", "My Extension 123", "my_extension_123"),
+        (
+            "complex-extension-name-123",
+            "Complex Extension Name 123",
+            "complex_extension_name_123",
+        ),
+        ("ext", "Ext", "ext"),
     ],
 )
-def test_template_rendering_with_different_ids(jinja_env, id_, name):
+def test_template_rendering_with_different_ids(
+    jinja_env, extension_id, name, backend_name
+):
     """Test templates render correctly with various extension ids/names."""
+    # Generate camelCase name for webpack from extension ID (new ID-based 
approach)
+    from superset_extensions_cli.utils import kebab_to_camel_case
+
+    mf_name = kebab_to_camel_case(extension_id)
+
     context = {
-        "id": id_,
+        "id": extension_id,
         "name": name,
+        "mf_name": mf_name,
+        "backend_name": backend_name,
+        "backend_package": f"superset_extensions.{backend_name}",
+        "backend_entry": f"superset_extensions.{backend_name}.entrypoint",
         "version": "1.0.0",
         "license": "MIT",
         "include_frontend": True,
@@ -193,23 +217,27 @@ def test_template_rendering_with_different_ids(jinja_env, 
id_, name):
     rendered = template.render(context)
     parsed = json.loads(rendered)
 
-    assert parsed["id"] == id_
+    assert parsed["id"] == extension_id
     assert parsed["name"] == name
-    assert parsed["backend"]["entryPoints"] == [f"{id_}.entrypoint"]
-    assert parsed["backend"]["files"] == [f"backend/src/{id_}/**/*.py"]
+    assert parsed["backend"]["entryPoints"] == [
+        f"superset_extensions.{backend_name}.entrypoint"
+    ]
+    assert parsed["backend"]["files"] == [
+        f"backend/src/superset_extensions/{backend_name}/**/*.py"
+    ]
 
     # Test package.json template
     template = jinja_env.get_template("frontend/package.json.j2")
     rendered = template.render(context)
     parsed = json.loads(rendered)
 
-    assert parsed["name"] == id_
+    assert parsed["name"] == extension_id
 
     # Test pyproject.toml template
     template = jinja_env.get_template("backend/pyproject.toml.j2")
     rendered = template.render(context)
 
-    assert id_ in rendered
+    assert f"superset_extensions.{backend_name}" in rendered
 
 
 @pytest.mark.unit
@@ -219,6 +247,7 @@ def 
test_template_rendering_with_different_versions(jinja_env, version):
     context = {
         "id": "test_ext",
         "name": "Test Extension",
+        "mf_name": "testExtension",
         "version": version,
         "license": "Apache-2.0",
         "include_frontend": True,
@@ -248,6 +277,10 @@ def 
test_template_rendering_with_different_licenses(jinja_env, license_type):
     context = {
         "id": "test_ext",
         "name": "Test Extension",
+        "mf_name": "testExtension",
+        "backend_name": "test_ext",
+        "backend_package": "superset_extensions.test_ext",
+        "backend_entry": "superset_extensions.test_ext.entrypoint",
         "version": "1.0.0",
         "license": license_type,
         "include_frontend": True,
@@ -314,6 +347,10 @@ def test_template_context_edge_cases(jinja_env):
     minimal_context = {
         "id": "minimal",
         "name": "Minimal",
+        "mf_name": "minimal",
+        "backend_name": "minimal",
+        "backend_package": "superset_extensions.minimal",
+        "backend_entry": "superset_extensions.minimal.entrypoint",
         "version": "1.0.0",
         "license": "MIT",
         "include_frontend": False,
diff --git a/superset-frontend/src/extensions/ExtensionsManager.ts 
b/superset-frontend/src/extensions/ExtensionsManager.ts
index 2c606553ed7..d546b72debc 100644
--- a/superset-frontend/src/extensions/ExtensionsManager.ts
+++ b/superset-frontend/src/extensions/ExtensionsManager.ts
@@ -160,7 +160,9 @@ class ExtensionsManager {
     // Initialize Webpack module federation
     // @ts-expect-error
     await __webpack_init_sharing__('default');
-    const container = (window as any)[id];
+    // Use moduleFederationName (camelCase) for webpack container access, 
fallback to id for compatibility
+    const containerName = (extension as any).moduleFederationName || id;
+    const container = (window as any)[containerName];
 
     // @ts-expect-error
     await container.init(__webpack_share_scopes__.default);
diff --git a/superset/extensions/utils.py b/superset/extensions/utils.py
index 883c9114728..1350d92c785 100644
--- a/superset/extensions/utils.py
+++ b/superset/extensions/utils.py
@@ -80,6 +80,34 @@ class InMemoryFinder(importlib.abc.MetaPathFinder):
 
             self.modules[mod_name] = (content, is_package, full_path)
 
+        # Create namespace packages for all parent modules
+        # This ensures 'superset_extensions' namespace package exists
+        namespace_packages: set[str] = set()
+        for mod_name in list(self.modules.keys()):
+            parts = mod_name.split(".")
+            for i in range(1, len(parts)):
+                namespace_name = ".".join(parts[:i])
+                if namespace_name not in self.modules:
+                    namespace_packages.add(namespace_name)
+
+        # Add namespace packages
+        for ns_name in namespace_packages:
+            # Create a virtual __init__.py path for the namespace package
+            if is_virtual_path:
+                ns_path = f"{source_base_path}/backend/src/"
+                f"{ns_name.replace('.', '/')}/__init__.py"
+            else:
+                ns_path = str(
+                    Path(source_base_path)
+                    / "backend"
+                    / "src"
+                    / ns_name.replace(".", "/")
+                    / "__init__.py"
+                )
+
+            # Namespace packages have empty content
+            self.modules[ns_name] = (b"", True, ns_path)
+
     def _get_module_name(self, file_path: str) -> Tuple[str, bool]:
         parts = list(Path(file_path).parts)
         is_package = parts[-1] == "__init__.py"
@@ -217,6 +245,7 @@ def build_extension_data(extension: LoadedExtension) -> 
dict[str, Any]:
             {
                 "remoteEntry": remote_entry_url,
                 "exposedModules": module_federation.exposes,
+                "moduleFederationName": module_federation.name,
                 "contributions": frontend.contributions.model_dump(),
             }
         )

Reply via email to