feat: export registry (#3842)

* feat: registry first iteration

Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>

* feat: improve webpack auto export loader

Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>

* chore: remove `compat` API

Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>

* chore: cleanup

Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>

---------

Signed-off-by: Sami Mazouz <sychocouldy@gmail.com>
This commit is contained in:
Sami Mazouz 2023-06-29 18:57:53 +01:00 committed by GitHub
parent cf70865aa6
commit 016503d8c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
74 changed files with 2206 additions and 1076 deletions

View File

@ -15,7 +15,7 @@
"declarationDir": "./dist-typings",
"paths": {
"flarum/*": ["../../../framework/core/js/dist-typings/*"],
"flarum/flags/*": ["../../flags/js/dist-typings/*"]
"ext:flarum/flags/*": ["../../flags/js/dist-typings/*"]
}
}
}

View File

@ -1,19 +0,0 @@
import addFlagsToPosts from './addFlagsToPosts';
import addFlagControl from './addFlagControl';
import addFlagsDropdown from './addFlagsDropdown';
import Flag from './models/Flag';
import FlagList from './components/FlagList';
import FlagPostModal from './components/FlagPostModal';
import FlagsPage from './components/FlagsPage';
import FlagsDropdown from './components/FlagsDropdown';
export default {
'flags/addFlagsToPosts': addFlagsToPosts,
'flags/addFlagControl': addFlagControl,
'flags/addFlagsDropdown': addFlagsDropdown,
'flags/models/Flag': Flag,
'flags/components/FlagList': FlagList,
'flags/components/FlagPostModal': FlagPostModal,
'flags/components/FlagsPage': FlagsPage,
'flags/components/FlagsDropdown': FlagsDropdown,
};

View File

@ -1,5 +1,5 @@
import app from 'flarum/forum/app';
import Page from 'flarum/components/Page';
import Page from 'flarum/common/components/Page';
import FlagList from './FlagList';

View File

@ -0,0 +1,10 @@
import './addFlagsToPosts';
import './addFlagControl';
import './addFlagsDropdown';
import './models/Flag';
import './components/FlagList';
import './components/FlagPostModal';
import './components/FlagsPage';
import './components/FlagsDropdown';

View File

@ -15,8 +15,4 @@ app.initializers.add('flarum-flags', () => {
addFlagsToPosts();
});
// Expose compat API
import flagsCompat from './compat';
import { compat } from '@flarum/core/forum';
Object.assign(compat, flagsCompat);
import './forum';

View File

@ -9,8 +9,7 @@
// This will output typings to `dist-typings`
"declarationDir": "./dist-typings",
"paths": {
"flarum/*": ["../../../framework/core/js/dist-typings/*"],
"@flarum/core/*": ["../../../framework/core/js/dist-typings/*"]
"flarum/*": ["../../../framework/core/js/dist-typings/*"]
}
}
}

View File

@ -9,12 +9,7 @@
// This will output typings to `dist-typings`
"declarationDir": "./dist-typings",
"paths": {
"flarum/*": ["../../../framework/core/js/dist-typings/*"],
// TODO: remove after export registry system implemented
// Without this, the old-style `@flarum/core` import is resolved to
// source code in flarum/core instead of the dist typings.
// This causes an inaccurate "duplicate export" error.
"@flarum/core/*": ["../../../framework/core/js/dist-typings/*"],
"flarum/*": ["../../../framework/core/js/dist-typings/*"]
}
}
}

View File

@ -9,12 +9,7 @@
// This will output typings to `dist-typings`
"declarationDir": "./dist-typings",
"paths": {
"flarum/*": ["../../../framework/core/js/dist-typings/*"],
// TODO: remove after export registry system implemented
// Without this, the old-style `@flarum/core` import is resolved to
// source code in flarum/core instead of the dist typings.
// This causes an inaccurate "duplicate export" error.
"@flarum/core/*": ["../../../framework/core/js/dist-typings/*"],
"flarum/*": ["../../../framework/core/js/dist-typings/*"]
}
}
}

View File

@ -1,31 +0,0 @@
import GroupMentionedNotification from './components/GroupMentionedNotification';
import MentionsUserPage from './components/MentionsUserPage';
import PostMentionedNotification from './components/PostMentionedNotification';
import UserMentionedNotification from './components/UserMentionedNotification';
import AutocompleteDropdown from './fragments/AutocompleteDropdown';
import PostQuoteButton from './fragments/PostQuoteButton';
import getCleanDisplayName from './utils/getCleanDisplayName';
import getMentionText from './utils/getMentionText';
import * as reply from './utils/reply';
import selectedText from './utils/selectedText';
import * as textFormatter from './utils/textFormatter';
import MentionableModel from './mentionables/MentionableModel';
import MentionFormat from './mentionables/formats/MentionFormat';
import Mentionables from './extenders/Mentionables';
export default {
'mentions/components/MentionsUserPage': MentionsUserPage,
'mentions/components/PostMentionedNotification': PostMentionedNotification,
'mentions/components/UserMentionedNotification': UserMentionedNotification,
'mentions/components/GroupMentionedNotification': GroupMentionedNotification,
'mentions/fragments/AutocompleteDropdown': AutocompleteDropdown,
'mentions/fragments/PostQuoteButton': PostQuoteButton,
'mentions/utils/getCleanDisplayName': getCleanDisplayName,
'mentions/utils/getMentionText': getMentionText,
'mentions/utils/reply': reply,
'mentions/utils/selectedText': selectedText,
'mentions/utils/textFormatter': textFormatter,
'mentions/mentionables/MentionableModel': MentionableModel,
'mentions/mentionables/formats/MentionFormat': MentionFormat,
'mentions/extenders/Mentionables': Mentionables,
};

View File

@ -0,0 +1,14 @@
import './components/GroupMentionedNotification';
import './components/MentionsUserPage';
import './components/PostMentionedNotification';
import './components/UserMentionedNotification';
import './fragments/AutocompleteDropdown';
import './fragments/PostQuoteButton';
import './utils/getCleanDisplayName';
import './utils/getMentionText';
import './utils/reply';
import './utils/selectedText';
import './utils/textFormatter';
import './mentionables/MentionableModel';
import './mentionables/formats/MentionFormat';
import './extenders/Mentionables';

View File

@ -15,8 +15,6 @@ import UserMentionedNotification from './components/UserMentionedNotification';
import GroupMentionedNotification from './components/GroupMentionedNotification';
import UserPage from 'flarum/forum/components/UserPage';
import LinkButton from 'flarum/common/components/LinkButton';
import User from 'flarum/common/models/User';
import Model from 'flarum/common/Model';
export { default as extend } from './extend';
@ -90,8 +88,4 @@ app.initializers.add('flarum-mentions', function () {
export * from './utils/textFormatter';
// Expose compat API
import mentionsCompat from './compat';
import { compat } from '@flarum/core/forum';
Object.assign(compat, mentionsCompat);
import './forum';

View File

@ -1,7 +1,7 @@
import app from 'flarum/forum/app';
import Badge from 'flarum/common/components/Badge';
import highlight from 'flarum/common/helpers/highlight';
import type Tag from 'flarum/tags/common/models/Tag';
import type Tag from 'ext:flarum/tags/common/models/Tag';
import type Mithril from 'mithril';
import MentionableModel from './MentionableModel';
import type HashMentionFormat from './formats/HashMentionFormat';

View File

@ -10,12 +10,7 @@
"declarationDir": "./dist-typings",
"paths": {
"flarum/*": ["../../../framework/core/js/dist-typings/*"],
"flarum/tags/*": ["../../tags/js/dist-typings/*"],
// TODO: remove after export registry system implemented
// Without this, the old-style `@flarum/core` import is resolved to
// source code in flarum/core instead of the dist typings.
// This causes an inaccurate "duplicate export" error.
"@flarum/core/*": ["../../../framework/core/js/dist-typings/*"],
"ext:flarum/tags/*": ["../../tags/js/dist-typings/*"]
}
}
}

View File

@ -9,12 +9,7 @@
// This will output typings to `dist-typings`
"declarationDir": "./dist-typings",
"paths": {
"flarum/*": ["../../../framework/core/js/dist-typings/*"],
// TODO: remove after export registry system implemented
// Without this, the old-style `@flarum/core` import is resolved to
// source code in flarum/core instead of the dist typings.
// This causes an inaccurate "duplicate export" error.
"@flarum/core/*": ["../../../framework/core/js/dist-typings/*"],
"flarum/*": ["../../../framework/core/js/dist-typings/*"]
}
}
}

View File

@ -7,7 +7,7 @@ import IndexPage from 'flarum/forum/components/IndexPage';
import Button from 'flarum/common/components/Button';
import ItemList from 'flarum/common/utils/ItemList';
import type { Children } from 'mithril';
import type Tag from 'flarum/tags/common/models/Tag';
import type Tag from 'ext:flarum/tags/common/models/Tag';
export type PusherBinding = {
channels: {

View File

@ -10,7 +10,7 @@
"declarationDir": "./dist-typings",
"paths": {
"flarum/*": ["../../../framework/core/js/dist-typings/*"],
"flarum/tags/*": ["../../tags/js/dist-typings/*"]
"ext:flarum/tags/*": ["../../tags/js/dist-typings/*"]
}
}
}

View File

@ -9,12 +9,7 @@
// This will output typings to `dist-typings`
"declarationDir": "./dist-typings",
"paths": {
"flarum/*": ["../../../framework/core/js/dist-typings/*"],
// TODO: remove after export registry system implemented
// Without this, the old-style `@flarum/core` import is resolved to
// source code in flarum/core instead of the dist typings.
// This causes an inaccurate "duplicate export" error.
"@flarum/core/*": ["../../../framework/core/js/dist-typings/*"],
"flarum/*": ["../../../framework/core/js/dist-typings/*"]
}
}
}

View File

@ -10,8 +10,7 @@
"declarationDir": "./dist-typings",
"baseUrl": ".",
"paths": {
"flarum/*": ["../vendor/flarum/core/js/dist-typings/*"],
"@flarum/core/*": ["../vendor/flarum/core/js/dist-typings/*"]
"flarum/*": ["../vendor/flarum/core/js/dist-typings/*"]
}
}
}

View File

@ -1,4 +1,4 @@
import app from 'flarum/app';
import app from 'flarum/admin/app';
app.initializers.add('flarum-suspend', () => {
app.extensionData.for('flarum-suspend').registerPermission(

View File

@ -1,13 +0,0 @@
import SuspendUserModal from './components/SuspendUserModal';
import SuspensionInfoModal from './components/SuspensionInfoModal';
import UserSuspendedNotification from './components/UserSuspendedNotification';
import UserUnsuspendedNotification from './components/UserUnsuspendedNotification';
import checkForSuspension from './checkForSuspension';
export default {
'suspend/components/suspendUserModal': SuspendUserModal,
'suspend/components/suspensionInfoModal': SuspensionInfoModal,
'suspend/components/UserSuspendedNotification': UserSuspendedNotification,
'suspend/components/UserUnsuspendedNotification': UserUnsuspendedNotification,
'suspend/checkForSuspension': checkForSuspension,
};

View File

@ -1,10 +1,10 @@
import app from 'flarum/forum/app';
import Modal from 'flarum/components/Modal';
import Button from 'flarum/components/Button';
import Stream from 'flarum/utils/Stream';
import withAttr from 'flarum/utils/withAttr';
import Modal from 'flarum/common/components/Modal';
import Button from 'flarum/common/components/Button';
import Stream from 'flarum/common/utils/Stream';
import withAttr from 'flarum/common/utils/withAttr';
import ItemList from 'flarum/common/utils/ItemList';
import { getPermanentSuspensionDate } from '../helpers/suspensionHelper';
export default class SuspendUserModal extends Modal {

View File

@ -1,5 +1,6 @@
import app from 'flarum/forum/app';
import Notification from 'flarum/components/Notification';
import Notification from 'flarum/forum/components/Notification';
import { isPermanentSuspensionDate } from '../helpers/suspensionHelper';
export default class UserSuspendedNotification extends Notification {

View File

@ -1,5 +1,5 @@
import app from 'flarum/forum/app';
import Notification from 'flarum/components/Notification';
import Notification from 'flarum/forum/components/Notification';
export default class UserUnsuspendedNotification extends Notification {
icon() {

View File

@ -0,0 +1,6 @@
import './components/SuspendUserModal';
import './components/SuspensionInfoModal';
import './components/UserSuspendedNotification';
import './components/UserUnsuspendedNotification';
import './checkForSuspension';

View File

@ -1,9 +1,9 @@
import { extend } from 'flarum/extend';
import app from 'flarum/app';
import UserControls from 'flarum/utils/UserControls';
import Button from 'flarum/components/Button';
import Badge from 'flarum/components/Badge';
import User from 'flarum/models/User';
import { extend } from 'flarum/common/extend';
import app from 'flarum/forum/app';
import UserControls from 'flarum/forum/utils/UserControls';
import Button from 'flarum/common/components/Button';
import Badge from 'flarum/common/components/Badge';
import User from 'flarum/common/models/User';
import SuspendUserModal from './components/SuspendUserModal';
import UserSuspendedNotification from './components/UserSuspendedNotification';
@ -42,8 +42,4 @@ app.initializers.add('flarum-suspend', () => {
checkForSuspension();
});
// Expose compat API
import suspendCompat from './compat';
import { compat } from '@flarum/core/forum';
Object.assign(compat, suspendCompat);
import './forum';

View File

@ -9,12 +9,7 @@
// This will output typings to `dist-typings`
"declarationDir": "./dist-typings",
"paths": {
"flarum/*": ["../../../framework/core/js/dist-typings/*"],
// TODO: remove after export registry system implemented
// Without this, the old-style `@flarum/core` import is resolved to
// source code in flarum/core instead of the dist typings.
// This causes an inaccurate "duplicate export" error.
"@flarum/core/*": ["../../../framework/core/js/dist-typings/*"],
"flarum/*": ["../../../framework/core/js/dist-typings/*"]
}
}
}

View File

@ -0,0 +1,9 @@
import '../common/common';
import './components/TagsPage';
import './components/EditTagModal';
import './addTagsHomePageOption';
import './addTagChangePermission';
import './addTagPermission';
import './addTagsPermissionScope';

View File

@ -1,17 +0,0 @@
import compat from '../common/compat';
import addTagsHomePageOption from './addTagsHomePageOption';
import addTagChangePermission from './addTagChangePermission';
import TagsPage from './components/TagsPage';
import EditTagModal from './components/EditTagModal';
import addTagPermission from './addTagPermission';
import addTagsPermissionScope from './addTagsPermissionScope';
export default Object.assign(compat, {
'tags/addTagsHomePageOption': addTagsHomePageOption,
'tags/addTagChangePermission': addTagChangePermission,
'tags/components/TagsPage': TagsPage,
'tags/components/EditTagModal': EditTagModal,
'tags/addTagPermission': addTagPermission,
'tags/addTagsPermissionScope': addTagsPermissionScope,
});

View File

@ -21,8 +21,4 @@ app.initializers.add('flarum-tags', (app) => {
addTagSelectionSettingComponent();
});
// Expose compat API
import tagsCompat from './compat';
import { compat } from '@flarum/core/admin';
Object.assign(compat, tagsCompat);
import './admin';

View File

@ -0,0 +1,11 @@
import './utils/sortTags';
import './models/Tag';
import './helpers/tagsLabel';
import './helpers/tagIcon';
import './helpers/tagLabel';
import './components/TagSelectionModal';
import './states/TagListState';

View File

@ -1,17 +0,0 @@
import sortTags from './utils/sortTags';
import Tag from './models/Tag';
import tagsLabel from './helpers/tagsLabel';
import tagIcon from './helpers/tagIcon';
import tagLabel from './helpers/tagLabel';
import TagSelectionModal from './components/TagSelectionModal';
import TagListState from './states/TagListState';
export default {
'tags/utils/sortTags': sortTags,
'tags/models/Tag': Tag,
'tags/helpers/tagsLabel': tagsLabel,
'tags/helpers/tagIcon': tagIcon,
'tags/helpers/tagLabel': tagLabel,
'tags/components/TagSelectionModal': TagSelectionModal,
'tags/states/TagListState': TagListState,
};

View File

@ -1,27 +0,0 @@
import compat from '../common/compat';
import addTagFilter from './addTagFilter';
import addTagControl from './addTagControl';
import TagHero from './components/TagHero';
import TagDiscussionModal from './components/TagDiscussionModal';
import TagsPage from './components/TagsPage';
import DiscussionTaggedPost from './components/DiscussionTaggedPost';
import TagLinkButton from './components/TagLinkButton';
import addTagList from './addTagList';
import addTagLabels from './addTagLabels';
import addTagComposer from './addTagComposer';
import getSelectableTags from './utils/getSelectableTags';
export default Object.assign(compat, {
'tags/addTagFilter': addTagFilter,
'tags/addTagControl': addTagControl,
'tags/components/TagHero': TagHero,
'tags/components/TagDiscussionModal': TagDiscussionModal,
'tags/components/TagsPage': TagsPage,
'tags/components/DiscussionTaggedPost': DiscussionTaggedPost,
'tags/components/TagLinkButton': TagLinkButton,
'tags/addTagList': addTagList,
'tags/addTagLabels': addTagLabels,
'tags/addTagComposer': addTagComposer,
'tags/utils/getSelectableTags': getSelectableTags,
});

View File

@ -0,0 +1,15 @@
import '../common/common';
import './utils/getSelectableTags';
import './components/TagHero';
import './components/TagDiscussionModal';
import './components/TagsPage';
import './components/DiscussionTaggedPost';
import './components/TagLinkButton';
import './addTagFilter';
import './addTagControl';
import './addTagList';
import './addTagLabels';
import './addTagComposer';

View File

@ -20,8 +20,4 @@ app.initializers.add('flarum-tags', function () {
addTagComposer();
});
// Expose compat API
import tagsCompat from './compat';
import { compat } from '@flarum/core/forum';
Object.assign(compat, tagsCompat);
import './forum';

View File

@ -9,12 +9,7 @@
// This will output typings to `dist-typings`
"declarationDir": "./dist-typings",
"paths": {
"flarum/*": ["../../../framework/core/js/dist-typings/*"],
// TODO: remove after export registry system implemented
// Without this, the old-style `@flarum/core` import is resolved to
// source code in flarum/core instead of the dist typings.
// This causes an inaccurate "duplicate export" error.
"@flarum/core/*": ["../../../framework/core/js/dist-typings/*"],
"flarum/*": ["../../../framework/core/js/dist-typings/*"]
}
}
}

View File

@ -98,6 +98,8 @@ interface FlarumObject {
* }
*/
extensions: Readonly<Record<string, ESModule>>;
reg: any;
}
declare const flarum: FlarumObject;

View File

@ -0,0 +1,40 @@
import '../common/common';
import './utils/saveSettings';
import './utils/ExtensionData';
import './utils/isExtensionEnabled';
import './utils/getCategorizedExtensions';
import './utils/generateElementId';
import './components/SettingDropdown';
import './components/EditCustomFooterModal';
import './components/SessionDropdown';
import './components/HeaderPrimary';
import './components/AdminPage';
import './components/AppearancePage';
import './components/StatusWidget';
import './components/ExtensionsWidget';
import './components/HeaderSecondary';
import './components/SettingsModal';
import './components/DashboardWidget';
import './components/ExtensionPage';
import './components/ExtensionLinkButton';
import './components/PermissionGrid';
import './components/ExtensionPermissionGrid';
import './components/MailPage';
import './components/UploadImageButton';
import './components/LoadingModal';
import './components/DashboardPage';
import './components/BasicsPage';
import './components/UserListPage';
import './components/EditCustomHeaderModal';
import './components/PermissionsPage';
import './components/PermissionDropdown';
import './components/AdminNav';
import './components/AdminHeader';
import './components/EditCustomCssModal';
import './components/EditGroupModal';
import './components/CreateUserModal';
import './routes';
import './AdminApplication';

View File

@ -1,77 +0,0 @@
import compat from '../common/compat';
import saveSettings from './utils/saveSettings';
import ExtensionData from './utils/ExtensionData';
import isExtensionEnabled from './utils/isExtensionEnabled';
import getCategorizedExtensions from './utils/getCategorizedExtensions';
import SettingDropdown from './components/SettingDropdown';
import EditCustomFooterModal from './components/EditCustomFooterModal';
import SessionDropdown from './components/SessionDropdown';
import HeaderPrimary from './components/HeaderPrimary';
import AdminPage from './components/AdminPage';
import AppearancePage from './components/AppearancePage';
import StatusWidget from './components/StatusWidget';
import ExtensionsWidget from './components/ExtensionsWidget';
import HeaderSecondary from './components/HeaderSecondary';
import SettingsModal from './components/SettingsModal';
import DashboardWidget from './components/DashboardWidget';
import ExtensionPage from './components/ExtensionPage';
import ExtensionLinkButton from './components/ExtensionLinkButton';
import PermissionGrid from './components/PermissionGrid';
import ExtensionPermissionGrid from './components/ExtensionPermissionGrid';
import MailPage from './components/MailPage';
import UploadImageButton from './components/UploadImageButton';
import LoadingModal from './components/LoadingModal';
import DashboardPage from './components/DashboardPage';
import BasicsPage from './components/BasicsPage';
import UserListPage from './components/UserListPage';
import EditCustomHeaderModal from './components/EditCustomHeaderModal';
import PermissionsPage from './components/PermissionsPage';
import PermissionDropdown from './components/PermissionDropdown';
import AdminNav from './components/AdminNav';
import AdminHeader from './components/AdminHeader';
import EditCustomCssModal from './components/EditCustomCssModal';
import EditGroupModal from './components/EditGroupModal';
import routes from './routes';
import AdminApplication from './AdminApplication';
import generateElementId from './utils/generateElementId';
import CreateUserModal from './components/CreateUserModal';
export default Object.assign(compat, {
'utils/saveSettings': saveSettings,
'utils/ExtensionData': ExtensionData,
'utils/isExtensionEnabled': isExtensionEnabled,
'utils/getCategorizedExtensions': getCategorizedExtensions,
'utils/generateElementId': generateElementId,
'components/SettingDropdown': SettingDropdown,
'components/EditCustomFooterModal': EditCustomFooterModal,
'components/SessionDropdown': SessionDropdown,
'components/HeaderPrimary': HeaderPrimary,
'components/AdminPage': AdminPage,
'components/AppearancePage': AppearancePage,
'components/StatusWidget': StatusWidget,
'components/ExtensionsWidget': ExtensionsWidget,
'components/HeaderSecondary': HeaderSecondary,
'components/SettingsModal': SettingsModal,
'components/DashboardWidget': DashboardWidget,
'components/ExtensionPage': ExtensionPage,
'components/ExtensionLinkButton': ExtensionLinkButton,
'components/PermissionGrid': PermissionGrid,
'components/ExtensionPermissionGrid': ExtensionPermissionGrid,
'components/MailPage': MailPage,
'components/UploadImageButton': UploadImageButton,
'components/LoadingModal': LoadingModal,
'components/DashboardPage': DashboardPage,
'components/BasicsPage': BasicsPage,
'components/UserListPage': UserListPage,
'components/EditCustomHeaderModal': EditCustomHeaderModal,
'components/PermissionsPage': PermissionsPage,
'components/PermissionDropdown': PermissionDropdown,
'components/AdminNav': AdminNav,
'components/AdminHeader': AdminHeader,
'components/EditCustomCssModal': EditCustomCssModal,
'components/EditGroupModal': EditGroupModal,
'components/CreateUserModal': CreateUserModal,
routes: routes,
AdminApplication: AdminApplication,
});

View File

@ -2,13 +2,4 @@ import app from './app';
export { app };
// Export public API
// Export compat API
import compatObj from './compat';
import proxifyCompat from '../common/utils/proxifyCompat';
// @ts-expect-error The `app` instance needs to be available on compat.
compatObj.app = app;
export const compat = proxifyCompat(compatObj, 'admin');
import './admin';

View File

@ -0,0 +1,58 @@
/**
* @internal
*/
export interface IExportRegistry {
moduleExports: Map<string, Map<string, any>>;
onLoads: Map<string, Map<string, Function[]>>;
/**
* Add an instance to the registry.
*/
add(namespace: string, id: string, object: any): void;
/**
* Add a function to run when object of id "id" is added (or overriden).
* If such an object is already registered, the handler will be applied immediately.
*/
onLoad(namespace: string, id: string, handler: Function): void;
/**
* Retrieve an object of type `id` from the registry.
*/
get(namespace: string, id: string): any;
}
export default class ExportRegistry implements IExportRegistry {
moduleExports = new Map<string, Map<string, any>>();
onLoads = new Map<string, Map<string, Function[]>>();
add(namespace: string, id: string, object: any): void {
this.moduleExports.set(namespace, this.moduleExports.get(namespace) || new Map());
this.moduleExports.get(namespace)?.set(id, object);
this.onLoads
.get(namespace)
?.get(id)
?.forEach((handler) => handler(object));
}
onLoad(namespace: string, id: string, handler: Function): void {
if (this.moduleExports.has(namespace) && this.moduleExports.get(namespace)?.has(id)) {
handler(this.moduleExports.get(namespace)?.get(id));
} else {
this.onLoads.set(namespace, this.onLoads.get(namespace) || new Map());
this.onLoads.get(namespace)?.set(id, this.onLoads.get(namespace)?.get(id) || []);
this.onLoads.get(namespace)?.get(id)?.push(handler);
}
}
get(namespace: string, id: string): any {
const module = this.moduleExports.get(namespace)?.get(id);
if (!module) {
console.warn(`No module found for ${namespace}:${id}`);
}
return module;
}
}

View File

@ -0,0 +1,89 @@
import './extend';
import './extenders';
import './states/PaginatedListState';
import './states/AlertManagerState';
import './states/ModalManagerState';
import './states/PageState';
import './utils/isObject';
import './utils/mixin';
import './utils/insertText';
import './utils/styleSelectedText';
import './utils/Drawer';
import './utils/anchorScroll';
import './utils/RequestError';
import './utils/abbreviateNumber';
import './utils/escapeRegExp';
import './utils/string';
import './utils/throttleDebounce';
import './utils/Stream';
import './utils/SubtreeRetainer';
import './utils/setRouteWithForcedRefresh';
import './utils/extract';
import './utils/ScrollListener';
import './utils/stringToColor';
import './utils/subclassOf';
import './utils/patchMithril';
import './utils/classList';
import './utils/extractText';
import './utils/formatNumber';
import './utils/mapRoutes';
import './utils/withAttr';
import './utils/focusTrap';
import './utils/isDark';
import './utils/KeyboardNavigatable';
import './models/Notification';
import './models/User';
import './models/Post';
import './models/Discussion';
import './models/Group';
import './models/Forum';
import './components/AlertManager';
import './components/Page';
import './components/Switch';
import './components/Badge';
import './components/LoadingIndicator';
import './components/Placeholder';
import './components/Separator';
import './components/Dropdown';
import './components/SplitDropdown';
import './components/RequestErrorModal';
import './components/FieldSet';
import './components/Select';
import './components/Navigation';
import './components/Alert';
import './components/Link';
import './components/LinkButton';
import './components/Checkbox';
import './components/ColorPreviewInput';
import './components/SelectDropdown';
import './components/ModalManager';
import './components/Button';
import './components/Modal';
import './components/GroupBadge';
import './components/TextEditor';
import './components/TextEditorButton';
import './components/EditUserModal';
import './components/Tooltip';
import './helpers/fullTime';
import './helpers/avatar';
import './helpers/icon';
import './helpers/humanTime';
import './helpers/punctuateSeries';
import './helpers/highlight';
import './helpers/username';
import './helpers/userOnline';
import './helpers/listItems';
import './helpers/textContrastClass';
import './resolvers/DefaultResolver';
import './Component';
import './Translator';
import './Model';
import './Application';
import './Fragment';

View File

@ -1,187 +0,0 @@
import * as extend from './extend';
import extenders from './extenders';
import Session from './Session';
import Store from './Store';
import BasicEditorDriver from './utils/BasicEditorDriver';
import evented from './utils/evented';
import EventEmitter from './utils/EventEmitter';
import KeyboardNavigatable from './utils/KeyboardNavigatable';
import liveHumanTimes from './utils/liveHumanTimes';
import ItemList from './utils/ItemList';
import mixin from './utils/mixin';
import humanTime from './utils/humanTime';
import computed from './utils/computed';
import insertText from './utils/insertText';
import styleSelectedText from './utils/styleSelectedText';
import Drawer from './utils/Drawer';
import anchorScroll from './utils/anchorScroll';
import RequestError from './utils/RequestError';
import abbreviateNumber from './utils/abbreviateNumber';
import escapeRegExp from './utils/escapeRegExp';
import * as string from './utils/string';
import * as ThrottleDebounce from './utils/throttleDebounce';
import Stream from './utils/Stream';
import SubtreeRetainer from './utils/SubtreeRetainer';
import setRouteWithForcedRefresh from './utils/setRouteWithForcedRefresh';
import extract from './utils/extract';
import ScrollListener from './utils/ScrollListener';
import stringToColor from './utils/stringToColor';
import subclassOf from './utils/subclassOf';
import patchMithril from './utils/patchMithril';
import proxifyCompat from './utils/proxifyCompat';
import classList from './utils/classList';
import extractText from './utils/extractText';
import formatNumber from './utils/formatNumber';
import mapRoutes from './utils/mapRoutes';
import withAttr from './utils/withAttr';
import * as FocusTrap from './utils/focusTrap';
import isDark from './utils/isDark';
import Notification from './models/Notification';
import User from './models/User';
import Post from './models/Post';
import Discussion from './models/Discussion';
import Group from './models/Group';
import Forum from './models/Forum';
import Component from './Component';
import Translator from './Translator';
import AlertManager from './components/AlertManager';
import Page from './components/Page';
import Switch from './components/Switch';
import Badge from './components/Badge';
import LoadingIndicator from './components/LoadingIndicator';
import Placeholder from './components/Placeholder';
import Separator from './components/Separator';
import Dropdown from './components/Dropdown';
import SplitDropdown from './components/SplitDropdown';
import RequestErrorModal from './components/RequestErrorModal';
import FieldSet from './components/FieldSet';
import Select from './components/Select';
import Navigation from './components/Navigation';
import Alert from './components/Alert';
import Link from './components/Link';
import LinkButton from './components/LinkButton';
import Checkbox from './components/Checkbox';
import ColorPreviewInput from './components/ColorPreviewInput';
import SelectDropdown from './components/SelectDropdown';
import ModalManager from './components/ModalManager';
import Button from './components/Button';
import Modal from './components/Modal';
import GroupBadge from './components/GroupBadge';
import TextEditor from './components/TextEditor';
import TextEditorButton from './components/TextEditorButton';
import EditUserModal from './components/EditUserModal';
import Tooltip from './components/Tooltip';
import Model from './Model';
import Application from './Application';
import fullTime from './helpers/fullTime';
import avatar from './helpers/avatar';
import icon from './helpers/icon';
import humanTimeHelper from './helpers/humanTime';
import punctuateSeries from './helpers/punctuateSeries';
import highlight from './helpers/highlight';
import username from './helpers/username';
import userOnline from './helpers/userOnline';
import listItems from './helpers/listItems';
import textContrastClass from './helpers/textContrastClass';
import Fragment from './Fragment';
import DefaultResolver from './resolvers/DefaultResolver';
import PaginatedListState from './states/PaginatedListState';
import isObject from './utils/isObject';
import AlertManagerState from './states/AlertManagerState';
import ModalManagerState from './states/ModalManagerState';
import PageState from './states/PageState';
export default {
extenders,
extend: extend,
Session: Session,
Store: Store,
'utils/BasicEditorDriver': BasicEditorDriver,
'utils/evented': evented,
'utils/EventEmitter': EventEmitter,
'utils/KeyboardNavigatable': KeyboardNavigatable,
'utils/liveHumanTimes': liveHumanTimes,
'utils/ItemList': ItemList,
'utils/mixin': mixin,
'utils/humanTime': humanTime,
'utils/computed': computed,
'utils/insertText': insertText,
'utils/styleSelectedText': styleSelectedText,
'utils/Drawer': Drawer,
'utils/anchorScroll': anchorScroll,
'utils/RequestError': RequestError,
'utils/abbreviateNumber': abbreviateNumber,
'utils/string': string,
'utils/SubtreeRetainer': SubtreeRetainer,
'utils/escapeRegExp': escapeRegExp,
'utils/extract': extract,
'utils/ScrollListener': ScrollListener,
'utils/stringToColor': stringToColor,
'utils/Stream': Stream,
'utils/subclassOf': subclassOf,
'utils/setRouteWithForcedRefresh': setRouteWithForcedRefresh,
'utils/patchMithril': patchMithril,
'utils/proxifyCompat': proxifyCompat,
'utils/classList': classList,
'utils/extractText': extractText,
'utils/formatNumber': formatNumber,
'utils/mapRoutes': mapRoutes,
'utils/withAttr': withAttr,
'utils/throttleDebounce': ThrottleDebounce,
'utils/isObject': isObject,
'utils/focusTrap': FocusTrap,
'utils/isDark': isDark,
'models/Notification': Notification,
'models/User': User,
'models/Post': Post,
'models/Discussion': Discussion,
'models/Group': Group,
'models/Forum': Forum,
Component: Component,
Fragment: Fragment,
Translator: Translator,
'components/AlertManager': AlertManager,
'components/Page': Page,
'components/Switch': Switch,
'components/Badge': Badge,
'components/LoadingIndicator': LoadingIndicator,
'components/Placeholder': Placeholder,
'components/Separator': Separator,
'components/Dropdown': Dropdown,
'components/SplitDropdown': SplitDropdown,
'components/RequestErrorModal': RequestErrorModal,
'components/FieldSet': FieldSet,
'components/Select': Select,
'components/Navigation': Navigation,
'components/Alert': Alert,
'components/Link': Link,
'components/LinkButton': LinkButton,
'components/Checkbox': Checkbox,
'components/ColorPreviewInput': ColorPreviewInput,
'components/SelectDropdown': SelectDropdown,
'components/ModalManager': ModalManager,
'components/Button': Button,
'components/Modal': Modal,
'components/GroupBadge': GroupBadge,
'components/TextEditor': TextEditor,
'components/TextEditorButton': TextEditorButton,
'components/Tooltip': Tooltip,
'components/EditUserModal': EditUserModal,
Model: Model,
Application: Application,
'helpers/fullTime': fullTime,
'helpers/avatar': avatar,
'helpers/icon': icon,
'helpers/humanTime': humanTimeHelper,
'helpers/punctuateSeries': punctuateSeries,
'helpers/highlight': highlight,
'helpers/username': username,
'helpers/userOnline': userOnline,
'helpers/listItems': listItems,
'helpers/textContrastClass': textContrastClass,
'resolvers/DefaultResolver': DefaultResolver,
'states/PaginatedListState': PaginatedListState,
'states/AlertManagerState': AlertManagerState,
'states/ModalManagerState': ModalManagerState,
'states/PageState': PageState,
};

View File

@ -3,9 +3,11 @@ import PostTypes from './PostTypes';
import Routes from './Routes';
import Store from './Store';
export default {
const extenders = {
Model,
PostTypes,
Routes,
Store,
};
export default extenders;

View File

@ -15,6 +15,8 @@ import localizedFormat from 'dayjs/plugin/localizedFormat';
dayjs.extend(relativeTime);
dayjs.extend(localizedFormat);
import './registry';
import patchMithril from './utils/patchMithril';
patchMithril(window);

View File

@ -0,0 +1,3 @@
import ExportRegistry from './ExportRegistry';
flarum.reg = new ExportRegistry();

View File

@ -1,12 +0,0 @@
export default function proxifyCompat(compat: Record<string, unknown>, namespace: string) {
// regex to replace common/ and NAMESPACE/ for core & core extensions
// and remove .js, .ts and .tsx extensions
// e.g. admin/utils/extract --> utils/extract
// e.g. tags/common/utils/sortTags --> tags/utils/sortTags
const regex = new RegExp(String.raw`(\w+\/)?(${namespace}|common)\/`);
const fileExt = /(\.js|\.tsx?)$/;
return new Proxy(compat, {
get: (obj, prop: string) => obj[prop] || obj[prop.replace(regex, '$1').replace(fileExt, '')],
});
}

View File

@ -1,3 +1,3 @@
// Re-exports `throttle-debounce` to be used in `compat.js`.
// Re-exports `throttle-debounce` to be added in the export registry.
export { throttle, debounce } from 'throttle-debounce';

View File

@ -9,7 +9,8 @@
* Replaces m.withAttr for Mithril 2.0.
* @see https://mithril.js.org/archive/v0.2.5/mithril.withAttr.html
*/
export default (key: string, cb: Function) =>
function (this: Element) {
export default function withAttr(key: string, cb: Function) {
return function (this: Element) {
cb(this.getAttribute(key) || (this as any)[key]);
};
}

View File

@ -1,156 +0,0 @@
import compat from '../common/compat';
import PostControls from './utils/PostControls';
import KeyboardNavigatable from '../common/utils/KeyboardNavigatable';
import slidable from './utils/slidable';
import History from './utils/History';
import DiscussionControls from './utils/DiscussionControls';
import alertEmailConfirmation from './utils/alertEmailConfirmation';
import UserControls from './utils/UserControls';
import Pane from './utils/Pane';
import ComposerState from './states/ComposerState';
import DiscussionListState from './states/DiscussionListState';
import GlobalSearchState from './states/GlobalSearchState';
import NotificationListState from './states/NotificationListState';
import PostStreamState from './states/PostStreamState';
import SearchState from './states/SearchState';
import UserSecurityPageState from './states/UserSecurityPageState';
import AffixedSidebar from './components/AffixedSidebar';
import DiscussionPage from './components/DiscussionPage';
import DiscussionListPane from './components/DiscussionListPane';
import LogInModal from './components/LogInModal';
import ComposerBody from './components/ComposerBody';
import ForgotPasswordModal from './components/ForgotPasswordModal';
import Notification from './components/Notification';
import LogInButton from './components/LogInButton';
import DiscussionsUserPage from './components/DiscussionsUserPage';
import Composer from './components/Composer';
import SessionDropdown from './components/SessionDropdown';
import HeaderPrimary from './components/HeaderPrimary';
import PostEdited from './components/PostEdited';
import PostStream from './components/PostStream';
import ChangePasswordModal from './components/ChangePasswordModal';
import IndexPage from './components/IndexPage';
import DiscussionRenamedNotification from './components/DiscussionRenamedNotification';
import DiscussionsSearchSource from './components/DiscussionsSearchSource';
import HeaderSecondary from './components/HeaderSecondary';
import ComposerButton from './components/ComposerButton';
import DiscussionList from './components/DiscussionList';
import ReplyPlaceholder from './components/ReplyPlaceholder';
import AvatarEditor from './components/AvatarEditor';
import Post from './components/Post';
import SettingsPage from './components/SettingsPage';
import TerminalPost from './components/TerminalPost';
import ChangeEmailModal from './components/ChangeEmailModal';
import NotificationsDropdown from './components/NotificationsDropdown';
import UserPage from './components/UserPage';
import PostUser from './components/PostUser';
import UserCard from './components/UserCard';
import UsersSearchSource from './components/UsersSearchSource';
import UserSecurityPage from './components/UserSecurityPage';
import NotificationGrid from './components/NotificationGrid';
import PostPreview from './components/PostPreview';
import EventPost from './components/EventPost';
import DiscussionHero from './components/DiscussionHero';
import PostMeta from './components/PostMeta';
import DiscussionRenamedPost from './components/DiscussionRenamedPost';
import DiscussionComposer from './components/DiscussionComposer';
import LogInButtons from './components/LogInButtons';
import NotificationList from './components/NotificationList';
import WelcomeHero from './components/WelcomeHero';
import SignUpModal from './components/SignUpModal';
import CommentPost from './components/CommentPost';
import ComposerPostPreview from './components/ComposerPostPreview';
import ReplyComposer from './components/ReplyComposer';
import NotificationsPage from './components/NotificationsPage';
import PostStreamScrubber from './components/PostStreamScrubber';
import EditPostComposer from './components/EditPostComposer';
import RenameDiscussionModal from './components/RenameDiscussionModal';
import Search from './components/Search';
import DiscussionListItem from './components/DiscussionListItem';
import LoadingPost from './components/LoadingPost';
import PostsUserPage from './components/PostsUserPage';
import DiscussionPageResolver from './resolvers/DiscussionPageResolver';
import BasicEditorDriver from '../common/utils/BasicEditorDriver';
import routes from './routes';
import ForumApplication from './ForumApplication';
import isSafariMobile from './utils/isSafariMobile';
export default Object.assign(compat, {
'utils/PostControls': PostControls,
// @deprecated import from 'flarum/common/utils/KeyboardNavigatable' instead
'utils/KeyboardNavigatable': KeyboardNavigatable,
'utils/slidable': slidable,
'utils/History': History,
'utils/DiscussionControls': DiscussionControls,
'utils/alertEmailConfirmation': alertEmailConfirmation,
'utils/UserControls': UserControls,
'utils/Pane': Pane,
'utils/BasicEditorDriver': BasicEditorDriver,
'utils/isSafariMobile': isSafariMobile,
'states/ComposerState': ComposerState,
'states/DiscussionListState': DiscussionListState,
'states/GlobalSearchState': GlobalSearchState,
'states/NotificationListState': NotificationListState,
'states/PostStreamState': PostStreamState,
'states/SearchState': SearchState,
'states/UserSecurityPageState': UserSecurityPageState,
'components/AffixedSidebar': AffixedSidebar,
'components/DiscussionPage': DiscussionPage,
'components/DiscussionListPane': DiscussionListPane,
'components/LogInModal': LogInModal,
'components/ComposerBody': ComposerBody,
'components/ForgotPasswordModal': ForgotPasswordModal,
'components/Notification': Notification,
'components/LogInButton': LogInButton,
'components/DiscussionsUserPage': DiscussionsUserPage,
'components/Composer': Composer,
'components/SessionDropdown': SessionDropdown,
'components/HeaderPrimary': HeaderPrimary,
'components/PostEdited': PostEdited,
'components/PostStream': PostStream,
'components/ChangePasswordModal': ChangePasswordModal,
'components/IndexPage': IndexPage,
'components/DiscussionRenamedNotification': DiscussionRenamedNotification,
'components/DiscussionsSearchSource': DiscussionsSearchSource,
'components/HeaderSecondary': HeaderSecondary,
'components/ComposerButton': ComposerButton,
'components/DiscussionList': DiscussionList,
'components/ReplyPlaceholder': ReplyPlaceholder,
'components/AvatarEditor': AvatarEditor,
'components/Post': Post,
'components/SettingsPage': SettingsPage,
'components/TerminalPost': TerminalPost,
'components/ChangeEmailModal': ChangeEmailModal,
'components/NotificationsDropdown': NotificationsDropdown,
'components/UserPage': UserPage,
'components/PostUser': PostUser,
'components/UserCard': UserCard,
'components/UsersSearchSource': UsersSearchSource,
'components/UserSecurityPage': UserSecurityPage,
'components/NotificationGrid': NotificationGrid,
'components/PostPreview': PostPreview,
'components/EventPost': EventPost,
'components/DiscussionHero': DiscussionHero,
'components/PostMeta': PostMeta,
'components/DiscussionRenamedPost': DiscussionRenamedPost,
'components/DiscussionComposer': DiscussionComposer,
'components/LogInButtons': LogInButtons,
'components/NotificationList': NotificationList,
'components/WelcomeHero': WelcomeHero,
'components/SignUpModal': SignUpModal,
'components/CommentPost': CommentPost,
'components/ComposerPostPreview': ComposerPostPreview,
'components/ReplyComposer': ReplyComposer,
'components/NotificationsPage': NotificationsPage,
'components/PostStreamScrubber': PostStreamScrubber,
'components/EditPostComposer': EditPostComposer,
'components/RenameDiscussionModal': RenameDiscussionModal,
'components/Search': Search,
'components/DiscussionListItem': DiscussionListItem,
'components/LoadingPost': LoadingPost,
'components/PostsUserPage': PostsUserPage,
'resolvers/DiscussionPageResolver': DiscussionPageResolver,
routes: routes,
ForumApplication: ForumApplication,
});

View File

@ -0,0 +1,75 @@
import '../common/common';
import '../common/utils/BasicEditorDriver';
import './utils/PostControls';
import './utils/slidable';
import './utils/History';
import './utils/DiscussionControls';
import './utils/alertEmailConfirmation';
import './utils/UserControls';
import './utils/Pane';
import './states/ComposerState';
import './states/DiscussionListState';
import './states/GlobalSearchState';
import './states/NotificationListState';
import './states/PostStreamState';
import './states/SearchState';
import './states/UserSecurityPageState';
import './components/AffixedSidebar';
import './components/DiscussionPage';
import './components/DiscussionListPane';
import './components/LogInModal';
import './components/ComposerBody';
import './components/ForgotPasswordModal';
import './components/Notification';
import './components/LogInButton';
import './components/DiscussionsUserPage';
import './components/Composer';
import './components/SessionDropdown';
import './components/HeaderPrimary';
import './components/PostEdited';
import './components/PostStream';
import './components/ChangePasswordModal';
import './components/IndexPage';
import './components/DiscussionRenamedNotification';
import './components/DiscussionsSearchSource';
import './components/HeaderSecondary';
import './components/ComposerButton';
import './components/DiscussionList';
import './components/ReplyPlaceholder';
import './components/AvatarEditor';
import './components/Post';
import './components/SettingsPage';
import './components/TerminalPost';
import './components/ChangeEmailModal';
import './components/NotificationsDropdown';
import './components/UserPage';
import './components/PostUser';
import './components/UserCard';
import './components/UsersSearchSource';
import './components/UserSecurityPage';
import './components/NotificationGrid';
import './components/PostPreview';
import './components/EventPost';
import './components/DiscussionHero';
import './components/PostMeta';
import './components/DiscussionRenamedPost';
import './components/DiscussionComposer';
import './components/LogInButtons';
import './components/NotificationList';
import './components/WelcomeHero';
import './components/SignUpModal';
import './components/CommentPost';
import './components/ComposerPostPreview';
import './components/ReplyComposer';
import './components/NotificationsPage';
import './components/PostStreamScrubber';
import './components/EditPostComposer';
import './components/RenameDiscussionModal';
import './components/Search';
import './components/DiscussionListItem';
import './components/LoadingPost';
import './components/PostsUserPage';
import './resolvers/DiscussionPageResolver';
import './routes';
import './ForumApplication';

View File

@ -6,11 +6,4 @@ import app from './app';
export { app };
// Export compat API
import compatObj from './compat';
import proxifyCompat from '../common/utils/proxifyCompat';
// @ts-ignore
compatObj.app = app;
export const compat = proxifyCompat(compatObj, 'forum');
import './forum';

View File

@ -12,7 +12,7 @@ import extractText from '../../common/utils/extractText';
* The `DiscussionControls` utility constructs a list of buttons for a
* discussion which perform actions on it.
*/
export default {
const DiscussionControls = {
/**
* Get a list of controls for a discussion.
*
@ -240,3 +240,5 @@ export default {
});
},
};
export default DiscussionControls;

View File

@ -9,7 +9,7 @@ import extractText from '../../common/utils/extractText';
* The `PostControls` utility constructs a list of buttons for a post which
* perform actions on it.
*/
export default {
const PostControls = {
/**
* Get a list of controls for a post.
*
@ -183,3 +183,5 @@ export default {
});
},
};
export default PostControls;

View File

@ -9,7 +9,7 @@ import ItemList from '../../common/utils/ItemList';
* The `UserControls` utility constructs a list of buttons for a user which
* perform actions on it.
*/
export default {
const UserControls = {
/**
* Get a list of controls for a user.
*
@ -146,3 +146,5 @@ export default {
app.modal.show(EditUserModal, { user });
},
};
export default UserControls;

View File

@ -40,7 +40,7 @@ class AddTranslations
$sources->addString(function () use ($locale) {
$translations = $this->getTranslations($locale);
return 'flarum.core.app.translator.addTranslations('.json_encode($translations).')';
return 'app.translator.addTranslations('.json_encode($translations).')';
});
});
}

View File

@ -28,9 +28,9 @@
document.getElementById('flarum-loading').style.display = 'none';
try {
flarum.core.app.load(data);
flarum.core.app.bootExtensions(flarum.extensions);
flarum.core.app.boot();
app.load(data);
app.bootExtensions(flarum.extensions);
app.boot();
} catch (e) {
var error = document.getElementById('flarum-loading-error');
error.innerHTML += document.getElementById('flarum-content').textContent;

View File

@ -6,7 +6,7 @@ module.exports = (options = {}) => ({
transform: {
'^.+\\.[tj]sx?$': [
'babel-jest',
require('flarum-webpack-config/babel.config.js'),
require('flarum-webpack-config/babel.config.cjs'),
],
'^.+\\.tsx?$': [
'ts-jest',

View File

@ -31,27 +31,3 @@ Add another build script to your `package.json` like the one below:
You'll need to configure a `tsconfig.json` file to ensure your IDE sets up Typescript support correctly.
For details about this, see the [`flarum/flarum-tsconfig` repository](https://github.com/flarum/flarum-tsconfig)
## Options
### `useExtensions`
`Array<string>`, defaults to `[]`.
An array of extensions whose modules should be made available. This is a shortcut to add [`externals`](https://webpack.js.org/configuration/externals/) configuration for extension modules. Imported extension modules will not be bundled, but will instead refer to the extension's exports included in the Flarum runtime (ie. `flarum.extensions["vendor/package"]`).
For example, to access the Tags extension module within your extension:
**forum.js**
```js
import { Tag } from '@flarum/tags/forum';
```
**webpack.config.js**
```js
module.exports = config({
useExtensions: ['flarum/tags'],
});
```

View File

@ -1,8 +1,9 @@
{
"name": "flarum-webpack-config",
"type": "module",
"version": "2.0.2",
"description": "Webpack config for Flarum JS and TS transpilation.",
"main": "index.js",
"main": "src/index.cjs",
"author": "Flarum Team",
"license": "MIT",
"prettier": "@flarum/prettier-config",
@ -21,13 +22,18 @@
"@babel/preset-typescript": "^7.18.6",
"@babel/runtime": "^7.20.1",
"babel-loader": "^9.1.0",
"loader-utils": "^1.4.0",
"schema-utils": "^3.0.0",
"typescript": "^4.9.3",
"webpack": "^5.76.0",
"webpack-bundle-analyzer": "^4.7.0"
},
"devDependencies": {
"prettier": "^2.8.0",
"@flarum/prettier-config": "^1.0.0"
"@flarum/prettier-config": "^1.0.0",
"babel-jest": "^29.5.0",
"jest": "^29.5.0",
"memfs": "^3.5.3",
"prettier": "^2.8.0"
},
"scripts": {
"dev": "echo 'skipping..'",
@ -39,6 +45,10 @@
"build-typings": "echo 'skipping..'",
"post-build-typings": "echo 'skipping..'",
"check-typings": "echo 'skipping..'",
"check-typings-coverage": "echo 'skipping..'"
"check-typings-coverage": "echo 'skipping..'",
"test": "jest"
},
"jest": {
"testEnvironment": "node"
}
}

View File

@ -0,0 +1,185 @@
/**
* Auto Export Loader
*
* This loader will automatically pick up all core and extension exports and add them to the registry.
*/
const path = require('path');
const fs = require('fs');
const { validate } = require('schema-utils');
const { getOptions, interpolateName } = require('loader-utils');
const optionsSchema = {
type: 'object',
properties: {
extension: {
type: 'string',
},
},
};
let namespace;
function addAutoExports(source, pathToModule, moduleName) {
let addition = '';
const defaultExportMatches = [...source.matchAll(/export\s+?default\s(?:abstract\s)?(?:(?:function|abstract|class)\s)?([A-Za-z_]*)/gm)];
const defaultExport = defaultExportMatches.length ? defaultExportMatches[0][1] : null;
// In case of an index.js file that exports multiple modules
// we need to add the directory as a module.
// For an example checkout the `common/extenders/index.js` file.
if (moduleName === 'index') {
const id = pathToModule.substring(0, pathToModule.length - 1);
// Add code at the end of the file to add the file to registry
addition += `\nflarum.reg.add('${namespace}', '${id}', ${defaultExport})`;
}
// In a normal case, we do one of two things:
else {
// 1. If there is a default export, we add the module to the registry with the default export.
// Example: `export default class Foo {}` will be added to the registry as `Foo`,
// and can be imported using `import Foo from 'flarum/../Foo'`.
if (defaultExport) {
// Add code at the end of the file to add the file to registry
addition += `\nflarum.reg.add('${namespace}', '${pathToModule}${moduleName}', ${defaultExport})`;
}
// 2. If there is no default export, then there are named exports,
// so we add the module to the registry with the map of named exports.
// Example: `export class Foo {}` will be added to the registry as `{ Foo: 'Foo' }`,
// and can be imported using `import { Foo } from 'flarum/../Foo'`,
// (checkout the `common/utils/string.ts` file for an example).
else {
// Another two case scenarios is when using `export { A, B } from 'x'`.
// 2.1. If there is a default export, we add the module to the registry with the default export and ignore the named exports.
// Example: `export { nanoid as default, x } from 'nanoid'` will be added to the registry as `nanoid`,
// and can be imported using `import nanoid from 'flarum/../nanoid'`. x will be ignored.
const objectExportWithDefaultMatches = [...source.matchAll(/export\s+?{.*as\s+?default.*}\s+?from\s+?['"](.*)['"]/gm)];
if (objectExportWithDefaultMatches.length) {
let objectDefaultExport = null;
source = source.replace(/export\s+?{\s?([A-z_0-9]*)\s?as\s+?default.*}\s+?from\s+?['"](.*)['"]/gm, (match, defaultExport, path) => {
objectDefaultExport = defaultExport;
return `import { ${defaultExport} } from '${path}';\nexport default ${defaultExport}`;
});
addition += `\nflarum.reg.add('${namespace}', '${pathToModule}${moduleName}', ${objectDefaultExport})`;
}
// 2.2. If there is no default export, check for direct exports from other modules.
// We add the module to the registry with the map of named exports.
// Example: `export { A, B } from 'nanoid'` will be added to the registry as `{ A, B }`,
// and can be imported using `import { A, B } from 'flarum/../nanoid'`.
else {
const exportCurlyPattern = /export\s+?{(.*)}\s+?from\s+?['"](.*)['"]/gm;
const namedExportMatches = [...source.matchAll(exportCurlyPattern)];
if (namedExportMatches.length) {
source = source.replaceAll(exportCurlyPattern, (match, names, path) => {
return names
.split(',')
.map((name) => `import { ${name} } from '${path}';\nexport { ${name} }`)
.join('\n');
});
// Addition to the registry is taken care of in step 2.3
}
}
// 2.3. Finally, we check for all named exports
// these can be `export function|class|.. Name ..`
// or `export { ... };
{
const matches = [...source.matchAll(/export\s+?(?:\* as|function|{\s*([A-z0-9, ]+)+\s?}|const|abstract\s?|class)+?\s?([A-Za-z_]*)?/gm)];
if (matches.length) {
const map = matches.reduce((map, match) => {
const names = match[1] ? match[1].split(',') : (match[2] ? [match[2]] : null);
if (!names) {
return map;
}
for (let name of names) {
name = name.trim();
if (name === 'interface' || name === '') {
continue;
}
map += `${name}: ${name},`;
}
return map;
}, '');
// Add code at the end of the file to add the file to registry
if (map) addition += `\nflarum.reg.add('${namespace}', '${pathToModule}${moduleName}', { ${map} })`;
}
}
}
}
return source + addition;
}
// Custom loader logic
module.exports = function autoExportLoader(source) {
const options = getOptions(this) || {};
validate(optionsSchema, options, {
name: 'Flarum Webpack Loader',
composerPath: 'Path to the extension composer.json file',
});
// Ensure that composer.json is watched for changes
// so that the loader is run again when composer.json
// is updated.
const composerJsonPath = path.resolve(options.composerPath || '../composer.json');
this.addDependency(composerJsonPath);
// Get the namespace of the module to be exported
// the namespace is essentially just the usual extension ID.
if (!namespace) {
const composerJson = JSON.parse(fs.readFileSync(composerJsonPath, 'utf8'));
// Get the value of the 'name' property
namespace =
composerJson.name === 'flarum/core' ? 'core' : composerJson.name.replace('/flarum-ext-', '-').replace('/flarum-', '').replace('/', '-');
}
// Get the type of the module to be exported
const location = interpolateName(this, '[folder]/', {
context: this.rootContext || this.context,
});
// Get the name of module to be exported
const moduleName = interpolateName(this, '[name]', {
context: this.rootContext || this.context,
});
// Don't export low level files
if ((/(admin|forum)\/$/.test(location) && moduleName !== 'app') || /(compat|ExportRegistry|registry)$/.test(moduleName)) {
return source;
}
// Don't export index.js of common
if (moduleName === 'index' && location === 'common/') {
return source;
}
// Don't export extend.js of extensions
if (namespace !== 'core' && /extend$/.test(moduleName)) {
return source;
}
// Get the path of the module to be exported
// relative to the src directory.
// Example: src/forum/components/UserCard.js => forum/components
const pathToModule = this.resourcePath.replace(path.resolve(this.rootContext, 'src') + '/', '').replace(/[A-Za-z_]+\.[A-Za-z_]+$/, '');
return addAutoExports(source, pathToModule, moduleName);
};

View File

@ -55,7 +55,7 @@ if (useBundleAnalyzer) {
plugins.push(new (require('webpack-bundle-analyzer').BundleAnalyzerPlugin)());
}
module.exports = function (options = {}) {
module.exports = function () {
return {
// Set up entry points for each of the forum + admin apps, but only
// if they exist.
@ -69,12 +69,15 @@ module.exports = function (options = {}) {
module: {
rules: [
{
include: /src/, // Only apply this loader to files in the src directory
loader: path.resolve(__dirname, './autoExportLoader.cjs'),
},
{
// Matches .js, .jsx, .ts, .tsx
// See: https://regexr.com/5snjd
test: /\.[jt]sx?$/,
loader: require.resolve('babel-loader'),
options: require('./babel.config'),
options: require('../babel.config.cjs'),
resolve: {
fullySpecified: false,
},
@ -91,33 +94,24 @@ module.exports = function (options = {}) {
externals: [
{
'@flarum/core/forum': 'flarum.core',
'@flarum/core/admin': 'flarum.core',
jquery: 'jQuery',
},
(function () {
const externals = {};
if (options.useExtensions) {
for (const extension of options.useExtensions) {
externals['@' + extension] =
externals['@' + extension + '/forum'] =
externals['@' + extension + '/admin'] =
"flarum.extensions['" + extension + "']";
}
}
return externals;
})(),
// Support importing old-style core modules.
function ({ request }, callback) {
let namespace;
let id;
let matches;
if ((matches = /^flarum\/(.+)$/.exec(request))) {
return callback(null, "root flarum.core.compat['" + matches[1] + "']");
namespace = 'core';
id = matches[1];
} else if ((matches = /^ext:([^\/]+)\/(?:flarum-(?:ext-)?)?([^\/]+)(?:\/(.+))?$/.exec(request))) {
namespace = `${matches[1]}-${matches[2]}`;
id = matches[3];
} else {
return callback();
}
callback();
return callback(null, `root flarum.reg.get('${namespace}', '${id}')`);
},
],

View File

@ -0,0 +1,54 @@
/**
* @jest-environment node
*/
import compiler from './compiler.js';
import 'regenerator-runtime/runtime';
const compile = async (path, useFinalOutput = false) => {
const stats = await compiler(path);
return useFinalOutput
? stats.finalOutput
: stats.toJson({
source: true,
}).modules[0].source;
};
test('A directory with index.js that exports multiple modules adds the directory as a module', async () => {
let output = await compile('src/common/bars/index.js', true);
expect(output).toContain("flarum.reg.add('flarum-framework', 'common/bars', bars)");
output = await compile('src/common/bars/Acme.js');
expect(output).toContain("flarum.reg.add('flarum-framework', 'common/bars/Acme', Acme)");
output = await compile('src/common/bars/Foo.js');
expect(output).toContain("flarum.reg.add('flarum-framework', 'common/bars/Foo', Foo)");
});
test('Simple default exports are added', async () => {
const output = await compile('src/common/Test.js');
expect(output).toContain("flarum.reg.add('flarum-framework', 'common/Test', Test)");
});
test('Named exports are added', async () => {
const output = await compile('src/common/foos/namedExports.js');
expect(output).toContain(
"flarum.reg.add('flarum-framework', 'common/foos/namedExports', { baz: baz,foo: foo,Bar: Bar,sasha: sasha,flarum: flarum,david: david, })"
);
});
test('Export as default from another module is added', async () => {
const output = await compile('src/common/foos/exportDefaultFrom.js', true);
expect(output).toContain("flarum.reg.add('flarum-framework', 'common/foos/exportDefaultFrom', potato");
});
test('Export from other modules is added', async () => {
const output = await compile('src/common/foos/exportFrom.js', true);
expect(output).toContain("flarum.reg.add('flarum-framework', 'common/foos/exportFrom', { potato: potato,franz: franz, }");
});
test('Export from with other named exports works', async () => {
const output = await compile('src/common/foos/exportFromWithNamedExports.js', true);
expect(output).toContain(
"flarum.reg.add('flarum-framework', 'common/foos/exportFromWithNamedExports', { potato: potato,franz: franz,baz: baz,foo: foo,Bar: Bar,sasha: sasha,forum: forum,david: david, }"
);
});

View File

@ -0,0 +1,48 @@
import path from 'path';
import webpack from 'webpack';
import { createFsFromVolume, Volume } from 'memfs';
import * as fs from 'fs';
export default (fixture, options = {}) => {
const compiler = webpack({
context: __dirname,
entry: `./${fixture}`,
output: {
path: path.resolve(__dirname),
filename: 'bundle.js',
},
module: {
rules: [
{
test: /\.js$/,
use: {
loader: path.resolve(__dirname, '../src/autoExportLoader.cjs'),
options: {
...options,
composerPath: '../../composer.json',
},
},
},
],
},
optimization: {
minimize: false,
minimizer: [],
},
});
compiler.outputFileSystem = createFsFromVolume(new Volume());
compiler.outputFileSystem.join = path.join.bind(path);
return new Promise((resolve, reject) => {
compiler.run((err, stats) => {
if (err) reject(err);
if (stats.hasErrors()) reject(stats.toJson().errors);
const outputFilepath = path.join(compiler.options.output.path, compiler.options.output.filename);
stats.finalOutput = compiler.outputFileSystem.readFileSync(outputFilepath, 'utf-8');
resolve(stats);
});
});
};

View File

@ -0,0 +1 @@
export default class Test {}

View File

@ -0,0 +1 @@
export default class Acme {}

View File

@ -0,0 +1 @@
export default class Foo {}

View File

@ -0,0 +1,9 @@
import Acme from './Acme.js';
import Foo from './Foo.js';
const bars = {
Acme,
Foo,
};
export default bars;

View File

@ -0,0 +1 @@
export { potato as default } from '../support/potato.js';

View File

@ -0,0 +1 @@
export { potato, franz } from '../support/potato.js';

View File

@ -0,0 +1,16 @@
export { potato, franz } from '../support/potato.js';
export function baz() {}
export function foo() {}
export class Bar {}
const sasha = 'camel';
export { sasha };
const forum = 'Flarum';
const david = 'david';
export { forum, david };

View File

@ -0,0 +1,14 @@
export function baz() {}
export function foo() {}
export class Bar {}
const sasha = 'camel';
export { sasha };
const flarum = 'Flarum';
const david = 'david';
export { flarum, david };

View File

@ -0,0 +1,4 @@
const potato = 'potato';
const franz = 'franz';
export { potato, franz };

1796
yarn.lock

File diff suppressed because it is too large Load Diff