mirror of
https://github.com/FrankerFaceZ/FrankerFaceZ.git
synced 2025-08-02 16:08:31 +00:00
WIP async modules
This commit is contained in:
parent
0d433c3ebd
commit
0fd86b1bb8
12 changed files with 522 additions and 274 deletions
|
@ -284,7 +284,7 @@ export default class AddonManager extends Module {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Error if this takes more than 5 seconds.
|
// Error if this takes more than 5 seconds.
|
||||||
await timeout(this.waitFor(`addon.${id}:registered`), 5000);
|
await timeout(this.waitFor(`addon.${id}:instanced`), 5000);
|
||||||
|
|
||||||
module = this.resolve(`addon.${id}`);
|
module = this.resolve(`addon.${id}`);
|
||||||
if ( module && ! module.loaded )
|
if ( module && ! module.loaded )
|
||||||
|
|
|
@ -11,6 +11,8 @@ import {serializeBlob, deserializeBlob} from 'utilities/blobs';
|
||||||
import SettingsManager from './settings/index';
|
import SettingsManager from './settings/index';
|
||||||
|
|
||||||
class FFZBridge extends Module {
|
class FFZBridge extends Module {
|
||||||
|
static construct_requires = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
const start_time = performance.now(),
|
const start_time = performance.now(),
|
||||||
|
|
|
@ -45,11 +45,11 @@ function sortExperimentLog(a,b) {
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
export default class ExperimentManager extends Module {
|
export default class ExperimentManager extends Module {
|
||||||
|
static construct_requires = ['settings'];
|
||||||
|
|
||||||
constructor(...args) {
|
constructor(...args) {
|
||||||
super(...args);
|
super(...args);
|
||||||
|
|
||||||
this.inject('settings');
|
|
||||||
|
|
||||||
this.settings.addUI('experiments', {
|
this.settings.addUI('experiments', {
|
||||||
path: 'Debugging > Experiments',
|
path: 'Debugging > Experiments',
|
||||||
component: 'experiments',
|
component: 'experiments',
|
||||||
|
@ -289,7 +289,7 @@ export default class ExperimentManager extends Module {
|
||||||
return window.__twilightSettings.experiments;
|
return window.__twilightSettings.experiments;
|
||||||
|
|
||||||
const core = this.resolve('site')?.getCore?.();
|
const core = this.resolve('site')?.getCore?.();
|
||||||
return core && core.experiments.experiments;
|
return core && core.experiments?.experiments;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
130
src/i18n.js
130
src/i18n.js
|
@ -69,6 +69,8 @@ const FACES = ['(・`ω´・)', ';;w;;', 'owo', 'ono', 'oAo', 'oxo', 'ovo;', 'Uw
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
export class TranslationManager extends Module {
|
export class TranslationManager extends Module {
|
||||||
|
static construct_requires = null;
|
||||||
|
|
||||||
constructor(...args) {
|
constructor(...args) {
|
||||||
super(...args);
|
super(...args);
|
||||||
this.inject('settings');
|
this.inject('settings');
|
||||||
|
@ -90,7 +92,71 @@ export class TranslationManager extends Module {
|
||||||
this.changed_strings = 0;
|
this.changed_strings = 0;
|
||||||
this.capturing = false;
|
this.capturing = false;
|
||||||
this.captured = new Map;
|
this.captured = new Map;
|
||||||
|
}
|
||||||
|
|
||||||
|
getLocaleOptions(val) {
|
||||||
|
if( val === undefined )
|
||||||
|
val = this.settings.get('i18n.locale');
|
||||||
|
|
||||||
|
const normal_out = [],
|
||||||
|
joke_out = [];
|
||||||
|
|
||||||
|
for(const locale of this.availableLocales) {
|
||||||
|
const data = this.localeData[locale];
|
||||||
|
let title = data?.native_name || data?.name || locale;
|
||||||
|
|
||||||
|
if ( data?.coverage != null && data?.coverage < 100 )
|
||||||
|
title = this.t('i18n.locale-coverage', '{name} ({coverage,number,percent} Complete)', {
|
||||||
|
name: title,
|
||||||
|
coverage: data.coverage / 100
|
||||||
|
});
|
||||||
|
|
||||||
|
const entry = {
|
||||||
|
selected: val === locale,
|
||||||
|
value: locale,
|
||||||
|
title
|
||||||
|
};
|
||||||
|
|
||||||
|
if ( data?.joke )
|
||||||
|
joke_out.push(entry);
|
||||||
|
else
|
||||||
|
normal_out.push(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
normal_out.sort((a, b) => a.title.localeCompare(b.title));
|
||||||
|
joke_out.sort((a, b) => a.title.localeCompare(b.title));
|
||||||
|
|
||||||
|
let out = [{
|
||||||
|
selected: val === -1,
|
||||||
|
value: -1,
|
||||||
|
i18n_key: 'setting.appearance.localization.general.language.twitch',
|
||||||
|
title: "Use Twitch's Language"
|
||||||
|
}];
|
||||||
|
|
||||||
|
if ( normal_out.length ) {
|
||||||
|
out.push({
|
||||||
|
separator: true,
|
||||||
|
i18n_key: 'setting.appearance.localization.general.language.languages',
|
||||||
|
title: 'Supported Languages'
|
||||||
|
});
|
||||||
|
|
||||||
|
out = out.concat(normal_out);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( joke_out.length ) {
|
||||||
|
out.push({
|
||||||
|
separator: true,
|
||||||
|
i18n_key: 'setting.appearance.localization.general.language.joke',
|
||||||
|
title: 'Joke Languages'
|
||||||
|
});
|
||||||
|
|
||||||
|
out = out.concat(joke_out);
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
onEnable() {
|
||||||
this.settings.addUI('i18n.debug.open', {
|
this.settings.addUI('i18n.debug.open', {
|
||||||
path: 'Debugging > Localization >> Editing',
|
path: 'Debugging > Localization >> Editing',
|
||||||
component: 'i18n-open',
|
component: 'i18n-open',
|
||||||
|
@ -264,71 +330,7 @@ export class TranslationManager extends Module {
|
||||||
this.emit(':update')
|
this.emit(':update')
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
getLocaleOptions(val) {
|
|
||||||
if( val === undefined )
|
|
||||||
val = this.settings.get('i18n.locale');
|
|
||||||
|
|
||||||
const normal_out = [],
|
|
||||||
joke_out = [];
|
|
||||||
|
|
||||||
for(const locale of this.availableLocales) {
|
|
||||||
const data = this.localeData[locale];
|
|
||||||
let title = data?.native_name || data?.name || locale;
|
|
||||||
|
|
||||||
if ( data?.coverage != null && data?.coverage < 100 )
|
|
||||||
title = this.t('i18n.locale-coverage', '{name} ({coverage,number,percent} Complete)', {
|
|
||||||
name: title,
|
|
||||||
coverage: data.coverage / 100
|
|
||||||
});
|
|
||||||
|
|
||||||
const entry = {
|
|
||||||
selected: val === locale,
|
|
||||||
value: locale,
|
|
||||||
title
|
|
||||||
};
|
|
||||||
|
|
||||||
if ( data?.joke )
|
|
||||||
joke_out.push(entry);
|
|
||||||
else
|
|
||||||
normal_out.push(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
normal_out.sort((a, b) => a.title.localeCompare(b.title));
|
|
||||||
joke_out.sort((a, b) => a.title.localeCompare(b.title));
|
|
||||||
|
|
||||||
let out = [{
|
|
||||||
selected: val === -1,
|
|
||||||
value: -1,
|
|
||||||
i18n_key: 'setting.appearance.localization.general.language.twitch',
|
|
||||||
title: "Use Twitch's Language"
|
|
||||||
}];
|
|
||||||
|
|
||||||
if ( normal_out.length ) {
|
|
||||||
out.push({
|
|
||||||
separator: true,
|
|
||||||
i18n_key: 'setting.appearance.localization.general.language.languages',
|
|
||||||
title: 'Supported Languages'
|
|
||||||
});
|
|
||||||
|
|
||||||
out = out.concat(normal_out);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( joke_out.length ) {
|
|
||||||
out.push({
|
|
||||||
separator: true,
|
|
||||||
i18n_key: 'setting.appearance.localization.general.language.joke',
|
|
||||||
title: 'Joke Languages'
|
|
||||||
});
|
|
||||||
|
|
||||||
out = out.concat(joke_out);
|
|
||||||
}
|
|
||||||
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
onEnable() {
|
|
||||||
this.capturing = this.settings.get('i18n.debug.capture');
|
this.capturing = this.settings.get('i18n.debug.capture');
|
||||||
if ( this.capturing )
|
if ( this.capturing )
|
||||||
this.loadStrings();
|
this.loadStrings();
|
||||||
|
|
|
@ -15,6 +15,8 @@ import {TranslationManager} from './i18n';
|
||||||
import Site from './sites/player';
|
import Site from './sites/player';
|
||||||
|
|
||||||
class FFZPlayer extends Module {
|
class FFZPlayer extends Module {
|
||||||
|
static construct_requires = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
const start_time = performance.now(),
|
const start_time = performance.now(),
|
||||||
|
|
12
src/raven.js
12
src/raven.js
|
@ -54,6 +54,8 @@ const ERROR_STRINGS = [
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
export default class RavenLogger extends Module {
|
export default class RavenLogger extends Module {
|
||||||
|
static construct_requires = null;
|
||||||
|
|
||||||
constructor(...args) {
|
constructor(...args) {
|
||||||
super(...args);
|
super(...args);
|
||||||
|
|
||||||
|
@ -62,7 +64,9 @@ export default class RavenLogger extends Module {
|
||||||
// Do these in an event handler because we're initialized before
|
// Do these in an event handler because we're initialized before
|
||||||
// settings are even ready.
|
// settings are even ready.
|
||||||
this.once('settings:enabled', () => {
|
this.once('settings:enabled', () => {
|
||||||
this.settings.add('reports.error.enable', {
|
const settings = this.resolve('settings');
|
||||||
|
|
||||||
|
settings.add('reports.error.enable', {
|
||||||
default: true,
|
default: true,
|
||||||
ui: {
|
ui: {
|
||||||
path: 'Data Management > Reporting >> Error Reports',
|
path: 'Data Management > Reporting >> Error Reports',
|
||||||
|
@ -71,7 +75,7 @@ export default class RavenLogger extends Module {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.settings.add('reports.error.include-user', {
|
settings.add('reports.error.include-user', {
|
||||||
default: false,
|
default: false,
|
||||||
ui: {
|
ui: {
|
||||||
path: 'Data Management > Reporting >> Error Reports',
|
path: 'Data Management > Reporting >> Error Reports',
|
||||||
|
@ -81,7 +85,7 @@ export default class RavenLogger extends Module {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.settings.add('reports.error.include-settings', {
|
settings.add('reports.error.include-settings', {
|
||||||
default: true,
|
default: true,
|
||||||
ui: {
|
ui: {
|
||||||
path: 'Data Management > Reporting >> Error Reports',
|
path: 'Data Management > Reporting >> Error Reports',
|
||||||
|
@ -91,7 +95,7 @@ export default class RavenLogger extends Module {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.settings.addUI('reports.error.example', {
|
settings.addUI('reports.error.example', {
|
||||||
path: 'Data Management > Reporting >> Example Report',
|
path: 'Data Management > Reporting >> Example Report',
|
||||||
component: 'async-text',
|
component: 'async-text',
|
||||||
|
|
||||||
|
|
|
@ -40,6 +40,8 @@ export const NO_SYNC_KEYS = ['session'];
|
||||||
* @extends Module
|
* @extends Module
|
||||||
*/
|
*/
|
||||||
export default class SettingsManager extends Module {
|
export default class SettingsManager extends Module {
|
||||||
|
static construct_requires = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a SettingsManager module.
|
* Create a SettingsManager module.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -23,12 +23,15 @@ export default class PlayerSite extends BaseSite {
|
||||||
constructor(...args) {
|
constructor(...args) {
|
||||||
super(...args);
|
super(...args);
|
||||||
|
|
||||||
|
this.inject('settings');
|
||||||
this.inject('i18n');
|
this.inject('i18n');
|
||||||
this.inject(Fine);
|
this.inject(Fine);
|
||||||
this.inject(Player);
|
this.inject(Player);
|
||||||
this.inject('tooltips', Tooltips);
|
this.inject('tooltips', Tooltips);
|
||||||
this.inject('css_tweaks', CSSTweaks);
|
this.inject('css_tweaks', CSSTweaks);
|
||||||
|
}
|
||||||
|
|
||||||
|
onEnable() {
|
||||||
this.DataSource = this.fine.define(
|
this.DataSource = this.fine.define(
|
||||||
'data-source',
|
'data-source',
|
||||||
n => n.consentMetadata && n.onPlaying && n.props && n.props.data
|
n => n.consentMetadata && n.onPlaying && n.props && n.props.data
|
||||||
|
@ -38,10 +41,6 @@ export default class PlayerSite extends BaseSite {
|
||||||
'player-menu',
|
'player-menu',
|
||||||
n => n.closeSettingsMenu && n.state && n.state.activeMenu && n.getMaxMenuHeight
|
n => n.closeSettingsMenu && n.state && n.state.activeMenu && n.getMaxMenuHeight
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
onEnable() {
|
|
||||||
this.settings = this.resolve('settings');
|
|
||||||
|
|
||||||
this.DataSource.on('mount', this.updateData, this);
|
this.DataSource.on('mount', this.updateData, this);
|
||||||
this.DataSource.on('update', this.updateData, this);
|
this.DataSource.on('update', this.updateData, this);
|
||||||
|
|
|
@ -22,20 +22,6 @@ export default class Metadata extends Module {
|
||||||
|
|
||||||
this.definitions = {};
|
this.definitions = {};
|
||||||
|
|
||||||
this.settings.add('metadata.player-stats', {
|
|
||||||
default: false,
|
|
||||||
changed: () => this.updateMetadata('player-stats')
|
|
||||||
});
|
|
||||||
|
|
||||||
this.settings.add('metadata.stream-delay-warning', {
|
|
||||||
default: 0
|
|
||||||
});
|
|
||||||
|
|
||||||
this.settings.add('metadata.uptime', {
|
|
||||||
default: 1,
|
|
||||||
changed: () => this.updateMetadata('uptime')
|
|
||||||
});
|
|
||||||
|
|
||||||
this.definitions.uptime = {
|
this.definitions.uptime = {
|
||||||
inherit: true,
|
inherit: true,
|
||||||
no_arrow: true,
|
no_arrow: true,
|
||||||
|
@ -353,6 +339,20 @@ export default class Metadata extends Module {
|
||||||
}
|
}
|
||||||
|
|
||||||
onEnable() {
|
onEnable() {
|
||||||
|
this.settings.add('metadata.player-stats', {
|
||||||
|
default: false,
|
||||||
|
changed: () => this.updateMetadata('player-stats')
|
||||||
|
});
|
||||||
|
|
||||||
|
this.settings.add('metadata.stream-delay-warning', {
|
||||||
|
default: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
this.settings.add('metadata.uptime', {
|
||||||
|
default: 1,
|
||||||
|
changed: () => this.updateMetadata('uptime')
|
||||||
|
});
|
||||||
|
|
||||||
const md = this.tooltips.types.metadata = target => {
|
const md = this.tooltips.types.metadata = target => {
|
||||||
let el = target;
|
let el = target;
|
||||||
if ( el._ffz_stat )
|
if ( el._ffz_stat )
|
||||||
|
|
|
@ -23,7 +23,6 @@ export default class Player extends PlayerBase {
|
||||||
'player-source',
|
'player-source',
|
||||||
n => n.setSrc && n.setInitialPlaybackSettings
|
n => n.setSrc && n.setInitialPlaybackSettings
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
wantsMetadata() {
|
wantsMetadata() {
|
||||||
return this.settings.get('player.embed-metadata');
|
return this.settings.get('player.embed-metadata');
|
||||||
|
|
|
@ -27,7 +27,7 @@ export default class VideoChatHook extends Module {
|
||||||
this.inject('site.web_munch');
|
this.inject('site.web_munch');
|
||||||
|
|
||||||
this.inject('chat');
|
this.inject('chat');
|
||||||
this.injectAs('site_chat', 'site.chat');
|
this.inject('site.chat', undefined, false, 'site_chat');
|
||||||
this.inject('site.chat.chat_line.rich_content');
|
this.inject('site.chat.chat_line.rich_content');
|
||||||
|
|
||||||
this.VideoChatController = this.fine.define(
|
this.VideoChatController = this.fine.define(
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
import EventEmitter from 'utilities/events';
|
import EventEmitter from 'utilities/events';
|
||||||
import {has} from 'utilities/object';
|
import {has} from 'utilities/object';
|
||||||
|
import { load } from './font-awesome';
|
||||||
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
@ -22,8 +23,12 @@ export const State = {
|
||||||
DISABLED: 0,
|
DISABLED: 0,
|
||||||
ENABLING: 1,
|
ENABLING: 1,
|
||||||
ENABLED: 2,
|
ENABLED: 2,
|
||||||
DISABLING: 3
|
DISABLING: 3,
|
||||||
}
|
|
||||||
|
UNINJECTED: 0,
|
||||||
|
LOAD_INJECTED: 1,
|
||||||
|
FULL_INJECTED: 2
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export class Module extends EventEmitter {
|
export class Module extends EventEmitter {
|
||||||
|
@ -34,6 +39,9 @@ export class Module extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
super(name, parent);
|
super(name, parent);
|
||||||
|
this.__module_promises = parent ? parent.__module_promises : {};
|
||||||
|
this.__module_dependents = parent ? parent.__module_dependents : {};
|
||||||
|
this.__module_sources = parent ? parent.__module_sources : {};
|
||||||
this.__modules = parent ? parent.__modules : {};
|
this.__modules = parent ? parent.__modules : {};
|
||||||
this.children = {};
|
this.children = {};
|
||||||
|
|
||||||
|
@ -43,12 +51,22 @@ export class Module extends EventEmitter {
|
||||||
if ( this.root === this )
|
if ( this.root === this )
|
||||||
this.__modules[this.__path || ''] = this;
|
this.__modules[this.__path || ''] = this;
|
||||||
|
|
||||||
|
this.__constructed = false;
|
||||||
|
this.__load_injections = {};
|
||||||
|
this.__enable_injections = {};
|
||||||
|
this.__inject_state = State.UNINJECTED;
|
||||||
this.__load_state = this.onLoad ? State.UNLOADED : State.LOADED;
|
this.__load_state = this.onLoad ? State.UNLOADED : State.LOADED;
|
||||||
this.__state = this.onLoad || this.onEnable ?
|
this.__state = this.onLoad || this.onEnable ?
|
||||||
State.DISABLED : State.ENABLED;
|
State.DISABLED : State.ENABLED;
|
||||||
|
|
||||||
|
// Inject any pre-construction injections.
|
||||||
|
const injections = this.__get_construct_requires();
|
||||||
|
if ( injections )
|
||||||
|
for(const [key, path] of Object.entries(injections))
|
||||||
|
this[key] = this.resolve(path, false, false);
|
||||||
|
|
||||||
this.__time('instance');
|
this.__time('instance');
|
||||||
this.emit(':registered');
|
this.emit(':instanced');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -65,7 +83,6 @@ export class Module extends EventEmitter {
|
||||||
get enabled() { return this.__state === State.ENABLED }
|
get enabled() { return this.__state === State.ENABLED }
|
||||||
get enabling() { return this.__state === State.ENABLING }
|
get enabling() { return this.__state === State.ENABLING }
|
||||||
|
|
||||||
|
|
||||||
get log() {
|
get log() {
|
||||||
if ( ! this.__log )
|
if ( ! this.__log )
|
||||||
this.__log = this.parent && this.parent.log.get(this.name);
|
this.__log = this.parent && this.parent.log.get(this.name);
|
||||||
|
@ -112,6 +129,17 @@ export class Module extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
__inject(injections) {
|
||||||
|
for(const [attr, name] of Object.entries(injections)) {
|
||||||
|
const module = this.resolve(name);
|
||||||
|
if ( ! module || !(module instanceof Module) )
|
||||||
|
throw new ModuleError(`unable to inject dependency ${name} for module ${this.name}`);
|
||||||
|
|
||||||
|
this[attr] = module;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
__load(args, initial, chain) {
|
__load(args, initial, chain) {
|
||||||
const path = this.__path || this.name,
|
const path = this.__path || this.name,
|
||||||
state = this.__load_state;
|
state = this.__load_state;
|
||||||
|
@ -142,16 +170,27 @@ export class Module extends EventEmitter {
|
||||||
if ( this.load_requires ) {
|
if ( this.load_requires ) {
|
||||||
const promises = [];
|
const promises = [];
|
||||||
for(const name of this.load_requires) {
|
for(const name of this.load_requires) {
|
||||||
const module = this.resolve(name);
|
// Resolve and instantiate the module.
|
||||||
if ( ! module || !(module instanceof Module) )
|
promises.push(Promise.resolve(this.resolve(name, true)).then(module => {
|
||||||
throw new ModuleError(`cannot find required module ${name} when loading ${path}`);
|
if ( ! module || !(module instanceof Module) )
|
||||||
|
throw new ModuleError(`cannot find required module ${name} when loading ${path}`);
|
||||||
|
|
||||||
promises.push(module.__enable([], initial, Array.from(chain)));
|
return module.__enable([], initial, Array.from(chain));
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( this.__inject_state === State.UNINJECTED ) {
|
||||||
|
if ( this.__load_injections ) {
|
||||||
|
this.__inject(this.__load_injections);
|
||||||
|
this.__load_injections = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.__inject_state = State.LOAD_INJECTED;
|
||||||
|
}
|
||||||
|
|
||||||
if ( this.onLoad ) {
|
if ( this.onLoad ) {
|
||||||
this.__time('load-self');
|
this.__time('load-self');
|
||||||
return this.onLoad(...args);
|
return this.onLoad(...args);
|
||||||
|
@ -163,6 +202,7 @@ export class Module extends EventEmitter {
|
||||||
this.__time('load-end');
|
this.__time('load-end');
|
||||||
this.emit(':loaded', this);
|
this.emit(':loaded', this);
|
||||||
return ret;
|
return ret;
|
||||||
|
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
this.__load_state = State.UNLOADED;
|
this.__load_state = State.UNLOADED;
|
||||||
this.__load_promise = null;
|
this.__load_promise = null;
|
||||||
|
@ -208,6 +248,8 @@ export class Module extends EventEmitter {
|
||||||
if ( this.load_dependents ) {
|
if ( this.load_dependents ) {
|
||||||
const promises = [];
|
const promises = [];
|
||||||
for(const name of this.load_dependents) {
|
for(const name of this.load_dependents) {
|
||||||
|
// All our dependents should be instantiated. An uninstantiated module is not loaded
|
||||||
|
// so we obviously do not need to unload it at this time.
|
||||||
const module = this.resolve(name);
|
const module = this.resolve(name);
|
||||||
if ( ! module )
|
if ( ! module )
|
||||||
//throw new ModuleError(`cannot find depending module ${name} when unloading ${path}`);
|
//throw new ModuleError(`cannot find depending module ${name} when unloading ${path}`);
|
||||||
|
@ -228,6 +270,7 @@ export class Module extends EventEmitter {
|
||||||
this.__time('unload-end');
|
this.__time('unload-end');
|
||||||
this.emit(':unloaded', this);
|
this.emit(':unloaded', this);
|
||||||
return ret;
|
return ret;
|
||||||
|
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
this.__load_state = State.LOADED;
|
this.__load_state = State.LOADED;
|
||||||
this.__load_promise = null;
|
this.__load_promise = null;
|
||||||
|
@ -237,6 +280,44 @@ export class Module extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*generateLoadGraph(chain) {
|
||||||
|
let initial = false;
|
||||||
|
if ( ! chain ) {
|
||||||
|
chain = [];
|
||||||
|
initial = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( chain.includes(this) )
|
||||||
|
return [`${this.name}: cyclic requirement`];
|
||||||
|
|
||||||
|
chain.push(this);
|
||||||
|
|
||||||
|
const out = [];
|
||||||
|
out.push(`${this.name}: ${this.enabled ? 'enabled' : this.enabling ? 'enabling' : this.disabling ? 'disabling' : 'disabled'}`);
|
||||||
|
|
||||||
|
const requires = this.requires;
|
||||||
|
if ( requires )
|
||||||
|
for(const req of requires) {
|
||||||
|
const module = this.resolve(req)
|
||||||
|
let mod_out;
|
||||||
|
if ( ! module )
|
||||||
|
mod_out = [`${req}: uninstantiated`];
|
||||||
|
else if ( ! module.enabled )
|
||||||
|
mod_out = module.generateLoadGraph(Array.from(chain));
|
||||||
|
else
|
||||||
|
continue;
|
||||||
|
|
||||||
|
for(const line of mod_out)
|
||||||
|
out.push(` ${line}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( initial )
|
||||||
|
return out.join('\n');
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}*/
|
||||||
|
|
||||||
|
|
||||||
__enable(args, initial, chain) {
|
__enable(args, initial, chain) {
|
||||||
const path = this.__path || this.name,
|
const path = this.__path || this.name,
|
||||||
state = this.__state;
|
state = this.__state;
|
||||||
|
@ -268,24 +349,42 @@ export class Module extends EventEmitter {
|
||||||
requires = this.requires,
|
requires = this.requires,
|
||||||
load_state = this.__load_state;
|
load_state = this.__load_state;
|
||||||
|
|
||||||
|
// Make sure our module is loaded before enabling it.
|
||||||
if ( load_state === State.UNLOADING )
|
if ( load_state === State.UNLOADING )
|
||||||
// We'd abort for this later to, but kill it now before we start
|
// We'd abort for this later too, but kill it now before we start
|
||||||
// any unnecessary work.
|
// any unnecessary work.
|
||||||
throw new ModuleError(`attempted to load module ${path} while module is being unloaded`);
|
throw new ModuleError(`attempted to load module ${path} while module is being unloaded`);
|
||||||
|
|
||||||
else if ( load_state === State.LOADING || load_state === State.UNLOADED )
|
else if ( load_state === State.LOADING || load_state === State.UNLOADED )
|
||||||
promises.push(this.load());
|
promises.push(this.load());
|
||||||
|
|
||||||
|
// We also want to load all our dependencies.
|
||||||
if ( requires )
|
if ( requires )
|
||||||
for(const name of requires) {
|
for(const name of requires) {
|
||||||
const module = this.resolve(name);
|
promises.push(Promise.resolve(this.resolve(name, true).then(module => {
|
||||||
if ( ! module || !(module instanceof Module) )
|
if ( ! module || !(module instanceof Module) )
|
||||||
throw new ModuleError(`cannot find required module ${name} when enabling ${path}`);
|
throw new ModuleError(`cannot find required module ${name} when enabling ${path}`);
|
||||||
|
|
||||||
promises.push(module.__enable([], initial, Array.from(chain)));
|
return module.__enable([], initial, Array.from(chain));
|
||||||
|
})));
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
|
|
||||||
|
if ( this.__inject_state !== State.FULL_INJECTED ) {
|
||||||
|
if ( this.__load_injections ) {
|
||||||
|
this.__inject(this.__load_injections);
|
||||||
|
this.__load_injections = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( this.__enable_injections ) {
|
||||||
|
this.__inject(this.__enable_injections);
|
||||||
|
this.__enable_injections = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.__inject_state = State.FULL_INJECTED;
|
||||||
|
}
|
||||||
|
|
||||||
if ( this.onEnable ) {
|
if ( this.onEnable ) {
|
||||||
this.__time('enable-self');
|
this.__time('enable-self');
|
||||||
return this.onEnable(...args);
|
return this.onEnable(...args);
|
||||||
|
@ -336,15 +435,18 @@ export class Module extends EventEmitter {
|
||||||
|
|
||||||
this.__time('disable-start');
|
this.__time('disable-start');
|
||||||
this.__state = State.DISABLING;
|
this.__state = State.DISABLING;
|
||||||
|
|
||||||
return this.__state_promise = (async () => {
|
return this.__state_promise = (async () => {
|
||||||
if ( this.__load_state !== State.LOADED )
|
if ( this.__load_state !== State.LOADED )
|
||||||
// We'd abort for this later to, but kill it now before we start
|
// We'd abort for this later too, but kill it now before we start
|
||||||
// any unnecessary work.
|
// any unnecessary work.
|
||||||
throw new ModuleError(`attempted to disable module ${path} but module is unloaded -- weird state`);
|
throw new ModuleError(`attempted to disable module ${path} but module is unloaded -- weird state`);
|
||||||
|
|
||||||
if ( this.dependents ) {
|
if ( this.dependents ) {
|
||||||
const promises = [];
|
const promises = [];
|
||||||
for(const name of this.dependents) {
|
for(const name of this.dependents) {
|
||||||
|
// All our dependents should be instantiated. An uninstantiated module is not enabled
|
||||||
|
// so we obviously do not need to disable it at this time.
|
||||||
const module = this.resolve(name);
|
const module = this.resolve(name);
|
||||||
if ( ! module )
|
if ( ! module )
|
||||||
// Assume a non-existent module isn't enabled.
|
// Assume a non-existent module isn't enabled.
|
||||||
|
@ -413,38 +515,210 @@ export class Module extends EventEmitter {
|
||||||
// Child Control
|
// Child Control
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
loadModules(...names) {
|
// These aren't being used anywhere.
|
||||||
return Promise.all(names.map(n => this.resolve(n).load()))
|
/*loadModules(...names) {
|
||||||
|
return Promise.all(names.map(n => this.resolve(n, true).then(module => module.load())));
|
||||||
}
|
}
|
||||||
|
|
||||||
unloadModules(...names) {
|
unloadModules(...names) {
|
||||||
return Promise.all(names.map(n => this.resolve(n).unload()))
|
return Promise.all(names.map(n => this.resolve(n)?.unload?.()));
|
||||||
}
|
}
|
||||||
|
|
||||||
enableModules(...names) {
|
enableModules(...names) {
|
||||||
return Promise.all(names.map(n => this.resolve(n).enable()))
|
return Promise.all(names.map(n => this.resolve(n, true).then(module => module.enable())));
|
||||||
}
|
}
|
||||||
|
|
||||||
disableModules(...names) {
|
disableModules(...names) {
|
||||||
return Promise.all(names.map(n => this.resolve(n).disable()))
|
return Promise.all(names.map(n => this.resolve(n)?.disable?.()));
|
||||||
}
|
}*/
|
||||||
|
|
||||||
|
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
// Module Management
|
// Module Management
|
||||||
// ========================================================================
|
// ========================================================================
|
||||||
|
|
||||||
resolve(name) {
|
/**
|
||||||
if ( name instanceof Module )
|
* Resolve a module. This will only return a module that has already been
|
||||||
return name;
|
* constructed, by default. If `construct` is true, a Promise will be
|
||||||
|
* returned and a module instance will be constructed.
|
||||||
|
*
|
||||||
|
* @param {String} name The name of the module to resolve.
|
||||||
|
* @param {Boolean} [construct=false] Whether or not a module
|
||||||
|
* should be constructed if it has not been already. When this is true,
|
||||||
|
* this method will always return a promise. When this is false, the
|
||||||
|
* method will never return a promise.
|
||||||
|
* @param {Boolean} [allow_missing=true] When this is false, an exception
|
||||||
|
* will be thrown for a missing module, rather than returning null.
|
||||||
|
* @returns {Module|Promise} A module, or a Promise that will return a
|
||||||
|
* module, depending on the value of `construct`.
|
||||||
|
*/
|
||||||
|
resolve(name, construct = false, allow_missing = true) {
|
||||||
|
const path = this.abs_path(name),
|
||||||
|
source = this.__module_sources[path],
|
||||||
|
module = this.__modules[path];
|
||||||
|
|
||||||
return this.__modules[this.abs_path(name)];
|
if ( ! construct ) {
|
||||||
|
if ( ! module && ! allow_missing ) {
|
||||||
|
if ( source )
|
||||||
|
throw new ModuleError(`instance for module "${path}" has not been constructed`);
|
||||||
|
else
|
||||||
|
throw new ModuleError(`unknown module "${path}"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return module || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have the module already, but wrap it in a promise for safety.
|
||||||
|
if ( module )
|
||||||
|
return Promise.resolve(module);
|
||||||
|
|
||||||
|
// We do not have the module. Do we know how to load it?
|
||||||
|
// If not, then return null or an exception.
|
||||||
|
if ( ! source ) {
|
||||||
|
if ( allow_missing )
|
||||||
|
return Promise.resolve(null);
|
||||||
|
|
||||||
|
return Promise.reject(new ModuleError(`unknown module "${path}"`));
|
||||||
|
}
|
||||||
|
|
||||||
|
// To instantiate a module, we need the name and the parent module.
|
||||||
|
const idx = path.lastIndexOf('.'),
|
||||||
|
nm = path.slice(idx + 1);
|
||||||
|
let p_path = null;
|
||||||
|
if ( idx !== -1 )
|
||||||
|
p_path = path.slice(0, idx);
|
||||||
|
|
||||||
|
console.log('resolve', name, path, nm, p_path);
|
||||||
|
|
||||||
|
// Is there an existing promise for constructing this module?
|
||||||
|
if ( this.__module_promises[path] )
|
||||||
|
return new Promise((s,f) => this.__module_promises[path].push([s,f]));
|
||||||
|
|
||||||
|
// We're still here, so load and instantiate the module, then
|
||||||
|
// return it.
|
||||||
|
return new Promise((s,f) => {
|
||||||
|
const proms = this.__module_promises[path] = [[s,f]];
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
let parent;
|
||||||
|
if ( p_path === this.__path )
|
||||||
|
parent = this;
|
||||||
|
else if ( p_path )
|
||||||
|
parent = await this.resolve(p_path, true, false);
|
||||||
|
else
|
||||||
|
parent = this.root;
|
||||||
|
|
||||||
|
const loader = await source;
|
||||||
|
if ( ! loader )
|
||||||
|
throw new ModuleError(`invalid loader for module "${path}`);
|
||||||
|
|
||||||
|
// Do we have pre-construct requirements?
|
||||||
|
const pre_requires = name === 'settings' ? null : ['settings'];
|
||||||
|
|
||||||
|
if ( Array.isArray(pre_requires) ) {
|
||||||
|
const promises = [];
|
||||||
|
for(const dep of pre_requires)
|
||||||
|
promises.push(Promise.resolve(this.resolve(dep, true)).then(module => {
|
||||||
|
if ( ! module || !(module instanceof Module) )
|
||||||
|
throw new ModuleError(`cannot find required module ${dep} when loading ${path}`);
|
||||||
|
|
||||||
|
return module.__enable([], path, []);
|
||||||
|
}));
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
let module;
|
||||||
|
if ( loader.prototype instanceof Module )
|
||||||
|
module = new loader(nm, parent);
|
||||||
|
else
|
||||||
|
module = loader(nm, parent);
|
||||||
|
|
||||||
|
if ( ! module || !(module instanceof Module))
|
||||||
|
throw new ModuleError(`invalid return value from module constructor for module "${path}"`);
|
||||||
|
|
||||||
|
module.__constructed = true;
|
||||||
|
this.__modules[path] = module;
|
||||||
|
|
||||||
|
const deps = this.__module_dependents[path];
|
||||||
|
this.__module_dependents[path] = null;
|
||||||
|
|
||||||
|
// Copy over the dependencies.
|
||||||
|
if ( deps ) {
|
||||||
|
if ( deps.load )
|
||||||
|
module.load_dependents = module.load_dependents ? [...module.load_dependents, ...deps.load] : deps.load;
|
||||||
|
if ( deps.enable )
|
||||||
|
module.dependents = module.dependents ? [...module.dependents, ...deps.enable] : deps.enable;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject any requirements.
|
||||||
|
this.__reflectDependencies();
|
||||||
|
return module;
|
||||||
|
|
||||||
|
})().then(result => {
|
||||||
|
this.__module_promises[path] = null;
|
||||||
|
for(const pair of proms)
|
||||||
|
pair[0](result);
|
||||||
|
}).catch(err => {
|
||||||
|
this.__module_promises[path] = null;
|
||||||
|
for(const pair of proms)
|
||||||
|
pair[1](err);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
hasModule(name) {
|
/**
|
||||||
const module = this.__modules[this.abs_path(name)];
|
* Reflect the dependencies of this module, registering it with
|
||||||
return module instanceof Module;
|
* all modules it depends on as a dependent so that those modules
|
||||||
|
* know to disable or unload this module when appropriate.
|
||||||
|
*
|
||||||
|
* @param {String[]} [load=null] An optional array of specific load dependencies to reflect.
|
||||||
|
* @param {String[]} [enable=null] An optional array of specific dependencies to reflect.
|
||||||
|
* @returns {undefined} Nothing
|
||||||
|
*/
|
||||||
|
__reflectDependencies(load = null, enable = null) {
|
||||||
|
if ( load == null )
|
||||||
|
load = this.__get_load_requires();
|
||||||
|
if ( enable == null )
|
||||||
|
enable = this.__get_requires();
|
||||||
|
|
||||||
|
if ( load && ! Array.isArray(load) )
|
||||||
|
load = [load];
|
||||||
|
if ( enable && ! Array.isArray(enable) )
|
||||||
|
enable = [enable];
|
||||||
|
|
||||||
|
const local = this.__path || 'core';
|
||||||
|
|
||||||
|
if ( load && load.length )
|
||||||
|
for(const path of load) {
|
||||||
|
const module = this.__modules[path];
|
||||||
|
if ( module ) {
|
||||||
|
const dependents = module.load_dependents = module.load_dependents || [];
|
||||||
|
if ( ! dependents.includes(local) )
|
||||||
|
dependents.push(local);
|
||||||
|
} else {
|
||||||
|
const dependents = this.__module_dependents[path] = this.__module_dependents[path] || {},
|
||||||
|
set = dependents.load = dependents.load || [];
|
||||||
|
if ( ! set.includes(local) )
|
||||||
|
set.push(local);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( enable && enable.length )
|
||||||
|
for(const path of enable) {
|
||||||
|
const module = this.__modules[path];
|
||||||
|
if ( module ) {
|
||||||
|
const dependents = module.dependents = module.dependents || [];
|
||||||
|
if ( ! dependents.includes(local) )
|
||||||
|
dependents.push(local);
|
||||||
|
} else {
|
||||||
|
const dependents = this.__module_dependents[path] = this.__module_dependents[path] || {},
|
||||||
|
set = dependents.enable = dependents.enable || [];
|
||||||
|
if ( ! set.includes(local) )
|
||||||
|
set.push(local);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -464,182 +738,146 @@ export class Module extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
inject(name, module, require = true) {
|
__get_construct_requires() {
|
||||||
if ( name instanceof Module || name.prototype instanceof Module ) {
|
let out;
|
||||||
require = module != null ? module : true;
|
if ( has(this, 'construct_requires') )
|
||||||
module = name;
|
out = this.construct_requires;
|
||||||
name = null;
|
else if ( has(this.constructor, 'construct_requires') )
|
||||||
|
out = this.constructor.construct_requires;
|
||||||
|
else
|
||||||
|
out = ['settings', 'i18n', 'experiments'];
|
||||||
|
|
||||||
|
if ( ! out || ! Array.isArray(out) )
|
||||||
|
return out;
|
||||||
|
|
||||||
|
const obj = {};
|
||||||
|
for(const path of out) {
|
||||||
|
const full = this.abs_path(path),
|
||||||
|
idx = full.lastIndexOf('.'),
|
||||||
|
name = full.slice(idx + 1);
|
||||||
|
|
||||||
|
obj[name] = full;
|
||||||
}
|
}
|
||||||
|
|
||||||
const requires = this.requires = this.__get_requires() || [];
|
return obj;
|
||||||
|
|
||||||
if ( module instanceof Module ) {
|
|
||||||
// Existing Instance
|
|
||||||
if ( ! name )
|
|
||||||
name = module.constructor.name.toSnakeCase();
|
|
||||||
|
|
||||||
} else if ( module && module.prototype instanceof Module ) {
|
|
||||||
// New Instance
|
|
||||||
if ( ! name )
|
|
||||||
name = module.name.toSnakeCase();
|
|
||||||
|
|
||||||
module = this.register(name, module);
|
|
||||||
|
|
||||||
} else if ( name ) {
|
|
||||||
// Just a Name
|
|
||||||
const full_name = name;
|
|
||||||
name = name.replace(/^(?:[^.]*\.)+/, '');
|
|
||||||
module = this.resolve(full_name);
|
|
||||||
|
|
||||||
// Allow injecting a module that doesn't exist yet?
|
|
||||||
|
|
||||||
if ( ! module || !(module instanceof Module) ) {
|
|
||||||
if ( module )
|
|
||||||
module[2].push([this.__path, name]);
|
|
||||||
else
|
|
||||||
this.__modules[this.abs_path(full_name)] = [[], [], [[this.__path, name]]]
|
|
||||||
|
|
||||||
requires.push(this.abs_path(full_name));
|
|
||||||
|
|
||||||
return this[name] = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
} else
|
|
||||||
throw new TypeError(`must provide a valid module name or class`);
|
|
||||||
|
|
||||||
if ( ! module )
|
|
||||||
throw new Error(`cannot find module ${name} or no module provided`);
|
|
||||||
|
|
||||||
if ( require )
|
|
||||||
requires.push(module.abs_path('.'));
|
|
||||||
|
|
||||||
if ( this.enabled && ! module.enabled )
|
|
||||||
module.enable();
|
|
||||||
|
|
||||||
return this[name] = module;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
injectAs(variable, name, module, require = true) {
|
/**
|
||||||
if ( name instanceof Module || name.prototype instanceof Module ) {
|
* Inject a dependency into this module. Dependencies are added as
|
||||||
require = module != null ? module : true;
|
* requirements, and are saved as variables with the module's name
|
||||||
module = name;
|
* within this module for easy access. Injecting the module `settings`
|
||||||
name = null;
|
* for example allows access to `this.settings` to use the settings
|
||||||
|
* module.
|
||||||
|
*
|
||||||
|
* Please note that injected dependencies are NOT available until
|
||||||
|
* the module is being enabled in the case of normal dependencies,
|
||||||
|
* or until the module is being loaded in the case of load
|
||||||
|
* dependencies.
|
||||||
|
*
|
||||||
|
* **Note:** Rather than providing a name, you can provide only
|
||||||
|
* a Module class or instance and the name will be determined
|
||||||
|
* based on the class name of the module. When doing so, you are
|
||||||
|
* not allowed to provide a Promise or other type of function
|
||||||
|
* loader.
|
||||||
|
*
|
||||||
|
* @param {String} name The name of the module to inject.
|
||||||
|
* @param {Class|Function|Promise} [loader] The loader that will
|
||||||
|
* provide the module we're injecting. This will be used to
|
||||||
|
* construct the module on demand .
|
||||||
|
* @param {Boolean} [load=false] If this is true, the injected
|
||||||
|
* dependency will be treated as a load dependency rather and
|
||||||
|
* injected prior to this module being loaded.
|
||||||
|
* @param {String} [key=null] An optional attribute name for
|
||||||
|
* injecting this dependency. If not provided, the name will be
|
||||||
|
* used as the attribute name.
|
||||||
|
* @returns {undefined} Nothing
|
||||||
|
*/
|
||||||
|
inject(name, loader, load = false, key = null) {
|
||||||
|
if ( this.__constructed )
|
||||||
|
throw new ModuleError(`Unable to use inject() outside constructor`);
|
||||||
|
|
||||||
|
// Did we get a name?
|
||||||
|
if ( typeof name !== 'string' ) {
|
||||||
|
// We didn't. Did we get a Module?
|
||||||
|
if ( name instanceof Module || name.prototype instanceof Module ) {
|
||||||
|
key = load;
|
||||||
|
load = loader;
|
||||||
|
loader = name;
|
||||||
|
name = null;
|
||||||
|
} else
|
||||||
|
throw new Error(`invalid type for name`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const requires = this.requires = this.__get_requires() || [];
|
// If we have a loader, go ahead and register it. This will also give us
|
||||||
|
// a name if we don't have one.
|
||||||
|
if ( loader )
|
||||||
|
name = this.register(name, loader);
|
||||||
|
|
||||||
if ( module instanceof Module ) {
|
if ( ! key ) {
|
||||||
// Existing Instance
|
const idx = name.lastIndexOf('.');
|
||||||
if ( ! name )
|
key = (idx === -1 ? name : name.slice(idx + 1)).toSnakeCase();
|
||||||
name = module.constructor.name.toSnakeCase();
|
}
|
||||||
|
|
||||||
} else if ( module && module.prototype instanceof Module ) {
|
const path = this.abs_path(name);
|
||||||
// New Instance
|
|
||||||
if ( ! name )
|
|
||||||
name = module.name.toSnakeCase();
|
|
||||||
|
|
||||||
module = this.register(name, module);
|
// Save this dependency, and also save it on the target module.
|
||||||
|
if ( load ) {
|
||||||
|
const requires = this.load_requires = this.__get_load_requires() || [];
|
||||||
|
if ( ! requires.includes(path) )
|
||||||
|
requires.push(path);
|
||||||
|
|
||||||
} else if ( name ) {
|
this.__reflectDependencies(path, false);
|
||||||
// Just a Name
|
this.__load_injections[key] = path;
|
||||||
const full_name = name;
|
|
||||||
name = name.replace(/^(?:[^.]*\.)+/, '');
|
|
||||||
module = this.resolve(full_name);
|
|
||||||
|
|
||||||
// Allow injecting a module that doesn't exist yet?
|
} else {
|
||||||
|
const requires = this.requires = this.__get_requires() || [];
|
||||||
|
if ( ! requires.includes(path) )
|
||||||
|
requires.push(path);
|
||||||
|
|
||||||
if ( ! module || !(module instanceof Module) ) {
|
this.__reflectDependencies(false, path);
|
||||||
if ( module )
|
this.__enable_injections[key] = path;
|
||||||
module[2].push([this.__path, variable]);
|
}
|
||||||
else
|
|
||||||
this.__modules[this.abs_path(full_name)] = [[], [], [[this.__path, variable]]]
|
|
||||||
|
|
||||||
requires.push(this.abs_path(full_name));
|
|
||||||
|
|
||||||
return this[variable] = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
} else
|
|
||||||
throw new TypeError(`must provide a valid module name or class`);
|
|
||||||
|
|
||||||
if ( ! module )
|
|
||||||
throw new Error(`cannot find module ${name} or no module provided`);
|
|
||||||
|
|
||||||
if ( require )
|
|
||||||
requires.push(module.abs_path('.'));
|
|
||||||
|
|
||||||
if ( this.enabled && ! module.enabled )
|
|
||||||
module.enable();
|
|
||||||
|
|
||||||
return this[variable] = module;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
register(name, module, inject_reference) {
|
/**
|
||||||
if ( name.prototype instanceof Module ) {
|
* Register a module into the module tree. By default, this does very
|
||||||
inject_reference = module;
|
* little. When providing a Module class, you can omit the name
|
||||||
module = name;
|
* argument. In that case, a name will be inferred from the class name.
|
||||||
name = module.name.toSnakeCase();
|
*
|
||||||
|
* When supplying a function or Promise for asynchronous loading, a
|
||||||
|
* name is required.
|
||||||
|
*
|
||||||
|
* The name is always treated as being relative to the current module.
|
||||||
|
* This is done by prefixing the name with a `.` character.
|
||||||
|
*
|
||||||
|
* @param {String} [name] The name of the Module being registered.
|
||||||
|
* @param {Class|Function|Promise} loader A Module class, or a function or
|
||||||
|
* promise that will eventually return a Module class.
|
||||||
|
* @returns {String} The name of the Module.
|
||||||
|
*/
|
||||||
|
register(name, loader) {
|
||||||
|
if ( name && name.prototype instanceof Module ) {
|
||||||
|
loader = name;
|
||||||
|
name = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const path = this.abs_path(`.${name}`),
|
if ( ! name && loader && loader.prototype instanceof Module )
|
||||||
proto = module.prototype,
|
name = loader.name.toSnakeCase();
|
||||||
old_val = this.__modules[path];
|
|
||||||
|
|
||||||
if ( !(proto instanceof Module) )
|
if ( ! name || typeof name !== 'string' )
|
||||||
throw new TypeError(`Module ${name} is not subclass of Module.`);
|
throw new TypeError('Invalid name');
|
||||||
|
|
||||||
if ( old_val instanceof Module )
|
// Make sure the name is relative.
|
||||||
|
name = `.${name}`;
|
||||||
|
|
||||||
|
const path = this.abs_path(name);
|
||||||
|
if ( this.__modules[path] || this.__module_sources[path] )
|
||||||
throw new ModuleError(`Name Collision for Module ${path}`);
|
throw new ModuleError(`Name Collision for Module ${path}`);
|
||||||
|
|
||||||
const dependents = old_val || [[], [], []],
|
this.__module_sources[path] = loader;
|
||||||
inst = this.__modules[path] = new module(name, this),
|
return name;
|
||||||
requires = inst.requires = inst.__get_requires() || [],
|
|
||||||
load_requires = inst.load_requires = inst.__get_load_requires() || [];
|
|
||||||
|
|
||||||
inst.dependents = dependents[0];
|
|
||||||
inst.load_dependents = dependents[1];
|
|
||||||
|
|
||||||
if ( inst instanceof SiteModule && ! requires.includes('site') )
|
|
||||||
requires.push('site');
|
|
||||||
|
|
||||||
for(const req_name of requires) {
|
|
||||||
const req_path = inst.abs_path(req_name),
|
|
||||||
req_mod = this.__modules[req_path];
|
|
||||||
|
|
||||||
if ( ! req_mod )
|
|
||||||
this.__modules[req_path] = [[path],[],[]];
|
|
||||||
else if ( Array.isArray(req_mod) )
|
|
||||||
req_mod[0].push(path);
|
|
||||||
else
|
|
||||||
req_mod.dependents.push(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
for(const req_name of load_requires) {
|
|
||||||
const req_path = inst.abs_path(req_name),
|
|
||||||
req_mod = this.__modules[req_path];
|
|
||||||
|
|
||||||
if ( ! req_mod )
|
|
||||||
this.__modules[req_path] = [[], [path], []];
|
|
||||||
else if ( Array.isArray(req_mod) )
|
|
||||||
req_mod[1].push(path);
|
|
||||||
else
|
|
||||||
req_mod.load_dependents.push(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
for(const [in_path, in_name] of dependents[2]) {
|
|
||||||
const in_mod = this.resolve(in_path);
|
|
||||||
if ( in_mod )
|
|
||||||
in_mod[in_name] = inst;
|
|
||||||
else
|
|
||||||
this.log.warn(`Unable to find module "${in_path}" that wanted "${in_name}".`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( inject_reference )
|
|
||||||
this[name] = inst;
|
|
||||||
|
|
||||||
return inst;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -695,7 +933,7 @@ export class ModuleError extends Error { }
|
||||||
|
|
||||||
export class CyclicDependencyError extends ModuleError {
|
export class CyclicDependencyError extends ModuleError {
|
||||||
constructor(message, modules) {
|
constructor(message, modules) {
|
||||||
super(`${message} (${modules.map(x => x.path).join(' => ')})`);
|
super(`${message} ${modules ? `(${modules.map(x => x.path).join(' => ')})` : ''}`);
|
||||||
this.modules = modules;
|
this.modules = modules;
|
||||||
}
|
}
|
||||||
}
|
}
|
Loading…
Add table
Add a link
Reference in a new issue