Skip to content

Engine - Modules - Discovery and Manifest #36

Description

@ollieread

Discovery is the process of scanning module sources, reading metadata, and producing the manifest and registrar structures the registry consumes. How this happens depends on the panel's mode, abstracted behind a ModuleSource interface.

ModuleSource

ModuleSource is the interface that the registry consumes. It provides the full set of module manifests and registrar metadata regardless of how they were produced. Two implementations:

CachedModuleSource (production)

Used in production mode. Loads bundled module metadata from a prebuilt file shipped inside the binary, and external module metadata from a JSON cache file at $paths->cache. Both are hydrated via fromArray(). No reflection occurs at runtime.

LiveModuleSource (debug)

Used in debug mode. Discovers and reflects all modules at boot:

  • Bundled modules are reflected from their registrar classes in the panel's own codebase
  • External modules are discovered from $paths->modules using Composer as a library, then their registrar classes are reflected

Changes to module code are picked up immediately without rebuilding or re-running discovery. This is the expected mode for local development and module development in Docker.

Bundled modules

First-party modules shipped inside the binary as part of the panel's own codebase. Their manifests are declared in code. In production mode their registrar metadata is prebuilt during the build step. In debug mode their registrar classes are reflected at boot.

External module discovery

External modules are installed into the modules directory ($paths->modules), which is a self-contained Composer project. Discovery reads the Composer lock file inside this directory to find all packages of type tgp-module. Composer is used as a library rather than a standalone tool.

The provide block in the modules composer.json is regenerated from the panel's own lock file during discovery to prevent dependency conflicts.

Module metadata

Each external module declares metadata in its composer.json under the extra.tgp key:

  • name — human-readable display name
  • registrar — fully qualified class name of the module's registrar
  • capabilities — list of capability identifiers the module declares
  • icon — optional icon reference
  • definition — optional module definition metadata

Standard Composer fields (package name, version, description, autoload) are read from the package itself.

ModuleManifest

ModuleManifest is a final readonly DTO containing the full resolved metadata for a single module:

  • Module ident (derived from package name for external, declared for bundled)
  • Vendor, version, name, description
  • Capabilities (as Capability enum cases)
  • Core flag (true for bundled modules)
  • Registrar class name
  • Icon, definition

Built by ModuleManifestBuilder from a Composer package for external modules. Bundled modules construct their manifests directly.

ModuleRegistrar (metadata)

ModuleRegistrar is a final readonly metadata class containing reflected metadata about a registrar class:

  • Which methods have #[Register], #[Boot], and #[Collect] attributes
  • For #[Collect] methods: the collector type accepted, whether #[Unscoped] is present, and the optional PanelContext parameter

Built by ModuleRegistrarBuilder using reflection. In production this happens at build time (bundled) or during discovery (external). In debug mode this happens at boot for all modules.

Capability enum

Capability is a string-backed enum declaring all recognised capabilities (CrossRoutes, ExtendSchema, ModifyUi, DaemonAccess, etc.). Capabilities are declared by modules in their manifest and checked at runtime by the code that enforces them. The module system itself does not enforce capabilities.

Registrar attributes

  • #[Register] — marks the method called during the register lifecycle phase
  • #[Boot] — marks the method called during the boot lifecycle phase
  • #[Collect] — marks a method that participates in collection for a given collector type
  • #[Unscoped] — marks a #[Collect] method as operating outside its module's implicit scope

Cache file

Discovery produces a JSON cache file written to $paths->cache. The file contains external module data only:

  • A map of module idents to serialised ModuleManifest data
  • A map of module idents to serialised ModuleRegistrar metadata

The file is regenerated in full on every discovery run. At boot, CachedModuleSource hydrates external manifests and registrars from this file via fromArray().

Prebuilt registrar file

During the build step, bundled module registrar classes are reflected and the resulting metadata is written to a generated file that ships inside the binary. CachedModuleSource loads this at boot in production mode. LiveModuleSource ignores it and reflects at boot instead.

DI integration

  • #[Manifest('ident')] resolvable attribute and resolver — injects a ModuleManifest for the given ident
  • #[Registrar('ident')] resolvable attribute and resolver — injects a ModuleRegistrar for the given ident

Tasks

  • Define Capability enum with all capability cases
  • Define #[Register], #[Boot], #[Collect], and #[Unscoped] registrar attributes
  • Implement ModuleManifest final readonly DTO with fromArray()
  • Implement ModuleManifestBuilder for constructing a manifest from a Composer package
  • Implement ModuleRegistrar final readonly metadata class with fromArray()
  • Implement ModuleRegistrarBuilder using reflection on the registrar class
  • Define ModuleSource interface
  • Implement CachedModuleSource loading from prebuilt file and JSON cache
  • Implement LiveModuleSource with full discovery and reflection at boot
  • Implement external module discovery using Composer as a library
  • Implement provide block regeneration from the panel lock file
  • Implement JSON cache file generation for external modules
  • Implement prebuilt registrar file generation for bundled modules
  • Implement #[Manifest] resolvable attribute and resolver
  • Implement #[Registrar] resolvable attribute and resolver
  • Write tests for ModuleManifestBuilder from a mock Composer package
  • Write tests for ModuleRegistrarBuilder reflection output
  • Write tests for CachedModuleSource loading and hydration
  • Write tests for LiveModuleSource discovery and reflection
  • Write tests for JSON cache file generation and structure
  • Write tests for #[Manifest] and #[Registrar] resolution

Metadata

Metadata

Assignees

Labels

layer: engineBase framework and engine work

Type

Fields

No fields configured for feat.

Projects

Status
Up next

Relationships

None yet

Development

No branches or pull requests

Issue actions