Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🔼 Extension 2.0 Upgrade #43

Open
SychO9 opened this issue Jul 27, 2024 · 0 comments
Open

🔼 Extension 2.0 Upgrade #43

SychO9 opened this issue Jul 27, 2024 · 0 comments

Comments

@SychO9
Copy link
Member

SychO9 commented Jul 27, 2024

Brainstorm

Most of the changes will be simple pattern replacements, to do that we need to take into account some base behavior/abilities.

  • Before we read and change code, we need to format it to ensure no missed patterns due to unexpected formatting.
  • For every file we visit (JS or PHP) before we get to the petterns, we need to parse the imports and have solid logic (using lexers) to add/remove imports. Then we can read the pattern replacements and apply any import changes that come with based on what the replacement needs.
  • Detective steps (:p) where we look into code, gather information, delete code, regenerate new code based on gathered info in hopes that no info was lost (tricky, might drop this idea).

Dependencies ✅

  • composer.json.require.flarum/* ^2.0 (when not set to*)
  • composer.json.require-dev.flarum/* ^2.0 (when not set to*)

Infra ✅

  • flarum/framework/.github/workflows/REUSABLE_backend.yml@main or @1.x => @2.x
  • flarum/framework/.github/workflows/REUSABLE_frontend.yml@main or @1.x => @2.x
  • phpstan

Frontend

mithril 2.0 -> 2.2

can't find exact instances of this in the code, but the more common instance is a copy from core of:

before

return [  
  <li className="Dropdown-header">{app.translator.trans('core.forum.search.users_heading')}</li>,  
  ...results.map((user) => {  
    const name = username(user);  
  
    const children = [highlight(name.text as string, query)];  
  
    return (  
      <li className="UserSearchResult" data-index={'users' + user.id()}>  
        <Link href={app.route.user(user)}>  
          {avatar(user)}  
          {{ ...name, text: undefined, children }}  
        </Link>  
      </li>    );  
  }),  
];

after

return [  
  <li className="Dropdown-header">{app.translator.trans('core.forum.search.users_heading')}</li>,  
  ...results.map((user) => {  
    const name = username(user, (name: string) => highlight(name, query));  
  
    return (  
      <li className="UserSearchResult" data-index={'users' + user.id()}>  
        <Link href={app.route.user(user)}>  
          {avatar(user)}  
          {name}  
        </Link>  
      </li>    );  
  }),  
];

Export Registry

Compat API ✅

compat API no longer works, instead all modules must be imported

before
https://github.com/flarum/framework/blob/1.x/extensions/tags/js/src/forum/compat.js
after
https://github.com/flarum/framework/blob/2.x/extensions/tags/js/src/forum/forum.ts

Importing Modules ✅

// Before  
import Tag from 'flarum/tags/common/models/Tag';  
  
// After  
import Tag from 'ext:flarum/tags/common/models/Tag';
  • warn of existing usage of @flarum/core imports (must remove/replace). ✅
  • warn of usage of useExtensions must use new import format of ext: instead ✅

Code splitting ✅

  • auto replace app.modal.show calls for lazy loaded modules ✅
  • auto replace extend/override of flarum lazy loaded modules ✅
// before
extend(LogInModal.prototype, 'oninit', function() {  
  console.log('LogInModal is loaded');  
});

// after
extend('flarum/forum/components/LogInModal', 'oninit', function() {  
  console.log('LogInModal is loaded');  
});

forum
Composer
DiscussionsUserPage
ForgotPasswordModal
NotificationsPage
PostStreamScrubber
SearchModal
SignUpModal
DiscussionComposer
EditPostComposer
LogInModal
PostStream
ReplyComposer
SettingsPage
UserSecurityPage

common
EditUserModal

Misc

  • if inherits NotificationsDropdown => HeaderDropdown
// before
 ... extends NotificationsDropdown

  getMenu() {
    return (
      <div className={'Dropdown-menu ' + this.attrs.menuClassName} onclick={this.menuClick.bind(this)}>
        {this.showing && <FlagList state={this.attrs.state} />}
      </div>
    );
  }

// after
 ... extends HeaderDropdown

  getContent() {
    return <FlagList state={this.attrs.state} />;
  }
  • Form: ✅
// before
<div className="Form$1">$2</div>

// after
import Form from 'flarum/common/components/Form';

<Form className="$1">$2</Form>
  • initializers.has('lock') => initializers.has('flarum-lock')
  • initializers.has('subscriptions') => initializers.has('flarum-subscriptions')
  • initializers.has('flarum/nicknames') => initializers.add('flarum-nicknames')

Backend

Misc

  • (Extend\Notification)->type() remove second arg. ✅
$dates
// before
$dates = [...]

// after
$casts = [... => 'datetime']

Filesystem

NullAdapter
// before
League\Flysystem\Adapter\NullAdapter

NullAdapter

// after
League\Flysystem\InMemory\InMemoryFilesystemAdapter

InMemoryFilesystemAdapter

// Before
League\Flysystem\Adpter\Local

Local

// after
League\Flysystem\Local\LocalFilesystemAdapter

LocalFilesystemAdapter

// before
new FilesystemAdapter(new Filesystem(new LocalAdapter($path)));

// after
new FilesystemAdapter(new Filesystem($adapter = new LocalAdapter($path)), $adapter);

Mailer

JSON:API

Attempt to use advanced steps for this, but likely not possible to do any automated upgrading. warn instead to read:

http://localhost:3000/extend/update-2_0#jsonapi

  • add before and after changes as an example (use the tags extension) ✅
  • create an api resource class for each type of serialized model ✅
  • Pre-fill the API resource with endpoints based on the current controllers ✅
  • Pre-fill the API resource with relationship declarations based on the Serializer ✅
  • auto insert the extender that registers the new api resource class ✅
  • add TODO comments on old classes (Serializers and Controllers) and old extenders, so the author knows to migrate their logic to the new api resource class. ✅

Search

  • Gambits (back to front) (produced js gambit based on pattern) ✅
// before
class TagFilterGambit extends AbstractRegexGambit implements FilterInterface
{
    use ValidateFilterTrait;

    /**
     * @var SlugManager
     */
    protected $slugger;

    public function __construct(SlugManager $slugger)
    {
        $this->slugger = $slugger;
    }

    protected function getGambitPattern()
    {
        return 'tag:(.+)';
    }

    protected function conditions(SearchState $search, array $matches, $negate)
    {
        ...
    }

    public function getFilterKey(): string
    {
        return 'tag';
    }

    public function filter(FilterState $filterState, $filterValue, bool $negate)
    {
        $this->constrain($filterState->getQuery(), $filterValue, $negate, $filterState->getActor());
    }

    protected function constrain(Builder $query, $rawSlugs, $negate, User $actor)
    {
        ...
    }
}

// after
class TagFilter implements FilterInterface
{
    use ValidateFilterTrait;

    /**
     * @var SlugManager
     */
    protected $slugger;

    public function __construct(SlugManager $slugger)
    {
        $this->slugger = $slugger;
    }

    public function getFilterKey(): string
    {
        return 'tag';
    }

    public function filter(FilterState $filterState, $filterValue, bool $negate)
    {
        $this->constrain($filterState->getQuery(), $filterValue, $negate, $filterState->getActor());
    }

    protected function constrain(Builder $query, $rawSlugs, $negate, User $actor)
    {
        ...
    }
}
export default class TagGambit extends KeyValueGambit {
  predicates = true;

  key(): string {
    return app.translator.trans('flarum-tags.lib.gambits.discussions.tag.key', {}, true);
  }

  hint(): string {
    return app.translator.trans('flarum-tags.lib.gambits.discussions.tag.hint', {}, true);
  }

  filterKey(): string {
    return 'tag';
  }

  gambitValueToFilterValue(value: string): string[] {
    return [value];
  }

  fromFilter(value: any, negate: boolean): string {
    let gambits = [];

    if (Array.isArray(value)) {
      gambits = value.map((value) => this.fromFilter(value.toString(), negate));
    } else {
      return `${negate ? '-' : ''}${this.key()}:${this.filterValueToGambitValue(value)}`;
    }

    return gambits.join(' ');
  }

  filterValueToGambitValue(value: string): string {
    return value;
  }
}
// before
(new Extend\Filter(PostFilterer::class))
	->addFilter(PostTagFilter::class),

(new Extend\Filter(DiscussionFilterer::class))
	->addFilter(TagFilterGambit::class)
	->addFilterMutator(HideHiddenTagsFromAllDiscussionsPage::class),

(new Extend\SimpleFlarumSearch(DiscussionSearcher::class))
	->addGambit(TagFilterGambit::class),

(new Extend\SimpleFlarumSearch(TagSearcher::class))
	->setFullTextGambit(FullTextGambit::class),

// after
(new Extend\SearchDriver(DatabaseSearchDriver::class))
	->addFilter(PostSearcher::class, PostTagFilter::class)
	->addFilter(DiscussionSearcher::class, TagFilter::class)
	->addMutator(DiscussionSearcher::class, HideHiddenTagsFromAllDiscussionsPage::class)
	->addSearcher(Tag::class, TagSearcher::class)
	->setFulltext(TagSearcher::class, FulltextFilter::class),
  • Flarum\Search\AbstractSearcher => Flarum\Search\Database\AbstractSearcher
  • Flarum\Filter\FilterState => Flarum\Search\SearchState
  • Flarum\Query => Flarum\Search
  • Flarum\Filter => Flarum\Search\Filter

LESS ✅

// before  
& when (@config-dark-mode) {  
  background: black;  
}  
  
// after  
[data-theme^=dark] & {  
  background: black;  
}
// before  
& when (@config-colored-header) {  
  background: black;  
}  
  
// after  
[data-colored-header^=true] & {  
  background: black;  
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant