dockview-core
Version:
Zero dependency layout manager supporting tabs, groups, grids and splitviews for vanilla TypeScript
171 lines (170 loc) • 6.08 kB
JavaScript
/**
* Internal module system for dockview.
*
* Modules are feature bundles that register services into the dockview
* component. `registerModules(...)` is the one public entry point — it lets a
* sibling package contribute modules that `DockviewComponent` picks up at
* construction. The richer opt-in surface (a per-component `modules` option,
* framework wrappers) is still reserved for a future version; the module
* authoring API (`defineModule`, the service contracts) remains internal.
*/
/**
* Typed helper for defining a module. Enforces that the factory's return
* type matches the slot in ServiceCollection at compile time, replacing
* the manual cast each module file would otherwise need.
*/
export function defineModule(config) {
return {
moduleName: config.name,
services: {
[config.serviceKey]: config.create,
},
init: config.init
? (host, services) => config.init(host, services[config.serviceKey])
: undefined,
dependsOn: config.dependsOn,
};
}
const _warnedMissingModule = new Set();
/**
* For tests — clears the once-per-key dedup cache used by `assertModule`.
*/
export function _resetMissingModuleWarnings() {
_warnedMissingModule.clear();
}
/**
* Returns the service if its module is registered, otherwise logs a
* deduplicated console error and returns `undefined`. Modelled on AG Grid's
* `assertModuleRegistered`: missing modules never throw — they degrade the
* affected feature to a no-op so consuming applications don't crash in
* production.
*
* Use at public-API entry points where the caller wants to surface which
* module is missing. For internal/lifecycle paths, plain `?.` chaining on
* the service slot is preferred — no log, just a silent no-op.
*/
export function assertModule(service, moduleName, context) {
if (service !== undefined) {
return service;
}
const key = `${moduleName}|${context !== null && context !== void 0 ? context : ''}`;
if (_warnedMissingModule.has(key)) {
return undefined;
}
_warnedMissingModule.add(key);
const where = context ? ` for ${context}` : '';
// eslint-disable-next-line no-console
console.error(`dockview: module "${moduleName}" is not registered${where}.`);
return undefined;
}
export class ModuleRegistry {
constructor() {
this._modules = new Map();
this._services = {};
this._initDisposables = [];
}
get services() {
return this._services;
}
register(module) {
if (this._modules.has(module.moduleName)) {
return;
}
if (module.dependsOn) {
for (const dep of module.dependsOn) {
this.register(dep);
}
}
this._modules.set(module.moduleName, module);
}
initialize(host) {
for (const module of this._modules.values()) {
if (!module.services) {
continue;
}
for (const [name, factory] of Object.entries(module.services)) {
this._services[name] = factory(host);
}
}
}
postConstruct(host) {
for (const module of this._modules.values()) {
if (module.init) {
this._initDisposables.push(module.init(host, this._services));
}
}
}
has(moduleName) {
return this._modules.has(moduleName);
}
dispose() {
// Tear down init() subscriptions first so they stop firing into
// services that are about to be disposed.
for (const disposable of this._initDisposables) {
disposable.dispose();
}
this._initDisposables.length = 0;
for (const service of Object.values(this._services)) {
if (service !== undefined &&
typeof service.dispose === 'function') {
service.dispose();
}
}
}
}
/**
* Process-global list of modules registered via {@link registerModules}.
* `DockviewComponent` appends these to its built-in set at construction, so
* importing a package that calls `registerModules(...)` (e.g. `dockview`)
* makes those modules available to every component in the process —
* modelled on AG Grid's `ModuleRegistry.registerModules`.
*/
const _globalModules = [];
/**
* Register modules globally. Idempotent per `moduleName` — registering the
* same module twice is a no-op. Intended to be called once at import time by
* the package that bundles a given set of modules.
*/
export function registerModules(modules) {
for (const module of modules) {
if (_globalModules.some((m) => m.moduleName === module.moduleName)) {
continue;
}
_globalModules.push(module);
}
}
/**
* Returns the globally-registered modules (a copy). `DockviewComponent` reads
* this to extend its built-in module set.
*/
export function getRegisteredModules() {
return [..._globalModules];
}
/**
* For tests — clears the global module registry.
*/
export function clearRegisteredModules() {
_globalModules.length = 0;
}
/**
* This marker exists for ONE purpose: a developer warning about the v7 package
* renames. It has no functional effect on dockview's behaviour. Following the
* renames, `dockview-core` is internal and `dockview` is the public JavaScript
* package; `dockview` calls {@link markDockviewPackageLoaded} on import so that
* `dockview-core` can detect — and warn about — being used directly.
*/
let _dockviewPackageLoaded = false;
/**
* Called once by the `dockview` package on import, solely so `dockview-core`
* can warn when it is used directly (see above). Not used for anything else.
*/
export function markDockviewPackageLoaded() {
_dockviewPackageLoaded = true;
}
/**
* Whether the `dockview` package has been loaded in this process. Used only to
* gate the "don't use dockview-core directly" developer warning.
*/
export function isDockviewPackageLoaded() {
return _dockviewPackageLoaded;
}