fix(extensions): enforce correct naming conventions (#38167)

This commit is contained in:
Ville Brofeldt
2026-02-23 08:21:35 -08:00
committed by GitHub
parent 6e94a6c21a
commit 40f609fdce
17 changed files with 1118 additions and 167 deletions

View File

@@ -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"]
}
}
```

View File

@@ -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