Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion code/core/src/cli/detect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,6 @@ export async function detectBuilder(packageManager: JsPackageManager, projectTyp
case ProjectType.ANGULAR:
case ProjectType.REACT_NATIVE: // technically react native doesn't use webpack, we just want to set something
case ProjectType.NEXTJS:
case ProjectType.EMBER:
return CoreBuilder.Webpack5;
case ProjectType.NUXT:
return CoreBuilder.Vite;
Expand Down
2 changes: 1 addition & 1 deletion code/core/src/cli/project_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ export const supportedTemplates: TemplateConfiguration[] = [
},
{
preset: ProjectType.EMBER,
dependencies: ['ember-cli'],
dependencies: ['ember-source'],
matcherFunction: ({ dependencies }) => {
return dependencies?.every(Boolean) ?? true;
},
Expand Down
61 changes: 61 additions & 0 deletions code/frameworks/ember/Button.stories.gjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// import { on } from '@ember/modifiers';
import { action } from 'storybook/actions';
import { fn } from 'storybook/test';

// More on how to set up stories at: https://storybook.js.org/docs/writing-stories
export default {
title: 'Example/Button',
render: (args) => <template>
<button>{{args.label}}</button>
</template>,
argTypes: {
label: { control: 'text' },
},
// This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/ember/writing-docs/autodocs
// tags: ['autodocs'],
args: { onClick: fn() },
};

// More on writing stories with args: https://storybook.js.org/docs/writing-stories/args
export const Text = {
args: {
label: 'Button',
},
};

export const Emoji = {
args: {
label: '😀 😎 👍 💯',
},
};

// export const TextWithAction = {
// render: () => ({
// template: hbs`
// <button {{on "click" this.onClick}}>
// Trigger Action
// </button>
// `,
// context: {
// onClick: () => action('This was clicked')(),
// },
// }),
// name: 'With an action',
// parameters: {
// notes: 'My notes on a button with emojis',
// },
// };

// export const ButtonWithLinkToAnotherStory = {
// render: () => ({
// template: hbs`
// <button {{on "click" this.onClick}}>
// Go to Welcome Story
// </button>
// `,
// context: {
// onClick: linkTo('example-button--docs'),
// },
// }),
// name: 'button with link to another story',
// };
3 changes: 3 additions & 0 deletions code/frameworks/ember/addon-main.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
name: '@storybook/ember',
};
14 changes: 8 additions & 6 deletions code/frameworks/ember/build-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,23 @@ const config: BuildEntries = {
exportEntries: ['.'],
entryPoint: './src/index.ts',
},
{
exportEntries: ['./client'],
entryPoint: './src/client/index.ts',
},
{
exportEntries: ['./client/config'],
entryPoint: './src/client/config.ts',
},
],
node: [
{
exportEntries: ['./node'],
entryPoint: './src/node/index.ts',
},
{
exportEntries: ['./server/framework-preset-babel-ember'],
entryPoint: './src/server/framework-preset-babel-ember.ts',
dts: false,
},
{
exportEntries: ['./preset'],
entryPoint: './src/preset.ts',
dts: false,
},
],
},
Expand Down
45 changes: 34 additions & 11 deletions code/frameworks/ember/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"storybook",
"storybook-framework",
"ember",
"ember-addon",
"component",
"components"
],
Expand All @@ -29,17 +30,28 @@
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./client": {
"types": "./dist/client/index.d.ts",
"default": "./dist/client/index.js"
},
"./client/config": {
"types": "./dist/client/config.d.ts",
"default": "./dist/client/config.js"
},
"./node": {
"types": "./dist/node/index.d.ts",
"default": "./dist/node/index.js"
},
"./package.json": "./package.json",
"./preset": "./dist/preset.js",
"./server/framework-preset-babel-ember": "./dist/server/framework-preset-babel-ember.js"
"./preset": {
"types": "./dist/preset.d.ts",
"default": "./dist/preset.js"
}
},
"files": [
"dist/**/*",
"template/cli/**/*",
"addon-main.cjs",
"README.md",
"*.js",
"*.d.ts",
Expand All @@ -50,26 +62,37 @@
"prep": "jiti ../../../scripts/build/build-package.ts"
},
"dependencies": {
"@storybook/builder-webpack5": "workspace:*",
"@storybook/builder-vite": "workspace:*",
"@storybook/global": "^5.0.0",
"babel-loader": "9.1.3",
"empathic": "2.0.0"
"content-tag": "^4.0.0",
"object-inspect": "^1.13.4"
},
"devDependencies": {
"ember-source": "~3.28.1",
"@babel/plugin-transform-runtime": "^7.28.3",
"@babel/plugin-transform-typescript": "^7.28.0",
"@embroider/core": "^4.2.3",
"@embroider/vite": "^1.2.3",
"babel-plugin-ember-template-compilation": "^2.0.0",
"decorator-transforms": "^2.3.0",
"lightningcss": "^1.30.1",
"typescript": "^5.8.3"
},
"peerDependencies": {
"@babel/core": "*",
"babel-plugin-ember-modules-api-polyfill": "^3.5.0",
"babel-plugin-htmlbars-inline-precompile": "^5.3.1",
"ember-source": "~3.28.1 || >=4.0.0",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"@embroider/core": "^4.2.0",
"@embroider/vite": "^1.2.0",
"babel-plugin-ember-template-compilation": "^2.0.0 || ^3.0.1",
"decorator-transforms": "^2.3.0",
"ember-source": "*",
"storybook": "workspace:^"
},
"publishConfig": {
"access": "public"
},
"ember-addon": {
"type": "addon",
"version": 2,
"main": "addon-main.cjs"
},
"gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae16"
}
1 change: 0 additions & 1 deletion code/frameworks/ember/preset.js

This file was deleted.

179 changes: 179 additions & 0 deletions code/frameworks/ember/src/client/_render.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/* eslint-disable local-rules/no-uncategorized-errors */
import type { Args, ArgsStoryFn, RenderContext, StoryContext } from 'storybook/internal/types';

// import { createApp, h, isReactive, isVNode, reactive } from 'vue';
import type Application from '@ember/application';
import type { PreviewWeb } from 'storybook/preview-api';

// import type { StoryFnVueReturnType, StoryID, VueRenderer } from './types';
import type { EmberRenderer, StoryFnEmberReturnType, StoryID } from './types';

export const render: ArgsStoryFn<EmberRenderer> = (props, context) => {
const { id, component: Component } = context;
if (!Component) {
throw new Error(
`Unable to render story ${id} as the component annotation is missing from the default export`
);
}

return () => h(Component, props, getSlots(props, context));
};

export const setup = (
fn: (app: Application, storyContext?: StoryContext<EmberRenderer>) => unknown
) => {
globalThis.PLUGINS_SETUP_FUNCTIONS ??= new Set();
globalThis.PLUGINS_SETUP_FUNCTIONS.add(fn);
};

const runSetupFunctions = async (
app: Application,
storyContext: StoryContext<EmberRenderer>
): Promise<void> => {
if (globalThis && globalThis.PLUGINS_SETUP_FUNCTIONS) {
await Promise.all([...globalThis.PLUGINS_SETUP_FUNCTIONS].map((fn) => fn(app, storyContext)));
}
};

const map = new Map<
EmberRenderer['canvasElement'] | StoryID,
{ emberApp: Application; reactiveArgs: Args }
>();

export async function renderToCanvas(
{
storyFn,
forceRemount,
showMain,
showException,
storyContext,
id,
}: RenderContext<EmberRenderer>,
canvasElement: EmberRenderer['canvasElement']
) {
const existingApp = map.get(canvasElement);

// if the story is already rendered and we are not forcing a remount, we just update the reactive args
if (existingApp && !forceRemount) {
// normally storyFn should be call once only in setup function,but because the nature of react and how storybook rendering the decorators
// we need to call here to run the decorators again
// i may wrap each decorator in memoized function to avoid calling it if the args are not changed
const element = storyFn(); // call the story function to get the root element with all the decorators
const args = getArgs(element, storyContext); // get args in case they are altered by decorators otherwise use the args from the context

updateArgs(existingApp.reactiveArgs, args);
return () => {
teardown(existingApp.emberApp, canvasElement);
};
}

if (existingApp && forceRemount) {
teardown(existingApp.emberApp, canvasElement);
}

// create vue app for the story

// create vue app for the story
const vueApp = createApp({
setup() {
storyContext.args = reactive(storyContext.args);
const rootElement = storyFn(); // call the story function to get the root element with all the decorators
const args = getArgs(rootElement, storyContext); // get args in case they are altered by decorators otherwise use the args from the context
const appState = { vueApp, reactiveArgs: reactive(args) };
map.set(canvasElement, appState);

return () => {
// not passing args here as props
// treat the rootElement as a component without props
return h(rootElement);
};
},
});

vueApp.config.errorHandler = (e: unknown, instance, info) => {
const preview = (window as Record<string, any>)
.__STORYBOOK_PREVIEW__ as PreviewWeb<VueRenderer>;
const isPlaying = preview?.storyRenders.some(
(renderer) => renderer.id === id && renderer.phase === 'playing'
);
// Errors thrown during playing need be shown in the interactions panel.
if (isPlaying) {
// Make sure that Vue won't swallow this error, by stacking it as a different event.
setTimeout(() => {
throw e;
}, 0);
} else {
showException(e as Error);
}
};
await runSetupFunctions(vueApp, storyContext);
vueApp.mount(canvasElement);

showMain();
return () => {
teardown(vueApp, canvasElement);
};
}

/** Generate slots for default story without render function template */
function getSlots(props: Args, context: StoryContext<EmberRenderer, Args>) {
const { argTypes } = context;
const slots = Object.entries(props)
.filter(([key]) => argTypes[key]?.table?.category === 'slots')
.map(([key, value]) => [key, typeof value === 'function' ? value : () => value]);

return Object.fromEntries(slots);
}

/**
* Get the args from the root element props if it is a vnode otherwise from the context
*
* @param element Is the root element of the story
* @param storyContext Is the story context
*/

function getArgs(element: StoryFnEmberReturnType, storyContext: StoryContext<EmberRenderer, Args>) {
return element.props && isVNode(element) ? element.props : storyContext.args;
}

/**
* Update the reactive args
*
* @param reactiveArgs
* @param nextArgs
* @returns
*/
export function updateArgs(reactiveArgs: Args, nextArgs: Args) {
if (Object.keys(nextArgs).length === 0) {
return;
}
const currentArgs = isReactive(reactiveArgs) ? reactiveArgs : reactive(reactiveArgs);
// delete all args in currentArgs that are not in nextArgs
Object.keys(currentArgs).forEach((key) => {
if (!(key in nextArgs)) {
delete currentArgs[key];
}
});
// update currentArgs with nextArgs
Object.assign(currentArgs, nextArgs);
}

/**
* Unmount the vue app
*
* @private
* @param storybookApp
* @param canvasElement
* @returns Void
*/

function teardown(
storybookApp: ReturnType<typeof createApp>,
canvasElement: EmberRenderer['canvasElement']
) {
storybookApp?.unmount();

if (map.has(canvasElement)) {
map.delete(canvasElement);
}
}
1 change: 1 addition & 0 deletions code/frameworks/ember/src/client/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { renderToCanvas } from './render';
5 changes: 5 additions & 0 deletions code/frameworks/ember/src/client/globals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { global } from '@storybook/global';

const { window: globalWindow } = global;

globalWindow.STORYBOOK_ENV = 'ember';
3 changes: 3 additions & 0 deletions code/frameworks/ember/src/client/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import './globals';

export { renderToCanvas } from './render';
Loading
Loading