Skip to content
New issue

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

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

Already on GitHub? # to your account

Expose key helpers on the API for addons #5292

Draft
wants to merge 16 commits into
base: master
Choose a base branch
from

Conversation

jerch
Copy link
Member

@jerch jerch commented Jan 10, 2025

Attempt to fix #5283.

@Tyriar

The addon stubs under /shared only add ~500 bytes on the xtermjs package, and also on addons that derive their addon class from it. Only inconvenience so far is the fact, how we synthesize the final xtermjs package, which leads to an import switch:

  • addons in xtermjs repo have to use import { ... } from 'shared/shared';
  • 3rd party addons can import the shared classes from xterm package

Currently I have only one testbed implemented in ProgressAddon.ts using the EmitterAddon class. Should be the same with DisposableAddon, but I didnt want to change too much in code before getting some feedback from you. So plz have a look at the idea and whether it goes into the right direction. Also we kinda need to cut type ropes for the vs objects, so plz also see the added types in d.ts - we prolly should keep them as small as possible while still being useful.

And last but not least the question, whether we should put these additions under a separate name in the API (maybe shared.*?)

@jerch
Copy link
Member Author

jerch commented Jan 10, 2025

@Tyriar Ah well, still not there - tried to simplify the imports/exports, which again led to pulling vs stuff into addon 😊 Need to fix that again...

@jerch
Copy link
Member Author

jerch commented Jan 10, 2025

Well treeshaking is not working with webpack, a 3rd party addon would still include the whole xterm.js lib. Needs a separate shared.js output in the final xterm.js (not gonna fix that today...)

@Tyriar
Copy link
Member

Tyriar commented Jan 10, 2025

3rd party addons can import the shared classes from xterm package
3rd party addon would still include the whole xterm.js lib.

I wouldn't worry about 3rd party libs using our helper. They will have access to Emitter and Event via the API so they can make the tiny wrappers on their end if they need them. Not worth it imo.

webpack.config.js Outdated Show resolved Hide resolved
Comment on lines 1961 to 1974
/**
* Get Emitter constructor.
*/
export const emitterCtor: EmitterCtorType;

/**
* Get DisposableStore contructor.
*/
export const disposableStoreCtor: DisposableStoreCtorType;

/**
* Turn a function into a Disposable.
*/
export const toDisposable: (fn: () => void) => IDisposable;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should be able to export class here, similar to export class Terminal. That way we don't eed to deal with this CtorType stuff

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't that pulling types from internal sources into the public API? My idea here was to decouple that with minimal stub types, so linter / type inspection doesn't rely on internal stuff.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't that pulling types from internal sources into the public API?

Everything in the API needs to be standalone, so we'd duplicate it there.

Also, we depend the other way for Terminal here to ensure our implementation matches the API:

export class Terminal extends Disposable implements ITerminalApi {

So we could do the same for DisposableStore/Emitter/etc. by depending on xterm.d.ts from public/Terminal.ts again.

typings/xterm.d.ts Outdated Show resolved Hide resolved
src/shared/shared.ts Outdated Show resolved Hide resolved
private _onChange: IEmitter<IProgressState>;
public onChange: IEvent<IProgressState>;

constructor(protected readonly _emitterCtor: EmitterCtorType) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it's nicest to just pass in the whole xterm API object to some addons? I think we can do this without error:

import type * as XtermApi from '@xterm/xterm';
import type { Terminal, ITerminalAddon, IDisposable } from '@xterm/xterm';

That way it would be nicer from the embedder side:

new ProgressAddon(xterm)
new WebglAddon(xterm)
etc.

Looks better than this imo:

new ProgressAddon(AddonDisposable)
new WebglAddon(AddonDisposable)

Copy link
Member Author

@jerch jerch Jan 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, thats a good idea. The dispoable + emitter ctors, if both are needed, already make this cumbersome and looking ugly. With the whole exports as one arguments it gets much nicer and easier to comprehend on caller side.
I even wonder if we should make that the first default argument on all addon ctors, this way ppl wont get it wrong on certain addons, just apply it on all (well thats a major API shift).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well the full module export type interface stubbing felt kinda wrong. My next approach looks like that:

on addon impl side:

import { ..., ISharedExports } from '@xterm/xterm';

export class AddonXY implements ... {

  constructor(sharedExports: ISharedExports) {
    // do something with things exposed under sharedExports
  }

on embedding side:

import type { Terminal, sharedExports } from '@xterm/xterm';
import { AddonXY } from '@term/addon-xy';

const term = new Terminal(...);
const addonXY = new AddonXY(sharedExports);

private _onChange: IEmitter<IProgressState>;
public onChange: IEvent<IProgressState>;

constructor(protected readonly _emitterCtor: EmitterCtorType) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In retrospect it's obvious this would be a breaking change, but that's fine and worth it considering the wins we get in bundle size.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

About breaking change - as I wrote above, I wonder whether to make the xterm exports the first ctor argument for all addons, e.g.:

import * as XtermApi from '@xterm/xterm';
import { AddonXY } from '@term/addon-xy';

const term = XtermApi.Terminal(...);
const addonXY = new AddonXY(XtermApi);

Then an addon ctor is free to use the exported ctors there or to ignore that argument:

import type * as XtermApi from '@xterm/xterm';
class AddonXY extends ... {
  contructor(xterm: XtermApi, other_args) {
    // if addon needs an event:
    this._onWhatever = xterm.Emitter<Whatever>();
    ...
    // else: just ignore xterm argument
  }
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or if thats too "globalish" looking, we could also aggregate the extra exports under a shared API endpoint:

import { Terminal, shared } from '@xterm/xterm';
import { AddonXY } from '@term/addon-xy';

const term = new Terminal(...);
const addonXY = new AddonXY(shared);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And last but not least - we could also keep Terminal the only exported impl endpoint and put the shared stuff on the terminal class instead:

import { Terminal } from '@xterm/xterm';
import { AddonXY } from '@term/addon-xy';

const term = new Terminal(...);
const addonXY = new AddonXY(Terminal.shared);

Copy link
Member Author

@jerch jerch Jan 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The latter has a few advantages, like keeping stuff under the Terminal umbrella and automatically gaining access to those symbols even on a terminal instance.

Edit: Tbh the ctor argument idea raises in fact the question, why not to load addons this way in the first place with a terminal instance as first argument. Do you remember, why we have the loadAddon mechanics on the terminal the way it is implemented currently?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not to load addons this way in the first place with a terminal instance as first argument

Terminal has always been the only thing exposed on the API, but it makes a lot of sense to pass in the full API when we start adding new things. This sort of breaking change is more impactful though as we can't just expect all addon ctors to have it as the first arg.

Do you remember, why we have the loadAddon mechanics on the terminal the way it is implemented currently?

Was a long time ago, but one of the big things we get is loadAddon lets the Terminal take ownership of it. So disposing of a terminal means the addon will be destroyed. That's pretty much all AddonManager does.

Copy link
Member Author

@jerch jerch Jan 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it def. smells like quite the big API change would be needed, so idk exactly how to proceed. Maybe we should go back to conceptual structuring before inventing a square wheel here? Gonna try to do a write up of what we have currently vs. what could be done about it in #5283.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posted long replies in #5283 (comment)

@Tyriar Tyriar changed the title API for addons Expose key helpers on the API for addons Jan 10, 2025
@Tyriar Tyriar added this to the 6.0.0 milestone Jan 10, 2025
@jerch
Copy link
Member Author

jerch commented Jan 11, 2025

I wouldn't worry about 3rd party libs using our helper. They will have access to Emitter and Event via the API so they can make the tiny wrappers on their end if they need them. Not worth it imo.

True. So lets treat the shared stuff as internal helpers only, and maybe place some recipes for event or dispoable usage in the docs for external folks. Both can be done easily with composition after we added their ctors to the API.

@jerch
Copy link
Member Author

jerch commented Jan 12, 2025

@Tyriar Results so far:

  • image addon: from ~80 kB down to ~54 kB
  • progress addon: from ~45 kB down to ~2 kB
  • search addon: from ~50kB down to ~11 kB
  • webgl addon: from ~240 kB down to ~125 kB

in total: from 415 kB down to 192 kB

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Expose important pieces of vs folder on API
2 participants