diff --git a/code/core/src/cli/detect.ts b/code/core/src/cli/detect.ts
index 9002e0d7a3ab..fdb5f7fc060d 100644
--- a/code/core/src/cli/detect.ts
+++ b/code/core/src/cli/detect.ts
@@ -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;
diff --git a/code/core/src/cli/project_types.ts b/code/core/src/cli/project_types.ts
index 7a75f0c9f327..fcdf8e489e93 100644
--- a/code/core/src/cli/project_types.ts
+++ b/code/core/src/cli/project_types.ts
@@ -146,7 +146,7 @@ export const supportedTemplates: TemplateConfiguration[] = [
},
{
preset: ProjectType.EMBER,
- dependencies: ['ember-cli'],
+ dependencies: ['ember-source'],
matcherFunction: ({ dependencies }) => {
return dependencies?.every(Boolean) ?? true;
},
diff --git a/code/frameworks/ember/Button.stories.gjs b/code/frameworks/ember/Button.stories.gjs
new file mode 100644
index 000000000000..c5228920a428
--- /dev/null
+++ b/code/frameworks/ember/Button.stories.gjs
@@ -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) =>
+
+ ,
+ 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`
+//
+// `,
+// 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`
+//
+// `,
+// context: {
+// onClick: linkTo('example-button--docs'),
+// },
+// }),
+// name: 'button with link to another story',
+// };
diff --git a/code/frameworks/ember/template/cli/Button.stories.js b/code/frameworks/ember/Button.stories.js
similarity index 100%
rename from code/frameworks/ember/template/cli/Button.stories.js
rename to code/frameworks/ember/Button.stories.js
diff --git a/code/frameworks/ember/addon-main.cjs b/code/frameworks/ember/addon-main.cjs
new file mode 100644
index 000000000000..bce97ff68c1c
--- /dev/null
+++ b/code/frameworks/ember/addon-main.cjs
@@ -0,0 +1,3 @@
+module.exports = {
+ name: '@storybook/ember',
+};
diff --git a/code/frameworks/ember/build-config.ts b/code/frameworks/ember/build-config.ts
index 346ee1c094b8..67eb5646e9e3 100644
--- a/code/frameworks/ember/build-config.ts
+++ b/code/frameworks/ember/build-config.ts
@@ -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,
},
],
},
diff --git a/code/frameworks/ember/package.json b/code/frameworks/ember/package.json
index 3d832ae2f394..81fa4db0a576 100644
--- a/code/frameworks/ember/package.json
+++ b/code/frameworks/ember/package.json
@@ -6,6 +6,7 @@
"storybook",
"storybook-framework",
"ember",
+ "ember-addon",
"component",
"components"
],
@@ -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",
@@ -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"
}
diff --git a/code/frameworks/ember/preset.js b/code/frameworks/ember/preset.js
deleted file mode 100644
index 4bd63d324002..000000000000
--- a/code/frameworks/ember/preset.js
+++ /dev/null
@@ -1 +0,0 @@
-export * from './dist/preset.js';
diff --git a/code/frameworks/ember/src/client/_render.ts b/code/frameworks/ember/src/client/_render.ts
new file mode 100644
index 000000000000..9ef37680b43d
--- /dev/null
+++ b/code/frameworks/ember/src/client/_render.ts
@@ -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 = (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) => unknown
+) => {
+ globalThis.PLUGINS_SETUP_FUNCTIONS ??= new Set();
+ globalThis.PLUGINS_SETUP_FUNCTIONS.add(fn);
+};
+
+const runSetupFunctions = async (
+ app: Application,
+ storyContext: StoryContext
+): Promise => {
+ 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,
+ 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)
+ .__STORYBOOK_PREVIEW__ as PreviewWeb;
+ 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) {
+ 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) {
+ 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,
+ canvasElement: EmberRenderer['canvasElement']
+) {
+ storybookApp?.unmount();
+
+ if (map.has(canvasElement)) {
+ map.delete(canvasElement);
+ }
+}
diff --git a/code/frameworks/ember/src/client/config.ts b/code/frameworks/ember/src/client/config.ts
new file mode 100644
index 000000000000..d2ead9cfe256
--- /dev/null
+++ b/code/frameworks/ember/src/client/config.ts
@@ -0,0 +1 @@
+export { renderToCanvas } from './render';
diff --git a/code/frameworks/ember/src/client/globals.ts b/code/frameworks/ember/src/client/globals.ts
new file mode 100644
index 000000000000..d2dd7e9307c5
--- /dev/null
+++ b/code/frameworks/ember/src/client/globals.ts
@@ -0,0 +1,5 @@
+import { global } from '@storybook/global';
+
+const { window: globalWindow } = global;
+
+globalWindow.STORYBOOK_ENV = 'ember';
diff --git a/code/frameworks/ember/src/client/index.ts b/code/frameworks/ember/src/client/index.ts
new file mode 100644
index 000000000000..778e832b09d9
--- /dev/null
+++ b/code/frameworks/ember/src/client/index.ts
@@ -0,0 +1,3 @@
+import './globals';
+
+export { renderToCanvas } from './render';
diff --git a/code/frameworks/ember/src/client/render.ts b/code/frameworks/ember/src/client/render.ts
new file mode 100644
index 000000000000..52117fca3be1
--- /dev/null
+++ b/code/frameworks/ember/src/client/render.ts
@@ -0,0 +1,14 @@
+import { renderComponent } from '@ember/renderer';
+
+import type { EmberRenderer, RenderContext } from './types';
+
+export function renderToCanvas(
+ { storyFn, showMain }: RenderContext,
+ canvasElement: EmberRenderer['canvasElement']
+) {
+ showMain();
+
+ console.log('Ember renderer', storyFn());
+
+ renderComponent(storyFn(), { into: canvasElement });
+}
diff --git a/code/frameworks/ember/src/client/types.ts b/code/frameworks/ember/src/client/types.ts
new file mode 100644
index 000000000000..0ff878dd294d
--- /dev/null
+++ b/code/frameworks/ember/src/client/types.ts
@@ -0,0 +1,52 @@
+import {
+ type Canvas,
+ type StoryContext as StoryContextBase,
+ type WebRenderer,
+} from 'storybook/internal/types';
+
+import type Application from '@ember/application';
+import { renderComponent } from '@ember/renderer';
+
+export type { RenderContext } from 'storybook/internal/types';
+
+export type StoryID = string;
+
+export interface ShowErrorArgs {
+ title: string;
+ description: string;
+}
+
+// export type StoryFnVueReturnType = ConcreteComponent;
+export type StoryFnEmberReturnType = unknown;
+
+// export type StoryContext = StoryContextBase;
+export type StoryContext = StoryContextBase;
+
+// export type StorybookVueApp = { vueApp: App; storyContext: StoryContext };
+export type StorybookEmberApp = { emberApp: Application; storyContext: StoryContext };
+
+export interface EmberRenderer extends WebRenderer {
+ // We are omitting props, as we don't use it internally, and more importantly, it completely changes the assignability of meta.component.
+ // Try not omitting, and check the type errros in the test file, if you want to learn more.
+ component: object;
+ storyResult: object;
+
+ // mount: (
+ // Component?: StoryFnEmberReturnType,
+ // // TODO add proper typesafety
+ // options?: { props?: Record; slots?: Record }
+ // ) => Promise