Okay. I guess I can start. Thank you very much for being here. Thanks also to the folks
organizing the Dev Room and the event. My name is Joakim. I work for Microsoft and I'm
here to talk to you about some stuff that we found while building a UI project, a JavaScript
UI project. And maybe, yeah, maybe it's helpful for you too. All right. So this is kind of
a high-level presentation. And I'm not here to tell you how to do stuff. Certainly, certain
things will be very basic or so. Others, hopefully, will also disagree. But the idea is to tell
you about the patterns that we found, which hopefully, if you want to do something like
this, you will already be, you know, aware of these things. So just to set up the context
of what I'm talking about, and I should have started the timer. Yeah, you know, when we
say like, plug-in system, we're talking about applications like, for example, ones you know
probably matter most or VS code. They have extensions or plug-ins, if you will. I usually
may just say plug-ins, but essentially the same thing. And the talk is not about the
product that we do, but just to give you an idea of, you know, the context. This is a
Kubernetes UI. It's built with React. And there's a server, you know, or backend and
a front-end. So it's very traditional in that sense. And you can run it as a web app or as
a desktop app. So, yeah, and when I talk about plug-ins, still in the context thing, what
this means is that, of course, it's code that should be loaded dynamically. It has an API
from a library, of course, for you to change stuff. And in our case, this is used basically
for changing, you know, the UI, most of it. But you can also change certain core things
like adding routes, deleting routes, I don't know, changing the token when you need to
get a token, stuff like that, right? But this is essentially the context of what I'm talking
about when I say plug-ins and functionality. Okay, so let's start by looking at what the
plug-in should look like. This is usually, we're talking about this bundled single JS
file, right? Let's not talk about, like, several JS files, you know, the drill, hopefully.
But so you have the plug-in code, maybe that's enough for us. It's been enough for a while.
But likely you will need some information together with the code, right? That's often
called a manifest. Certain plug-in systems that I worked before, it was kind of programmatic.
So the plug-in itself declares, here's my name, here's my, I don't know, dependencies
for other plug-ins or whatnot. We recommend not doing that. We recommend using a text
file, or where you declare like a manifest file, where you have all that information
about the plug-in. Turns out that, you know, package JSON is pretty good for that already.
So the advantages of not having this in the file, hopefully they're obvious, but the thing
is that you don't have to load the code before you know if you should load the code, right?
So if you have metadata that you need to use to decide whether to use the code, for example,
you want to tell the user, okay, here's the name of the plug-in. Do you want to enable
this plug-in? So it's better that the code is not already running.
Loading and loading plug-ins. So, you know, this is coming into what the plug-in should
be structured like. But of course, you have to load the plug-ins dynamically. And usually
there is this pattern of an activate method. This activate, of course, is about telling
the plug-in developer more or less when the code should be, you know, loaded. So that's
the sole purpose of this, right? This is not so much for anything like, okay, if you don't
put code inside the activate, that means you're not one. Don't trust that, right? But it also
can be used to tell the system that when you try to activate the plug-in, then you can
have, like, a return, for example, from that activate method. And the plug-in can tell
the system, I could not activate, right? We'll see a couple of examples.
Without the activate, and our plug-ins, for a while, also didn't have the, like, well,
you can use them without the activate, but that's because you should assume that the
whole loading is deactivate, right? So both methods are fine. Like I said, this is, in
many cases, a matter of taste, but this is, like, a pattern. So if you have an activate,
of course, should you have a deactivate? This is when you tell the plug-in, okay, you're
going away. Maybe there's certain things that the plug-in needs to do before it goes away,
like cleaning up. This is unlikely to be used by most plug-ins, but the thing is that, like,
you shouldn't rely on deactivate for just when the, like, you shouldn't trust that the
plug-in will only run the code inside the activate. You should also not trust the plug-in
to deactivate itself, right? So this is mostly about telling the plug-in developers, here's
a way that you know when the code is going to be activated and deactivated. Yeah, for
unloading, of course, you should just reload the system without the plug-in, mostly. And
that's pretty much the conclusion of the section, right? Don't trust plug-ins code, I guess.
Now going into the structure, and hopefully this is a bit more interesting now. Yeah,
so you have to decide how can people write plug-ins, right? And since we are extending
functionality and, you know, extending something that you already know how it will work, maybe
a plug-in class sounds good, like an object oriented where you extend stuff, sounds good.
But of course, right now we have a lot of functional code going on, so ultimately this
is a matter of taste, right? We can argue, like, what's fastest, what's quicker to do,
but in such applications, it's fine if you're not squeezing that extra 200 mil. So, yeah,
so these plug-ins do exactly the same. For example, in this case, the plug-ins would
say, okay, we only work on Mondays, so if the, you know, if the day is not on Monday,
then you just tell the system, I'm not activating, but these are different flavors. However,
maybe an interesting plot twist would be that, of course, plug-ins have a life cycle, right?
They activate, they deactivate, so that's what React kind of gives you for a component.
So why not making a plug-in be a React component, right? You already have, you know, certain
life cycle things, like use effect, for example. Another advantage is that you could also use
hooks inside it. So we, for example, in our system, have some hooks, and if you just use
the, this method, it's going to be a bit complicated to do, right? But if you use this, then probably
it's a good idea. And if you, of course, this only works if you have a React-based system,
the other systems, I don't know, but yeah.
But then the other thing about the functionality is, okay, how about implementing actual stuff
to change the system, right? And in here, I think there are mainly two options, so you
can make it so that it's very textual and declarative. So let's say that you have a
top bar, we do have a top bar, and you want actions. Actions would be like a button or
a string or something else that you want to put there, right? So if you make it declarative,
like the left example there, of course, the system is responsible for interpreting whatever
you put there, so as long as you don't implement stuff, you won't give any power to that plug-in
implementation. On the other hand, this is kind of a bit limiting, right? So because
you always have to develop more and more functionality to support it. If you want to
be a bit more flexible, although not as, I guess, friendly to new developers, you can
just say, okay, just put a component here and that's it, right? And it can be a string
or something else. So that's the right side. And of course, maybe people will do stuff
that you're not expecting with that, but also you basically support anything. So it depends
on the level of what you really want to give. Now, the functionality itself, like I won't
bother you with the functionality that we have, but of course, you will usually say,
okay, I want people to add, again, top bar actions or, I don't know, like a new route
and all that, and it will be likely that you also have to have a way to remove those, right?
So you can add an action. Somebody will say, okay, now I need to remove it if it's not
Monday or something, right? So it's some sort of crude or crud or whatever you call it.
So let's look at what it could look like. So in our case, now I talk about header actions
and I actually put this screenshot here just to illustrate, but this is a header action,
it's just a header with an action. And yeah, so if we want to support something like this,
should you have one function per operation, like register header action like we have here?
So you declare the button and you do it or maybe, okay, if you add one button, maybe
you can add a list of buttons, just keeps getting appended there. Sounds great. And
then you have a counterpart for the operation that's removing header actions. And in this
case, you can just call it deregister or just to be the opposite or remove, to be a bit
more direct. However, how can you actually identify what you added there, right? So if
you declare the component or the function in this case on the left, then you have access
to it. So you just call it again, hopefully it will equal to the same thing internally
and the system can understand, okay, this is something that we have here. So let me remove
that. But let's say that we already have default actions. So how can the user refer to the
default actions, right? Will they import them? That sounds like they cannot really refer
to the actions by the function name because it gets minimized and then things don't work.
So of course, one solution is to add IDs. So it's probably a good idea that whenever
you have a function where you're just passing a component or something else, probably you
should identify it, right? If you are to refer to it later. But then you're very happy it
works and somebody will go and say, hey, that's cool, but you keep appending the actions.
I want to actually have my actions prepended. And then your world goes upside down, right?
So either you can add, of course, like, I don't know, like an index to the function
when you call it. So now you have ID, index and the actual action. Or you can scrape
all that and, for example, just use a list processor. So instead of registering, okay,
add function, remove function and all that, you can say, here's my list processor for
other actions, right? It's going to be fed whatever is the default actions and you can
add them, you can remove them, you can shuffle them, whatever. So it, of course, you have
to identify them as well. So the ID keeps going there. But this is, I think, a more
flexible way and less work to maintain. Now, yeah, developer experience. So it's supposedly
important that users can start plugins and develop plugins easily for your system. So
just like other programs that you probably used before, there should be, like, this boot
strap way of creating a plugin, right? Either that or, of course, you have a folder of examples
that you say, okay, just use this and modify it. We do have something like a bootstrap
script, like the headland plugin, it's called, because we're original. And, yeah, and that's
interesting because you can just generate the base plugin. But also, you should take
into account that you should require the developers to configure as less stuff as possible, right?
So one way would be to say, okay, here's the package JSON that we generated, of course.
Here's the TS config that we generated. Here's the web pack configuration that we have and
all that. However, of course, the next time that you need to upgrade the plugins, then
you have to upgrade all that stuff. So a probably better idea is to try to add as least stuff
as possible, right? So if, for example, in the case of TS config, instead of just shipping
the whole TS config and then you have to figure out how to upgrade that if you need, we are
already, as a module, we are already inside the application at the development stage.
So we just point to it, right? We ship the TS config that we want and we point to it.
So if the developer touches it, then it's fine. We're never touching that again. We're
just touching the file that we ship, right? So it's going to be updated automatically
as long as you update the module. So actually, we try to keep the dependencies as simple
as possible. In this case, it would be just to have one, right? Our headland plugin package
in this case. All right. Next is about bundling. So now you have your API with the processing,
this processing and stuff. Yeah. And Webpack is very easy to use. Yeah. So you get your
bundle, right? And you get your bundle to be, you know, you get your single JavaScript file
to be run, ready to be run. But of course, that's going to, if you just have like React,
for example, that this plugin will import, then you get React bundle there. If you have
your, in our case, headland plugin, then you're going to have that library, you know, just
bundled in. And you should try to avoid that, right? Because it's going to be running inside
a system that already has this, it should run with the same versions, and it should
not, you know, just even if it's for size matters, not pack the same thing. So Webpack
has this thing called external modules, and you can just say, okay, whenever you find
this import, actually, we mean this variable. So when it finds the React router, it says
don't care about the React router, just use whatever you put there. In our case, it's
like plug-in lib, React router, and it's going to use the same code. So we were like, okay,
this is great. We can avoid shipping all this stuff. We were going to keep our headland
plugin package really lean. It's going to be great. So we even thought, okay, we're not
even shipping our own library. We're just shipping the type declarations. That's going
to be fine. So we spent many hours wiring, you know, TS config and Webpack and whatnot
to make sure that it was happy when users are developing. They can see that the imports
seem to work, even though they don't exist inside the library. And then somebody wanted
to test the plug-in. And they're like, oh, okay, so now you cannot test the plug-in because
you don't have the libraries around. So maybe you have to use the program itself to test
the plug-in, but that's probably not a great idea. So we were like, okay, we have to ship
the actual library. Yeah. And so, yeah, so it still works as an external module. So we're
not bundling it, but we're shipping it. And it's fine because, I mean, yes, the headland
plugin package is slightly larger, but that's okay because it's just one time. So take that
into account and don't get too extreme with not shipping stuff. Yeah, we're getting to
the end. So running the plug-ins, right? So now you got your bundle. It's not bundling
React because we got it. It's not bundling your library because you got it. And you're
going to run it. However, of course, at some point you will break API. And if you do break
API, it would be nice that you don't load a plug-in that will be broken because it's
expanded. You'll break the system for your users. So package.json has this already. It's
part of its known keywords, I guess, called engines. So you can just put something there
for your system. And then, of course, when you run it, you should just check for that
before running it. Now, how to run the actual system? So now you have everything in place
and you can check for it before you load it. But how do you actually load it? Do you load
it? Of course, it's going to be loaded in the front and that's what we're talking about
with JavaScript. And this is highly special to each project, right? So maybe you have
something there where the users can just load the files directly and it just refreshes and
loads. In our case, we didn't want to do that. We wanted it to be very transparent to the
user. So if the user, of course, downloads a plug-in, next time they start the application,
they should see that things changed. But also, in the case of our application, it works also
if you deploy it as a web service like that. So in that case, we don't want really the
users to keep using different plug-ins. In this case, it's more like, okay, whoever deployed
it is giving you an user experience with the base code plus a bunch of plug-ins that the
users shouldn't really know about. And for security reasons, of course, we don't want
the users to keep loading plug-ins on something that is now, even though it runs locally,
it's going to, you have it just deployed for this user and other users. So they would
have different user experiences if they keep adding their own plug-ins. So what we do in
our case is that we have the backend or the server. It's a server, but depending on if
it's running locally or actually in a server. And then that's the thing that has to have
access to the plug-ins themselves. So it reads the plug-ins and then it has an endpoint
and the front end, when it loads, before it loads everything, it says, okay, what are
the plug-ins that you have? It says, I got this 10 plug-ins. It says, okay, give me the
plug-ins now or give me this one plug-in now, for example. Then you get the actual bundle
JS code from the backend, and then it loads it dynamically, and then you get a shiny thing.
So this way, of course, if you're running it locally and you have the plug-ins in the
plug-in folder that it expects, the backend has access, it actually keeps watching the
folder in our case. It gets refreshed whenever you change stuff there, and that's how we
do it locally. If you're running on the server, then, of course, we don't check if the plug-ins
change or not. That's not supposed to happen, but you still get essentially the same experience.
But like I said, this is mostly, this is very tied to each project. And that's all I got.
So thank you. Thank you very much. We do have time for questions, so raise your hand high
and we'll start with the first question. You said putting some dependencies in the plug-in,
so having a module that they import so they can use some things like hooks. Do you have
to, or do you do anything against a plug-in modifying those things that would then mess
with other plug-ins, like changing the objects you've passed in? I cannot understand what
you're saying. So if the plug-ins are depending on a module that you've made, and they're
the same ones being passed into different plug-ins, could they modify the things you're
passing into, then mess with the other plug-ins? Yes, but what was the actual question? Is
there a way to mitigate against them changing how the plug-in system behaves for the other
plug-ins? But you mean for example in the example of the actions, whatever goes there?
So you had a button, I say you have a button class you're passing in, they can extend.
What if they changed the behavior of that button class to then other plug-ins have a
modified version when running? Yeah, but that's actually something by design, right? So you're
supposed to, let's say that you changed the delete button and now you still have the delete
button, but the delete button will no longer delete. It will just say, actually it wasn't
on the example, but it will just say not today, right? So the delete button on the left just
says not today, right? It actually replaces the delete button in this case, but that's
fine, right? Because that's what the plug-ins are supposed to do, right? So the plug-ins
are supposed to do, and maybe you have even plug-ins that, okay, they expect you already
to have other things in there, so if you have a combination of both plug-ins they can see
that you added stuff, and yeah, so that's by design. Of course if you install plug-ins
that will make your system not do anything, well that's also, you should be careful about
what you install. You talk about security in the front-end, but
isn't that something that the back-end should handle more and just keep the JavaScript as
light as possible instead? About the security, what?
You talk about security in the front-end, so that users can't add their own plug-ins,
but isn't that something that the user would be responsible for anyway?
No, no, yeah, maybe, I mean I was rushing maybe I didn't explain that correctly. No,
the thing about the security is not so much about the security, it's about the user experience.
So you suppose as a user to add your own plug-ins of course, but that's if you use in our case
our application as a desktop application, because then it's you who is responsible for
that application. When you go and you use it because you access some service that gives
you in the browser, then it's the person that deployed that or the company or whatever that
is supposed to give you the plug-ins that you are supposed to see, so you shouldn't
change the way that the application works, but that's of course our decision, right?
In other cases like a guest lag or something like that, you can add actually different
plug-ins for yourself, and that's cool too, but this is like I said, this was a highly
intimate decision for our own project. So a couple of months back when we were
checking for the plug-ins, so usually a few applications run the plug-ins in an isolated
environment, like they ship their micro runtimes and run in them and then try to communicate.
So in your use case, are you running them in the parent application context? Because
in that case, we can't always trust what users are writing in their plug-ins, right?
So they can steal stuff from Windows, things like that. So do you have any check to see
all the plug-ins and do that due diligence before I load them to the store or something
like that? Yeah, security would be a whole talk about it.
Which we don't have time for? We're just running the plug-ins as is because
as of now, you know, we don't have, for example, you cannot just download plug-ins from NPM
right now, right? We're going to have that. When we have that, we're going to have a different
way to run them, hopefully. I know that depending on the system, you're going to find that some
people do have a way to isolate them. There's a good blog article by Figma doing that.
And that's kind of cool that you say what approaches they took and what conclusion they
got to. Other, you know, other programs, they just say, okay, you're supposed to install
stuff that you trust and they go through some, you know, just like when you install an NPM
package, it can be harmful, right? But there are mechanisms to kind of mitigate that.
So I want to make it as secure as possible, but that's not, you know, it was not security
from the start before we actually have the system.
Do you have any ways of handling code splitting and other stuff like that? Maybe a plug-in
wants to load some components later on. Is there a way you can handle it using your method
of doing that? If a plug-in wants to add components?
If a plug-in developer wants to use code splitting and loading stuff later on, is it fine?
I mean, if you have an active method, I mean, if I understand your question, you have the
moment where the plug-ins are loaded, right? So you can just say, okay, I'm not supposed
to be, I'm not supposed to be running the buttons, nouns, or changing the functionality
now. I'm supposed to be changing the functionality whenever. Of course, that's a responsibility
for the plug-ins, right? We just say, okay, we're loading you. Now you should make sure
that you do whatever you want. But it should be like, you can, of course, this is just
code. You can change when it wants, right?
Thank you again.