generated from unplugin/unplugin-starter
-
-
Notifications
You must be signed in to change notification settings - Fork 2
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
[RFC] feat: create defineArgs
initial implementation
#73
Draft
floroz
wants to merge
1
commit into
tobiasdiez:main
Choose a base branch
from
floroz:feat/support-args
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
import { describe, expect, it } from 'vitest' | ||
import { compileScript, parse as parseSFC } from 'vue/compiler-sfc' | ||
import { extractDefineArgs, parse } from '../parser' | ||
|
||
describe('extractDefineArgs', () => { | ||
// Question: `defineArgs` is a fake "macro" we created. Could we actually | ||
// implement it as a macro, so that it supports generics to allow | ||
// for `defineArgs<typeof Button>() and then infer the type of the args | ||
// with autocomplete for props and slots? | ||
it('should extract args', () => { | ||
const { descriptor } = parseSFC(` | ||
<script setup> | ||
defineArgs({foo: 'bar'}) | ||
</script> | ||
<template> | ||
<h1>Test</h1> | ||
</template> | ||
`) | ||
|
||
const resolved = compileScript(descriptor, { id: 'test' }) | ||
|
||
expect(extractDefineArgs(resolved)).toMatchObject({ | ||
args: { | ||
foo: 'bar', | ||
}, | ||
}) | ||
}) | ||
|
||
|
||
}) | ||
|
||
describe('parse', () => { | ||
it('should parse the args', () => { | ||
const { args } = parse(` | ||
<script setup> | ||
defineArgs({foo: 'bar'}) | ||
</script> | ||
<template> | ||
<Stories> | ||
<Story id="test" title="test" template="<h1>test</h1>"></Story> | ||
</Stories> | ||
</template> | ||
`) | ||
expect(args).toMatchObject({ | ||
args: { | ||
foo: 'bar', | ||
}, | ||
}) | ||
}) | ||
|
||
it("should expose the args to the template", () => { | ||
const { args } = parse(` | ||
<script setup> | ||
const args = defineArgs({default: 'hello world'}) | ||
</script> | ||
<template> | ||
<Stories> | ||
<Story id="test" title="test"> | ||
<b-button>{{ args.default }}</b-button> | ||
</Story> | ||
</Stories> | ||
</template> | ||
`) | ||
expect(args.default).toBe('hello world') | ||
}) | ||
|
||
it("should parse the args from the template and allow for local story overrides", () => { | ||
const { stories } = parse(` | ||
<script setup> | ||
const args = defineArgs({default: 'hello world'}) | ||
</script> | ||
<template> | ||
<Stories> | ||
<Story id="test" title="test" :args="{default: 'foo bar'}"> | ||
<button>{{ args.default }}</button> | ||
</Story> | ||
</Stories> | ||
</template> | ||
`) | ||
|
||
// expect the args to be overwritten | ||
// TODO: test that the template is compiled to <button>foo bar</button> | ||
// right now it is <button>{{ args.default }}</button> | ||
// is it correct? at which stage should `args` be compiled into their actual values? | ||
expect(stories[0].template).toContain('foo bar') | ||
}) | ||
}) | ||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
@tobiasdiez this is probably the first important point to discuss.
I do like that the experience for clients would feel extremely close to the native Storybook renderer, where you have the
render(args) { return <Story /> }
whereargs
is an object available within the context of the Stories.The difference here would be that our
args
will be within a global scope of the file, but would assume different values based on the Story in which its used.The vue approach of such a feature, would be a scoped slot otherwise:
What do you think? The first is closer to Storybook but would involved blackbox magic, the latter is maybe more verbose but closer to Vue's architecture.
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.
One problem I see with the global args definition, is that one naturally would expect something like
to work. But this seems hard to implement - how is the story knowing which of the
argsOne/Two
it is supposed to overwrite?One the other hand, I really like the scoped slots version which by definition provides local
args
for each story. I would only rename theargs
prop of the story in your example toinitial-args
(or default?) to make the purpose a bit clearer.Another small point: Maybe we can pass the args directly as the slot argument, so that you can conveniently destruct it as in the following example:
According to https://github.com/storybookjs/storybook/blob/d8c5bbae3f40b2f1293c210e220522a6fbbf87bd/code/renderers/vue3/src/public-types.ts#L48C17-L48C17 and https://github.com/ComponentDriven/csf/blob/4c735fea4f0c9605b93497238303cb4ab9304727/src/story.ts#L170-L173, a story render function can only accept
args
andcontext
, so what we would loose with the above is thecontext
variable. Maybe that can be supported by prefixing it with a dollar sign?But its also not a big pain to have
v-slot
emit{ args, context }
as the default storybook renderer.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.
Args architecture
I do agree on the scoped slot preference. It's also much closer to the Vue's native experience and removes a layer of "magic" from final users.
I wouldn't destructure the
args
object since it allows to keep the same render function signature ofargs, context
, but also allows to quickly understand in the template which variables are coming from theargs
, and which from the global script context.TypeScript support
What about TypeScript support? Would there be a way for clients to declare their component's type and have
args
IDE autocompletion for props and slots?Where should that declaration occur? in the
defineArgs
? I think it makes sense to keepdefineArgs
instead ofinitial-args
prop since it removes the need to also come up with a different convention for the pro naming on the story.Design proposal
defineArgs
as call site to provide type inferencedefineArgs
sets the defaultargs
as theMeta
in Storybook. We will follow the same approach forargTypes
(defineArgTypes
) andparameter
(defineParamaters
).defineArgs
support for TypeScript would be an important feature to keep the experience in using the addon onthe same level as the Story CSF.
Local override using
:args
prop on the<Story />
synthetic templateWe use
:args
on the local Story component to provide the local overrideExpose
args
to the template via scoped slot on<Story />
synthetic templateShould it be the
default
slot? should we used a name slot like#render
since its meant to replicate therender
function in CSF?Sketch of final usage example from clients
Next steps
If happy with the design, the next step should be to create a PR to have a working implementation (without TypeScript support) of
defineArgs
,:args
property andv-slot="{args}"
.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.
Very good, I agree with all your points. Only question I still have is how the
defineArgs
would provide the types. I don't think this is possible with the current implementation of volar.Instead, I would introduce a macro
defineArgTypes
and pass the result of that call to each story (ideally it would suffices to pass it once to theStories
component, but I don't think type interference for child components is implemented in volar, see #74). So the result would be something like:In addition we could provide a
defineArgs
macro that provides a convenient way to define args in thescript
block without loosing intellisense etc, but I would leave it for a follow-up PR.What do you think?
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.
Linking the macro to the
Stories
to spread the type inference is definitely a great idea.The issue I see is that if we want to leverage
defineArgTypes
as the function to take the generic, we are forcing clients always to defineargTypes
even when not required.At this point, since the type inference is essential for clients and we need to provide good ergonomics to declare types/consume them on
Stories
, I would suggest following the Storybook CSF's structure and creating adefineMeta
macro.Whenever a Story CSF is created, all clients must create a
meta: Meta<typeof Component>
object, whether they need to explicitly declareargs
,argTypes
, etc., or not.The two challenges here are:
Can the
meta
type passed ontoStories
provide type information toStory
so that it can provide inference for theStory
:args
prop?If not, then we would need to connect the
meta
on<Story :meta="meta" />
for each Story to provide the type inference. That could lead to some confusion due to different concepts of Meta in CSF. I wonder if, at that point, we could use the new Vue generics syntax on theStory
.Looking at this usage
I also wonder, do we need to implement the complexity of local
:args
prop overriding the one coming from themeta
?A client who wants a local override, since they always have to work with the template, compared to the
StoryObj
of CSF, could simply do:That would remove some complexity from our side and keep the footprint of the addon smaller on their files?
To recap
Pending to define:
Stories
might not be passed ontoStory
. How do we want to share type information coming frommeta
with the smallest footprint?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.
It's there mostly to make eslint happy/make it conform to vue requirements. Vue 2 template has to have exactly one root component. This is no longer the case for vue 3, so yes, we could use a
useMeta
macro as an alternative to specify global information.The problem is that, for standard storybooks, one can manually overwrite
argTypes
for a given story, and add additional args that are not coming from the component. So this needs to be supported as well. But for component this is not the case (at least I couldn't find it in the docs).What I just realized is that one can write plugins for volar (see eg https://github.com/vue-macros/vue-macros/tree/main/packages/volar or https://github.com/Tahul/pinceau/blob/main/src/volar.ts). With this one could provide the type information from general
define...
macros and push down the type information from a centralStories
element (e.g. just replicateargTypes/component
props from the parent to theStory
childs if they don't overwrite it).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, for
args
,argTypes
,parameters
, etc. they all will need a global macro to set up the default for themeta
, and a local prop on<Story />
for the local override.I think that's quite clear and there's no ambiguity.
Let's describe the problems we need to solve:
1. Users need to set up the default of configuration on their
Meta
, within thescript
block.Here we've got two approaches to choose from:
defineMeta<typeof Component>
as a way to access the entire schema of the Storybook Meta.This would be my preferred approach, a unified interface exposing 1:1 the
Meta<typeof Component
.For the first iteration, we could exclusively support
args
to validate the concept and review the implementation.defineArgs
,defineArgTypes
,defineParameters
).The issue I would have with this approach is that each "fragment" to access the Meta will require type information in the generic signature
While the
defineMeta
would contain all the options within a single call, hence reducing the type footprint in the file.2. Users need to access type information on their
<Story />
from the props exposed by each slot.There are two approaches to solving this problem:
Volar plugin capturing the generic signature passed to
defineMeta
and providing that to each<Story />
Adding the
: component
prop to each<Story />
to avoid polluting the functionality and semantics of existingargs
andargTypes
.Once we have a supported solution from Volar, this property can be deprecated gradually to avoid breaking change (we can just ignore that prop internally once we make the switch of the addon implementation).
:component
is still not ideal, so for example since indefineMeta
we would have{ title: 'components/Button', component: Button }
, we can also consider just creating a descriptive and dedicated prop for this specific issue, to avoid any potential confusion with Storybook functionalities.Something like
:component-instance
for example.3. Change the addon approach from string token, to export Vue Components and functions
This is something I'd like you to confirm to make sure we're on the same page in terms of potential implementation.
With these new features, unless we create a Volar plugin, we would need to create real components that are exported to the users.
It will probably need to be a dedicated export point to avoid conflict with the output of the Storybook addon
Is my understanding correct here?
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.
Based on the above, we could start tackling some work in parallel.
If my understanding of point 3. is correct, I could tackle point 1 (create the macro and the Story component with scoped slots, and local overrides) while you research further into point 2 (Volar plugin to avoid creating the type prop on each Story)?
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 we should start extracting some of these points to new issues, since some of them are actually not strictly speaking involved args support.
defineMeta
for a uniform treatment, and the singledefineArgTypes
etc methods if someone prefers that (or wants to define a special arg type for a single story), and passing the options via a centralStories
component (which would be closer to the Option API of vue). The implementation overhead is there, but not too much in my opinion.component
(orcomponent-instance
) overargTypes
. The latter is supposed to define what args are available (and how they are rendered), so a natural place to provide typing information for the args. Especially since you can overwrite/append to the automatically derived component-based args.vue
to do the compilation to csf (instead of the manual parsing using the vue ast) but couldn't really find a way to do this. In principle, I would be open for such a shift. (But again, this is not strictly needed to support args).But yes, I will try to play around with the volar plugin and see whats possible.
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.
Good idea @tobiasdiez, I will open a few new issue to move forward these discussions within the right context.
Regarding 1) and 2):
I am okay with using
argTypes
as the call site to provide the type inference. My concern was only around the fact that end users will need to declare them even when they want to use whatever default is automatically inferred.But honestly, after discussing this at length, I think all alternatives have equal trade-offs, so we can pick this one until we can provide some type injection to
<Story />
from the<Stories :component />
directly.Regarding not exposing any implementation, but only types within the global scope for those macros, does this mean we want to expose a
types.d.ts
for user to import in theirtsconfig.json
?Would the usage look like this?
I am going to send you an email regarding some admin questions. For the next steps, I will open some new issues to track the discussion we've been having here.