diff --git a/.storybook/preview.js b/.storybook/preview.js index 837ee8382..9b0815955 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -8,6 +8,7 @@ import tokens from '../src/compiled/tokens/js/tokens'; import 'focus-visible'; import '../src/index-with-dependencies.scss'; import './preview.scss'; +import { makeTwigInclude } from '../src/make-twig-include'; const breakpoints = tokens.size.breakpoint; // Extend the languages Storybook will highlight @@ -67,6 +68,22 @@ export const parameters = { // https://github.com/storybookjs/storybook/blob/v6.0.21/addons/docs/docs/docspage.md#inline-stories-vs-iframe-stories inlineStories: true, prepareForInline: (storyFn) => htmlToReactParser.parse(storyFn()), + transformSource(src, storyContext) { + try { + const storyFunction = storyContext.getOriginal(); + const rendered = storyFunction(storyContext.args); + // The twing/source-inputs-loader.js file makes it so that whenever twig templates are rendered, + // the arguments and input path are stored in the window.__twig_inputs__ variable. + // __twig_inputs__ is a map between the output HTML and and objects with the arguments and input paths + // Here, since we have the rendered HTML, we can look up what the arguments and path were + // that correspond to that output + const input = window.__twig_inputs__?.get(rendered); + if (!input) return src; + return makeTwigInclude(input.path, input.args); + } catch { + return src; + } + }, }, viewport: { viewports: { diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6c606751d..aa2663bbe 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -73,3 +73,27 @@ This is generally not necessary, but in case you need to manually publish a vers You can run `npm publish --dry-run` to see everything that _would_ happen during publish, without actually publishing to the npm registry. Note the branch is `v-next` for now. When we we merge this branch to `master`, these instructions should be updated. + +## Overriding source code previews in Storybook + +For most stories, we are able to generate a twig source code snippet for Storybook to display automatically. When stories use `useEffect` or other hooks, the source code snippet cannot be generated automatically, so the JS that was passed to `` is shown instead (there may be other cases where this happens as well). In those cases, you can manually pass the source code to ``: + +```jsx +import { makeTwigInclude } from '../../make-twig-include'; + + {(args) => template(args)} +; +``` + +This generates a source code snippet like this: + +```twig +{% include 'asdf' with { + "foo": "bar" +} only %} +``` diff --git a/src/make-twig-include.js b/src/make-twig-include.js new file mode 100644 index 000000000..85f3c5196 --- /dev/null +++ b/src/make-twig-include.js @@ -0,0 +1,13 @@ +/** + * Generate a twig source string of an include with args, i.e. + * {% include '@cloudfour/components/button.twig' with { "type": "disabled" } only %} + * @param {string} path + * @param {any} args + */ +export const makeTwigInclude = (path, args) => { + const argsString = + Object.keys(args).length > 0 + ? ` with ${JSON.stringify(args, null, 2)}` + : ''; + return `{% include '${path}'${argsString} only %}`; +}; diff --git a/twing/source-inputs-loader.js b/twing/source-inputs-loader.js new file mode 100644 index 000000000..1adc26f3c --- /dev/null +++ b/twing/source-inputs-loader.js @@ -0,0 +1,24 @@ +const { relative } = require('path'); + +// This loader changes the functions that are generated by importing .twig files +// It makes the functions attach the arguments and twig file path to a global __twig__inputs__ global +// __twig__inputs__ is a map between the output HTML and and objects with the arguments and input paths +// __twig__inputs__ is used in transformSource in .storybook/preview.js to generate the twig source code previews + +module.exports = function (source) { + // Change it from an absolute path to a path starting with @cloudfour/ + const path = relative(this.rootContext, this.resourcePath).replace( + /^src\//, + '@cloudfour/' + ); + return source.replace( + `return template.render(context)`, + `const rendered = template.render(context); + const inputs = window.__twig_inputs__ || (window.__twig_inputs__ = new Map()); + inputs.set( + rendered, + { path: ${JSON.stringify(path)}, args: context } + ); + return rendered;` + ); +}; diff --git a/twing/webpack-options.js b/twing/webpack-options.js index 8a33b5089..f1b353fa2 100644 --- a/twing/webpack-options.js +++ b/twing/webpack-options.js @@ -5,12 +5,18 @@ const twingVirtualEnvironmentPath = require.resolve( const twingLoader = { test: /\.twig$/, - use: { - loader: 'twing-loader', - options: { - environmentModulePath: twingEnvironmentNodePath, + use: [ + // Webpack loaders run in reverse, so this top loader will run last + { + loader: require.resolve('./source-inputs-loader'), }, - }, + { + loader: 'twing-loader', + options: { + environmentModulePath: twingEnvironmentNodePath, + }, + }, + ], }; const alias = {