Skip to content
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

Svelte: Svelte syntax Component Story Format #7682

Closed
wants to merge 19 commits into from

Conversation

rixo
Copy link

@rixo rixo commented Aug 5, 2019

Issue: #6653

Add support for authoring stories in Svelte format, mirroring MDX story syntax, and exporting result that conforms to CSF. CSF can be imported directly from the *.stories.svelte files thanks to a webpack loader.

Changes have been applied over existing app/svelte and examples/svelte-kitchen-sink to preserve support for legacy format and have everything under a single @storybook/svelte package.

What I did

  • Add a webpack loader that transforms *.stories.svelte to CSF

  • Add Meta and Story svelte components to @storybook/svelte

  • Change presets to include the svelte CSF loader

  • Move existing examples using storiesOf under legacy (still in the same svelte-kitchen-sink example)

  • Add examples using Svelte component syntax

How to test

cd examples/svelte-kitchen-sink
yarn storybook

Details

Following discussion in the issue and my first PR #7023, I have adapted the syntax to mirror that of MDX with Meta and Story components.

Integrating with CSF removed the need for any extra boilerplate (the new architecture also made the integration effort much simpler on my part since all the tricky things that are not framework specific are now handled directly by the core).

Here's an example of the resulting syntax that this gives us:

<script>
  import { Meta, Story } from '@storybooks/svelte'
  import { action } from '@storybook/addon-actions'

  import Button from './Button.svelte'
</script>

<Meta title="My|Stories" parameters={{ ... }} decorators={[ ... ]} />

<Story name"first" parameters={{ ... }} decorators={[ ... ]}>
  <p>Here's the component:</p>
  <Button>Click me!</Button/>
</Story>

<Story name"actions">
  <Button onClick={action('clicked')}>Click me!</Button/>
</Story>

With direct import, we get access to a Svelte component that is a preconfigured preview of the target story:

import { first } from './my.stories.svelte'

const FirstPreview = first() // returns a svelte component

// render the "story preview" (this is regular svelte component API)
const cmp = new FirstPreview({ target: document.body })

Next steps

The most pressing issue that remains to be addressed (IMO) is babel transpilation. Currently, Svelte components are not transpiled down to ES5; we're using the ES2015 output of Svelte compiler directly. Since those components (e.g. Story, Meta) are used in the browser, that imposes a hard limit on the browsers we can support. And, internally, it also causes interop issues between transpiled code and ES2015 code (transpiled classes cannot extend ES classes).

And then testing, interop with addons (source, docs...), MDX support... The babel issue is probably a blocker for some of these too.

@rixo rixo requested a review from kazupon as a code owner August 5, 2019 10:56
@vercel
Copy link

vercel bot commented Aug 5, 2019

This pull request is automatically deployed with Now.
To access deployments, click Details below or on the icon next to each push.

Latest deployment for this branch: https://monorepo-git-fork-rixo-svelte-csf.storybook.now.sh

@shilman shilman changed the title Svelte csf Svelte: New Svelte syntax Component Story Format Aug 5, 2019
@shilman shilman changed the title Svelte: New Svelte syntax Component Story Format Svelte: Svelte syntax Component Story Format Aug 5, 2019
@shilman
Copy link
Member

shilman commented Aug 5, 2019

cc @cam-stitt @plumpNation reviews welcome!! 👋

@vercel vercel bot temporarily deployed to staging August 5, 2019 11:29 Inactive
@vercel vercel bot temporarily deployed to staging August 5, 2019 14:56 Inactive
@vercel vercel bot temporarily deployed to staging August 6, 2019 11:03 Inactive
@rixo
Copy link
Author

rixo commented Aug 6, 2019

Here's my findings & interrogations following my work to configure Babel for Svelte (to fix tests, linting, and address the question of browser support).

I've also got an open question about what should be the proper result of a Svelte storyFn that need to be answered to move forward. Even though, after writing the issue, I think I've got the answer, I prefer to have it discussed here, because it might a pretty high impact decision for the future (for other framework integration, notably).

Babel

Here are the requirements that need to be considered regarding Svelte support:

  • Svelte doesn't ship with transpiled lib files (under node_modules), so it must be transpiled with the same babel options (or at least to the same ES level) as the SB app in order to achieve the level of browser support intended by the default babel config.

  • If we want to use "Svelte HOC" pattern for storyFn (see bellow), then we need to be able to extend from svelte's generated component classes. That means that the same class transform (kept as ES2015 classes, or transpiled down to ES5 functions) must be applied everywhere: compiled svelte components, svelte's own files (under node_modules), and every SB (app / addons) JS/TS files. Because classes transpiled to ES5 functions are not compatible with actual ES2015 classes (they can't extend from them => runtime error).

There's another requirement specific to the svelte-stories-loader used to process *.stories.svelte file:

  • svelte-stories-loader need the code passed to it (result of previous loaders) to be CommonJS, because it needs to dynamically generates CSF named exports. ❓ How future proof is this? (This requirement could probably be removed by doing more static analysis in the loader to prepare named exports from there -- which would in turn impose annoying but probably acceptable limitations on the Svelte story format: Story names musts be static strings, and Story components must be at the root of the template, to prevent having them in a dynamic if block or loop).

In my last commits, I have adapted framework presets to address these requirements, as well as the jest transform and config to fix tests in example/svelte-kitchen-sink.

I'm no Webpack / Babel ninja, so maybe someone who's more familiar with general handling of Babel in SB should take a second look...

What should be returned by storyFn?

Why this is important? This is what external consumers will get when using direct CSF imports (e.g. in Jest tests). This also has library wide implications for the implementation of various SB features for Svelte (e.g. addons/centered).

In the current state of my PR, both approach are implemented. The POJO format is used by the existing app/svelte, while I've experimented with "raw svelte component" as the output of parsing Svelte format *.stories.svelte.

But in the end, it probably make sense to pick & support only one approach.

import { myStory as storyFn } from './my.stories.svelte'

const story = storyFn() // <= what is story, here?

A descriptive POJO?

That's what is returned by the current implementation:

{
  Component: MyComponent,
  props: { ... },
  on: { ... },
  Wrapper: WrapperComponent,
  WrapperProps: { ... },
}

Pros

  • Can be hand written easily, and so it's easy to support by MDX in its current form.

  • Could be extended to also support an HTML element as wrapper (or multiple ones), which may help Svelte support for simple wrapping decorators, because it would spare them the need to deal with an actual

Cons

  • Rendering the story (outside of SB) requires some knowledge about the format of this POJO. (A render or renderStory function should probably be provided by SB, to prevent the actual POJO structure from becoming part of the public API, complicating further evolution of the format.)

  • Only one level of wrapping component is possible. This one's quite the bummer. Multiple decorators that need to wrap the story with another Svelte component (like addons/centered currently does) cannot be composed together with the current format. Wrapper could probably be turned into an array to address this issue, though.

An actual Svelte component?

This would give us this kind of result:

import { myStory: storyFn } from './my.stories.svelte'

const StoryCmp = storyFn()

// rendering the story
const cmp = new StoryCmp({ target: document.body })

It's important to consider the implementation here, because it will affect the whole SB project, regarding Babel needs.

To make this possible, we can use a Svelte HOC of sort, or rather a "preconfigured component" (my terminology, this is not necessarily standard linguo or common practice -- although Svelte's creator once gave his blessing to it).

Svelte components are classes, and we can extend those classes to wrap a component and feed it preconfigured props. The result is another valid Svelte component (that can be used with its imperative API from SvelteComponent, AND that can also be used transparently in a regular Svelte template).

that feed preconfigured props & all to the target component. Here's what it looks like:

import MyComponent from './MyComponent.svelte'

class MyStory extends MyComponent {
  // most notable config options are `target` and `props`
  constructor(config) {
    super({ ...config, props: { ...preconfiguredProps, ...config.props } })
  }
}

Pros

  • Easy to render, using regular Svelte component API.

  • Easy to nest infinitely in decorators that need it, by repeating the same pattern (example in centered addon).

  • The result of storyFn being an actual Svelte component, it can be reused in a Svelte file.

Cons

  • Extra knowledge of SB internals is required to render story with configured decorators & parameters. But there's nothing specific to Svelte here.

  • Not a lot of room to extend configuration, if the need arise later.

  • Requires that all SB packages be transpiled with the same Babel class transform. At least the ones that uses the extend pattern described above.

Regarding the last point, this should probably be handled in any case, for user code at least, because they could use the pattern themselves somewhere in their own code. As mentioned above, this is not a common practice (that I am aware of), but it's not forbidden or dismissed as an antipattern either, and the idea has already been floated in the community...

My personal conclusion

Considering that:

  • CSF consumers will always need a minimal knowledge (or some lib to abstract this knowledge) of CSF structure to use a story to its fullest (including decorators / parameters);

  • POJO is simpler to support by MDX in its current form;

  • POJO may make Svelte support easier for addons / core;

  • Wrapped components might make Webpack / Babel configuration more complicated for the end user and/or run into scary Babel issues...

I'd say POJO wins.

With some adaptations maybe. In particular, I think something to enable adding pure HTML wrappers would make Svelte support easier in many cases for other addons, by avoiding the need to bundle a dedicated svelte component themselves (for example in the centered addon).

@ndelangen ndelangen added this to the 5.4.0 milestone Nov 16, 2019
@shilman shilman modified the milestones: 5.4.0, 6.0.0 Dec 16, 2019
@jonatansberg
Copy link

jonatansberg commented May 29, 2020

@rixo Have you dropped this in favour of https://github.com/rixo/svench?

@plimeor
Copy link

plimeor commented Jun 22, 2020

@rixo Have you dropped this in favour of https://github.com/rixo/svench?

Looks like it is.

@shilman shilman removed this from the 6.0 milestone Jul 30, 2020
@BlackFenix2
Copy link
Contributor

@rixo so no storybook svelte? we can use Svelte Components inside storybook now, but the controls addon doesn't yet auto-generate arguments :(
https://storybook.js.org/docs/svelte/essentials/controls

Controls argument autodetection doesn't currently work with your framework.

To use Controls, you'll need to define the ArgTypes manually

@shilman is there a way i can submit a PR to add Svelte support for Controls argument autodetection? where is the autodetection implementation for React/Angular/Vue? i could try to trace from those.

@shilman
Copy link
Member

shilman commented Aug 29, 2020

@BlackFenix2 jump on our discord. I'd love to walk you through it.

https://discord.gg/UUt2PJb

@BlackFenix2
Copy link
Contributor

@shilman funny you should mention, i joined the discord server 5 minutes ago. just gotta wait 5 more min...

@BlackFenix2
Copy link
Contributor

@shilman if I haven't driven everyone crazy yet, I could try a shot at finishing this PR after I finish adding autogenerating controls for svelte. This is to write stories in .svelte files?

@BlackFenix2
Copy link
Contributor

BlackFenix2 commented Sep 3, 2020

i re-uploaded the PR to use a storybook branch here: #12367

i don't see the original author re-engaging with us.

@shilman
Copy link
Member

shilman commented Sep 3, 2020

Thanks @BlackFenix2. Closing this!

@shilman shilman closed this Sep 3, 2020
@shilman
Copy link
Member

shilman commented Feb 24, 2021

This work lives on here: storybookjs/addon-svelte-csf#1 🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants