# Extension File Structure

Extensions are split into three main parts: Frontend, Backend, and Database.

All Extensions have a package name, which is defined in the backend `Cargo.toml` file, and is also required in the `Metadata.toml` file.
These are semi-java-like package names, so they should be all lowercase, and can contain dots, for example `dev.0x7d8.test`.

Package name with underscores (also referred to as package identifiers) means dots are replaced with underscores, so `dev.0x7d8.test` turns into `dev_0x7d8_test`.

## Initializing an Extension

Use the extension templates to get an extension up and running quickly.

```bash
# create a new extension from the template, replace the name with your package name with underscores
panel-rs extensions init dev.0x7d8.test # <-- replace this with your package name
```

## Frontend

```bash
backend-extensions/
  (package_identifier)/
    frontend/
    package.json # REQUIRED file containing additional dependencies
    public/ # optional directory to include static files,
      file1.jpg # this file would be available at <url>/file1.jpg
    src/ # REQUIRED directory for typescript src
      app.css # optional css file, bundled as its own chunk so it can be disabled with the extension
      index.(ts|tsx) # REQUIRED file containing extension entrypoint
      translations.ts # optional file containing extension translations
```

::: info Compatibility symlinks
Everything an extension owns lives under `backend-extensions/<package_identifier>/`. The older paths `frontend/extensions/<identifier>` and `database/extension-migrations/<identifier>` still exist as symlinks so existing tooling keeps working, and which of the pair is the real directory depends on the container type. Treat `backend-extensions/` as canonical: deleting or copying "the directory" through the legacy path may only move a link.
:::

### package.json

Add dependencies for your frontend extension code to this `package.json`, or leave it as-is - the required extension code and all dependencies of the base panel are already available.

```json
{
  "name": "extension",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "main": "src/index.ts",
  "types": "src/index.ts",
  "dependencies": {
    "shared": "workspace:*"
  }
}
```

### src/index.ts

```ts
import { Extension, ExtensionContext } from 'shared';
import type { MantineThemeOverride } from '@mantine/core';

// the class name doesn't matter much, but naming it after your package is advisable
class Dev0x7d8TestExtension extends Extension {
  public cardConfigurationPage: React.FC | null = null;
  public cardComponent: React.FC | null = null;
  public cardIcon: React.ReactNode = null;

  // Your extension entrypoint, this runs when the page is loaded
  public initialize(ctx: ExtensionContext): void {
    console.log('Dev0x7d8TestExtension initialized!', ctx);
  }

  // Your extension mantine theme entrypoint, this runs when the page is loaded
  public initializeMantineTheme(ctx: ExtensionContext): MantineThemeOverride {
    return {};
  }

  // Your extension can also provide a resolver for css variables, this runs when the page is loaded
  public initializeMantineCssResolver(ctx: ExtensionContext): CSSVariablesResolver | null {
    return null;
  }

  /**
   * Your extension call processor, this can be called by other extensions to interact with yours,
   * if the call does not apply to your extension, simply return `ctx.skip()` to continue the matching process.
   *
   * Optimally (if applies) make sure your calls are globally unique, for example by prepending them with `yourauthorname_yourextensioname_`
   */
  public processCall(ctx: ExtensionContext, name: string, args: object): unknown {
    return ctx.skip();
  }
  
  // https://typedocs.calagopus.com/classes/extensions_shared_src_extension.Extension
}

export default new Dev0x7d8TestExtension();
```

## Backend

```bash
backend-extensions/
  (package_name_with_underscores)/
    Cargo.toml # REQUIRED file containing extension identifier (again), Author information and dependencies
    Metadata.toml # REQUIRED file containing additional extension information
    src/ # REQUIRED directory for backend rust src
      lib.rs # REQUIRED file containing extension backend entrypoint
```

### Cargo.toml

```toml
[package]
name = "dev_0x7d8_test" # once again, package name with underscores
description = "Test John Pork effortlessly." # short description of your extension
authors = ["0x7d8"] # authors of your extension
version = "1.0.0" # version of your extension
edition = { workspace = true }

[dependencies]
shared = { workspace = true }
async-trait = { workspace = true }
tracing = { workspace = true }
```

### Metadata.toml

```toml
package_name = "dev.0x7d8.test" # package name without underscores
name = "0x7d8 Extension Test" # human-readable name of your extension
panel_version = ">=1.1.0" # panel version requirement of your extension, must be a valid semver comparator
```

::: warning
`panel_version` is enforced when the extension is loaded. The requirement must rule out panel versions older than `1.1.0` - a requirement like `>=1.0.0` (which would *admit* pre-1.1.0 panels) is declined outright, and an extension whose requirement doesn't match the running panel version won't load either.
:::

### src/lib.rs

```rs
use shared::{State, extensions::Extension};

#[derive(Default)]
pub struct ExtensionStruct; // must be named this, must implement Default and Send

#[async_trait::async_trait]
impl Extension for ExtensionStruct {
    async fn initialize(&mut self, _state: State) {
        tracing::info!("dev_0x7d8_test extension initialize called");
    }
    
    // https://cratedocs.calagopus.com/shared/extensions/trait.Extension
}
```

## Database (optional)

```bash
backend-extensions/
  (package_identifier)/
    migrations/
    (yyyymmddhhmmss)_migration_name/
      up.sql # REQUIRED file containing the SQL statements to apply the migration
      down.sql # REQUIRED file containing the SQL statements to rollback the migration
```

::: info
Auto-generate the migration files by running `panel-rs database-migrator create <package_name>` - this creates a new migration with the correct timestamp and file structure for you to fill in.
:::

### up.sql

```sql
-- SQL statements to apply the migration, for example:
CREATE TABLE IF NOT EXISTS dev_0x7d8_test_table (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(255) NOT NULL,
    created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
```

### down.sql

```sql
-- SQL statements to rollback the migration, for example:
DROP TABLE IF EXISTS dev_0x7d8_test_table;
```
