-
-
Notifications
You must be signed in to change notification settings - Fork 20
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
Feedback requested: Refactor library for Vue v3 support #2
Conversation
Wherever possible, I have kept as close to the Vue v2 code as I could, but I was forced to make typing judgement calls in a few places that should probably be double-checked. The only exception to the hands-off approach is in the example app, which I took the liberty of porting to the Vue v3 composition API since I'd be touching most of the LOC in it anyway.
example/App.vue
Outdated
// Technically more fragile than the former type guard, but Vue 3 doesn't | ||
const { default: _, ...Icon } = Phosphor; | ||
const allIcons = Object.keys(Icon); |
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.
Pardon the truncated comment here. The gist is that some of the necessary types for the type guard are private in Vue 3, and rather than putting two more hours into sorting the mess out I just opted for simply omitting default
from the plugin import and assuming the remaining members are all icon components.
If this were something a user would be exposed to I'd solve the typing problem, but since this is only a demo site I didn't think it too much of a burden to keep in sync with any new exports from entry.ts
.
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, entry.ts
only registers the exported members of lib/index.ts
which are all icon components, so your guard is good for now.
Stellar to see a PR like this, thanks for putting in the time! On first glance it seems solid to me, but I have exactly zero experience with Vue3 and little enough with Vue2 to start. That being said, I'll give it a whirl on my end and see what I see. I'm not too bothered about relaxing the type checks in the example app, but I'd like to keep explicit types in components if it's doable in v3. Personally, types feel a bit clunky in Vue so I feel your decision to forge ahead for now. And yes, If you have ideas for improvement when it comes to making this more idiomatic Vue, please feel free to share. I am very much a React person writing what they think is a passable Vue port 😄. I've made a vue3 branch in this repo if you want to continue to work from there. |
@@ -87,7 +87,6 @@ function generateComponents() { | |||
:fill="displayColor" | |||
:transform="displayMirrored" | |||
v-bind="$attrs" | |||
v-on="$listeners" |
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.
Example the first... I've been told that in Vue you don't pass callbacks down, but it seems like that just means you need to manually make components emit every type of event anyone might want to hook into. This was my attempt at a clean solution. Is there something more idiomatic?
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.
For more complex components Vue gently recommends explicitly defining and distributing all component props and then turning on strict prop checking for the component, so anything extraneous errors(warns?) at runtime. This sort of use-case where the component is a thin wrapper around another element is one of the example exceptions though, and you’re not breaking any Vue conventions by doing it.
In Vue 3 the way listeners are handled has been streamlined, so all the native listeners are just on $attrs unless you explicitly emit a custom event with the same name.
This actually worked fine in Vue 3 unchanged but throws a deprecation warning.
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.
Nice to hear that's been changed.
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 was pleasantly surprised that the fix for the deprecation notice was just to delete the line and everything kept working as expected!
<span class="name">{{ icon.options.name }}</span> | ||
<div class="icons" :title="icon.options.name"> | ||
<component :is="icon.options.name" /> | ||
<div class="row" v-for="icon in icons" :key="icon"> |
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.
Do components get properly stringified in this case? If so, neat.
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 changed the assignment to icons
to an Object.keys
call, so it’s just the string names of the components. No magic here.
The demo app now globally loads the plug-in the same way a consumer app would, using app.use(PhosphorVue)
in the entry point, so there wasn’t a need to dynamically register all the component objects anymore and I just dropped the references to them.
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.
Gotcha, missed that the first time around! Does this give us tree-shaking as well, if you were to only app.use(SomeIcon)
?
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.
You couldn't app.use(SomeIcon)
, because the icons aren't Plugin
typed (i.e. they don't have an install
function), but you should be able to import individual icon components like import { PhFoo, PhBar, PhBaz } from 'phosphor-vue'
and that should theoretically get tree-shaken.
You could do it in the app entry point to globally register them like app.component(PhFoo)
if you expect to use it a lot of places, or you could import it in a specific SFC and locally register it there.
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.
^ that is assuming I configure Rollup properly to build anything at all 😆
Thanks for taking a look! I’d never written a Vue plugin before so it was a fun opportunity to dive into that side of things.
It’s definitely my intent to keep the components type-safe and as explicit as possible. The personal project I was working on is actually my first real foray into combining Vue and TS, since as you say it was pretty clunky in Vue v2 and my professional uses of Vue have therefore been plain JS in the past with TS reserved for backend code. It has definitely improved in Vue v3, I’ve found, but also relies on synthetic types for things like the component object returned by If I sort that out I can of course reinstate the actual type guard in the demo app too.
I won’t say I didn’t notice a little React-ness 😁, but everyone stumbles on Vue from a different background. I came from Ember so I definitely have “wait I have to do that myself?” moments where I just expect to assign something to a magic variable and have it automatically become a loading spinner or whatever.
Cool I’ve rebased the PR to that branch. |
Partially for the conciseness, partly because it allows explicit types in a Vue 3 friendly way
After quite a bit of puzzling, and fighting the TS language server to actually show me the full inferred type, I think I have puzzled it out. I opted to type the return value of const component: PhosphorIcon = defineComponent({
props: PropValidator,
setup(props: SetupIconProps) {
return { ...useDefaultPropsFromContext(props) };
}
}); seems much more readable and informative than export default defineComponent<
typeof PropValidator,
ToRefs<IconProps>,
unknown,
{},
{},
ComponentOptionsMixin,
ComponentOptionsMixin,
Record<string, any>,
string
>({
props: PropValidator,
setup(props: SetupIconProps) {
return { ...useDefaultPropsFromContext(props) };
}
}); and the type of the component is what we need elsewhere anyway. What I haven't quite figured out is the build. I don't know if I just fundamentally misunderstand TypeScript reexports, if I mangled something in the Rollup config, or if VSCode is caching/hiding something from me, but I can't for the life of me get the individual components to be accessible when loading the bundled library. I'd appreciate your help sorting that out if you get the chance, because it's probably something incredibly obvious that I'm missing. |
I like it! Just grokking over the refactor now, I'll see if I can troubleshoot the bundler issues. Had the previous commit working like a dream when I tested last night, btw. |
I should probably note that I haven’t pushed any of my build-system changes to this PR, so it definitely won’t build as-is. |
Component typings look great, if somewhat inscrutable -- thanks Vue! Not likely to be that useful for the end-user, but I like the security it buys for codegen and anyone wishing to extend. Also liking the streamlined context injection. As for the build setup, not sure I'll be of much use as I'm not familiar with Vue build tools and used a template to set this up for Vue 2. I did find this though: https://dev.to/shubhadip/vue-3-component-library-270p |
Actually getting it to output JS bundles is easy enough, the issue is in the types. Some of my confusion was created by the fact VSCode apparently only reads Nothing I've tried has managed to generate a single declarations file with all the necessary exports and types listed. In fact, none even have the named exports at all, as though
|
The result doesn't have proper type definitions though, so it's not "working" per se
This patch fixes rendering bugs with icons using strokes for dot elements, whereby using a stroke weight larger than the circle radius caused unexpected clipping. We reverted to using ellipse elements for all cases.
Follow vuejs/core#2687 for status
This is a result of specifying a Prettier version in devDeps, which uses a newer version of Prettier with slightly different defaults
This provides the appropriate types with the built package, but continues to be suboptimal and super non-DRY. I haven't yet found a way to avoid explicitly declaring every named export, unfortunately.
I haven't abandoned this! It's currently working except for tree-shaking, though I'm still not happy with the solution for outputting types (I'm generating |
Thank Ben, and apologies I haven't had much of a chance to work look at this either. My solution for tree-shaking in the React lib was to set |
Note: this tree-shakes, but only mostly due to Webpack 4 struggling with "double-barrel" structures like we have with `index.ts` and `entry.ts`. Importing a single icon from Phosphor-Vue is not a full 2.5MB anymore, but there's still a bunch of unnecessary code included (it *does* seem to purge the unused templates which is the bulk of the library size). Webpack 5 apparently handles double-barrels better, so we should see that problem disappear once vue-cli ships Webpack 5 support and clients start using it (track vuejs/vue-cli#5611)
No worries, it's a busy time of year. The tip on I'll get this branch up to speed with |
Beautiful! Tested in dev and prod builds and everything seems to be in order, tree-shaking included. Merging this now. Will publish as Thanks for the efforts, glad to have you here. |
Oh, does documentation need to be updated? No longer need to specify EDIT: nope, nevermind. Still required, we just call |
RE: publishing, the loose standard for Vue 2/3 support seems to be publishing the Vue 3 version under a suitable major version bump (where possible the Vue 3 version under 3.x, but obviously that’s dependent on the library’s own versioning matching up), then marking the latest release of that major version with the @next tag. That way users get the latest Vue 2.x compatible versions with ‘phosphor-vue’, and can install the Vue 3.x version with e.g. ‘phosphor-vue@next’ |
I guess now you mention it I've seen that pattern, but not normally applied to parallel releases/architectures. How would one go about getting a specific version of the vue3 lib in that case, should they need to? |
You'd just specify the version range as normal, hence why it's necessarily accompanied by a major version bump in addition to the tags. I've seen a couple libraries actually bump the major numbers a couple numbers to leave room for an additional Vue 2 compatible major release, but where possible they seem to be sticking to 2.x and 3.x. For example, Vue itself is set up this way. Running 1 Come to think of it, that version is wrong; should probably be |
NOTE: this should not actually be merged into master. Vue v3 and Vue v2 are API-incompatible and support for them can't easily coexist in the same branch/release, but GitHub won't let me submit a PR targeting a new upstream branch.
I stumbled upon Phosphor the other day, and being such a lovely little icon set I was quick to drop it into a personal project. Unfortunately my project is written in the recently-released Vue v3, which has several breaking changes from Vue v2, and plugins for older versions of Vue will not work. Rather than opening an issue asking for an update on a project with active commits today (well, one of the other Phosphor repos), I figured I'd dive in and take a stab at the bulk of the porting work.
Wherever possible, I have kept as close to the Vue v2 code as I could, but I was forced to make typing judgement calls (by which I mean "letting the compiler infer the types") in a few places that should probably be double-checked and corrected. The only exception to the hands-off approach is in the example app, which I took the liberty of porting to the Vue v3 composition API since I ended up having to touch most of the LOC in it anyway.
I still need to do some more in-depth testing, but the unscientific method of "the demo site works" has shown positive results so I figured I'd submit this sooner rather than later.