-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
base: master
Are you sure you want to change the base?
Conversation
@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... |
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...) |
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. |
typings/xterm.d.ts
Outdated
/** | ||
* 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; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
xterm.js/src/browser/public/Terminal.ts
Line 25 in d81b25c
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.
private _onChange: IEmitter<IProgressState>; | ||
public onChange: IEvent<IProgressState>; | ||
|
||
constructor(protected readonly _emitterCtor: EmitterCtorType) { |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
}
}
There was a problem hiding this comment.
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);
There was a problem hiding this comment.
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);
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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)
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. |
@Tyriar Results so far:
in total: from 415 kB down to 192 kB |
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:import { ... } from 'shared/shared';
Currently I have only one testbed implemented in
ProgressAddon.ts
using theEmitterAddon
class. Should be the same withDisposableAddon
, 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.*
?)