Skip to content
Open
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
18 changes: 12 additions & 6 deletions code/frameworks/ember/build-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,27 @@ const config: BuildEntries = {
exportEntries: ['.'],
entryPoint: './src/index.ts',
},
{
exportEntries: ['./client'],
entryPoint: './src/client/index.ts',
},
{
exportEntries: ['./client/config'],
entryPoint: './src/client/config.ts',
},
{
exportEntries: ['./client/docs/config'],
entryPoint: './src/client/docs/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
42 changes: 28 additions & 14 deletions code/frameworks/ember/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,40 +29,54 @@
"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"
},
"./client/docs/config": {
"types": "./dist/client/docs/config.d.ts",
"default": "./dist/client/docs/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",
"!src/**/*"
],
"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",
"ts-dedent": "^2.0.0",
"typescript": "^5.8.3"
},
"devDependencies": {
"ember-source": "~3.28.1",
"typescript": "^5.8.3"
"@types/node": "^22.0.0",
"ember-source": "^6.8.0",
"typescript": "^5.8.3",
"vite": "^7.0.4"
},
"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",
"storybook": "workspace:^"
"ember-source": ">=6.8.0",
"storybook": "workspace:^",
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0"
},
"publishConfig": {
"access": "public"
Expand Down
13 changes: 13 additions & 0 deletions code/frameworks/ember/src/client/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { enhanceArgTypes } from 'storybook/internal/docs-tools';
import type { ArgTypesEnhancer, Parameters } from 'storybook/internal/types';

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

export const parameters: Parameters = {
renderer: 'ember',
docs: {
story: { inline: true },
},
};

export const argTypesEnhancers: ArgTypesEnhancer[] = [enhanceArgTypes];
15 changes: 15 additions & 0 deletions code/frameworks/ember/src/client/docs/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { SourceType } from 'storybook/internal/docs-tools';
import type { DecoratorFunction, Parameters } from 'storybook/internal/types';

import { sourceDecorator } from './sourceDecorator';

export const parameters: Parameters = {
docs: {
source: {
type: SourceType.DYNAMIC,
language: 'html',
},
},
};

export const decorators: DecoratorFunction[] = [sourceDecorator];
86 changes: 86 additions & 0 deletions code/frameworks/ember/src/client/docs/sourceDecorator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { SourceType } from 'storybook/internal/docs-tools';
import type { ArgTypes, Args, DecoratorFunction } from 'storybook/internal/types';

import { emitTransformCode, useEffect, useRef } from 'storybook/preview-api';

import type { StoryFn } from '../public-types';
import type { EmberRenderer } from '../types';

function skipSourceRender(context: Parameters<DecoratorFunction<EmberRenderer>>[1]) {
const sourceParams = context?.parameters.docs?.source;
const isArgsStory = context?.parameters.__isArgsStory;

// always render if the user forces it
if (sourceParams?.type === SourceType.DYNAMIC) {
return false;
}

// never render if the user is forcing the block to render code, or
// if the user provides code, or if it's not an args story.
return !isArgsStory || sourceParams?.code || sourceParams?.type === SourceType.CODE;
}

export const sourceDecorator: DecoratorFunction<EmberRenderer> = (storyFn, context) => {
const source = useRef<string | undefined>(undefined);
const story = storyFn();

useEffect(() => {
const renderedForSource = context?.parameters.docs?.source?.excludeDecorators
? (context.originalStoryFn as StoryFn)(context.args, context)
: story;

if (!skipSourceRender(context)) {
const code =
generateGlimmerSource(renderedForSource, context.args, context.argTypes) ?? undefined;
emitTransformCode(code, context);
source.current = code;
}
});

return story;
};
Comment on lines +23 to +41
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add dependency array and cache guard to prevent emitting transformed source on every render.

The useEffect() hook currently lacks a dependency array, causing it to run after every render and calling emitTransformCode unnecessarily. The source.current cache is assigned but never checked before emitting. This differs from the React and other framework implementations, which properly track dependencies and avoid redundant channel emissions.

 export const sourceDecorator: DecoratorFunction<EmberRenderer> = (storyFn, context) => {
   const source = useRef<string | undefined>(undefined);
   const story = storyFn();

   useEffect(() => {
     const renderedForSource = context?.parameters.docs?.source?.excludeDecorators
       ? (context.originalStoryFn as StoryFn)(context.args, context)
       : story;

-    if (!skipSourceRender(context)) {
-      const code =
-        generateGlimmerSource(renderedForSource, context.args, context.argTypes) ?? undefined;
-      emitTransformCode(code, context);
-      source.current = code;
-    }
-  });
+    if (skipSourceRender(context)) return;
+
+    const code =
+      generateGlimmerSource(renderedForSource as any, context.args, context.argTypes) ?? undefined;
+
+    if (code !== source.current) {
+      emitTransformCode(code, context);
+      source.current = code;
+    }
+  }, [
+    context.id,
+    context.args,
+    context.argTypes,
+    context.parameters.docs?.source?.excludeDecorators,
+    context.parameters.docs?.source?.type,
+    context.parameters.docs?.source?.code,
+    context.parameters.__isArgsStory,
+  ]);
🤖 Prompt for AI Agents
In code/frameworks/ember/src/client/docs/sourceDecorator.ts around lines 23 to
41, update the useEffect to include an appropriate dependency array and a cache
guard so transformed source is only emitted when it changes: compute the new
code as before, then compare it to source.current and only call
emitTransformCode and update source.current when code !== source.current;
finally, add a dependency array that covers the values that can change (for
example: context.args, context.argTypes,
context.parameters?.docs?.source?.excludeDecorators, context.originalStoryFn,
and story) so the effect runs only when relevant inputs change.


export function generateGlimmerSource(
component: object & { name?: string },
args: Args,
argTypes: ArgTypes
): string | null {
const name = component.name;
if (!name) {
return null;
}

const propsArray = Object.entries(args)
.map(([k, v]) => toArgument(k, v, argTypes))
.filter((p) => p);

if (propsArray.length === 0) {
return `<${name} />`;
} else if (propsArray.length > 3) {
return `<${name}\n ${propsArray.join('\n ')}\n/>`;
}
return `<${name} ${propsArray.join(' ')} />`;
}
Comment on lines +43 to +63
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

generateGlimmerSource() likely reads the wrong “component” shape for Ember stories.
In this renderer, the story output is typically a render result object (e.g. { Component, args }), not the component itself—so component.name will often be undefined and source won’t render.

 export function generateGlimmerSource(
-  component: object & { name?: string },
+  rendered: any,
   args: Args,
   argTypes: ArgTypes
 ): string | null {
-  const name = component.name;
+  const component = rendered?.Component ?? rendered;
+  const name: string | undefined = component?.name;
   if (!name) {
     return null;
   }
@@
 }
🤖 Prompt for AI Agents
In code/frameworks/ember/src/client/docs/sourceDecorator.ts around lines 43 to
63, generateGlimmerSource assumes the passed "component" is the component itself
and reads component.name, but Ember stories pass a render result object (e.g. {
Component, args }), so component.name is often undefined; change the name
extraction to handle both shapes by checking for a Component property and using
Component.name when present (falling back to component.name), update the
function/type signature accordingly, and keep the existing null-return behavior
if no name can be resolved.


function toArgument(key: string, value: unknown, argTypes: ArgTypes): string | null {
if (value === undefined || value === null) {
return null;
}

const argType = argTypes[key];

// event should be skipped
if (argType && argType.action) {
return null;
}

if (typeof value === 'string') {
return `@${key}=${JSON.stringify(value)}`;
}

if (typeof value === 'number' || typeof value === 'boolean') {
return `@${key}={{${JSON.stringify(value)}}}`;
}

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

export { renderToCanvas } from './render';
export * from './public-types';
export * from './portable-stories';
42 changes: 42 additions & 0 deletions code/frameworks/ember/src/client/portable-stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import type {
NamedOrDefaultProjectAnnotations,
NormalizedProjectAnnotations,
} from 'storybook/internal/types';

import {
setProjectAnnotations as originalSetProjectAnnotations,
setDefaultProjectAnnotations,
} from 'storybook/preview-api';

import * as INTERNAL_DEFAULT_PROJECT_ANNOTATIONS from './render';
import type { EmberRenderer } from './types';

/**
* Function that sets the globalConfig of your storybook. The global config is the preview module of
* your .storybook folder.
*
* It should be run a single time, so that your global config (e.g. decorators) is applied to your
* stories when using `composeStories` or `composeStory`.
*
* Example:
*
* ```jsx
* // setup-file.js
* import { setProjectAnnotations } from '@storybook/preact';
* import projectAnnotations from './.storybook/preview';
*
* setProjectAnnotations(projectAnnotations);
* ```
*
* @param projectAnnotations - E.g. (import projectAnnotations from '../.storybook/preview')
*/
export function setProjectAnnotations(
projectAnnotations:
| NamedOrDefaultProjectAnnotations<any>
| NamedOrDefaultProjectAnnotations<any>[]
): NormalizedProjectAnnotations<EmberRenderer> {
setDefaultProjectAnnotations(INTERNAL_DEFAULT_PROJECT_ANNOTATIONS);
return originalSetProjectAnnotations(
projectAnnotations
) as NormalizedProjectAnnotations<EmberRenderer>;
Comment on lines +11 to +41
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# First, check if the file exists and get its current state
echo "=== Checking portable-stories.ts file existence and content ==="
wc -l code/frameworks/ember/src/client/portable-stories.ts

# Inspect what ./render exports to ensure it's suitable as default project annotations
echo -e "\n=== Inspecting render.ts exports ==="
rg -n --type=ts "^export\s+(const|function|type|interface|class)\s+" code/frameworks/ember/src/client/render.ts

# Also check for namespace exports and default exports
echo -e "\n=== Checking for export patterns in render.ts ==="
rg -n "^export\s+" code/frameworks/ember/src/client/render.ts | head -20

# Check the entire render.ts file to understand its structure
echo -e "\n=== Checking render.ts file size and structure ==="
wc -l code/frameworks/ember/src/client/render.ts

Repository: storybookjs/storybook

Length of output: 560


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check the actual imports and type definitions in portable-stories.ts
echo "=== Reading portable-stories.ts to see current state ==="
cat -n code/frameworks/ember/src/client/portable-stories.ts | head -50

# Look for setDefaultProjectAnnotations import
echo -e "\n=== Checking for setDefaultProjectAnnotations import ==="
rg -n "setDefaultProjectAnnotations|originalSetProjectAnnotations" code/frameworks/ember/src/client/portable-stories.ts

Repository: storybookjs/storybook

Length of output: 2051


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check the render.ts file content to understand what gets exported as INTERNAL_DEFAULT_PROJECT_ANNOTATIONS
echo "=== Reading render.ts file content ==="
cat -n code/frameworks/ember/src/client/render.ts

Repository: storybookjs/storybook

Length of output: 3343


🏁 Script executed:

#!/bin/bash
set -eulo pipefail

# Search for NamedOrDefaultProjectAnnotations type definition
echo "=== Searching for NamedOrDefaultProjectAnnotations type definition ==="
rg -n "NamedOrDefaultProjectAnnotations" code/frameworks/ember/src/client/ -A 3

# Also search in common locations
echo -e "\n=== Searching in storybook internal types ==="
fd "types.ts" code/ | xargs rg -l "NamedOrDefaultProjectAnnotations" 2>/dev/null || echo "Not found with fd approach"

# Direct search
echo -e "\n=== Direct search for type definition ==="
rg "type NamedOrDefaultProjectAnnotations|interface NamedOrDefaultProjectAnnotations" -A 3

Repository: storybookjs/storybook

Length of output: 1622


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find and examine setDefaultProjectAnnotations signature
echo "=== Searching for setDefaultProjectAnnotations definition ==="
rg -n "setDefaultProjectAnnotations" code/core/src --type=ts -A 5 | head -40

# Look at the ProjectAnnotations type to understand what should be passed
echo -e "\n=== Searching for ProjectAnnotations type definition ==="
rg -n "type ProjectAnnotations|interface ProjectAnnotations" code/core/src --type=ts -A 10 | head -50

Repository: storybookjs/storybook

Length of output: 3215


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Compare how other frameworks (like react) handle portable-stories
echo "=== Checking react framework's portable-stories ==="
if [ -f "code/frameworks/react/src/client/portable-stories.ts" ]; then
  cat -n code/frameworks/react/src/client/portable-stories.ts | head -50
fi

# Check preact too
echo -e "\n=== Checking preact framework's portable-stories ==="
if [ -f "code/frameworks/preact/src/client/portable-stories.ts" ]; then
  cat -n code/frameworks/preact/src/client/portable-stories.ts | head -50
fi

Repository: storybookjs/storybook

Length of output: 170


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for other frameworks' render.ts to see what they export
echo "=== Checking react framework's render.ts exports ==="
if [ -f "code/frameworks/react/src/client/render.ts" ]; then
  rg -n "^export\s+" code/frameworks/react/src/client/render.ts
fi

echo -e "\n=== Checking preact framework's render.ts exports ==="
if [ -f "code/frameworks/preact/src/client/render.ts" ]; then
  rg -n "^export\s+" code/frameworks/preact/src/client/render.ts
fi

echo -e "\n=== Checking vue framework's render.ts exports ==="
if [ -f "code/frameworks/vue3/src/client/render.ts" ]; then
  rg -n "^export\s+" code/frameworks/vue3/src/client/render.ts
fi

Repository: storybookjs/storybook

Length of output: 224


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# List all client directory structures
echo "=== Framework structure comparison ==="
for dir in code/frameworks/*/src/client/; do
  echo "Framework: $(basename $(dirname $(dirname $(dirname "$dir"))))"
  ls -1 "$dir" | grep -E "(render|portable-stories|preview)" || echo "  (no matching files)"
done

Repository: storybookjs/storybook

Length of output: 223


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Search for any explicit ProjectAnnotations assignments or exports in ember
echo "=== Searching for ProjectAnnotations in ember framework ==="
rg -n "ProjectAnnotations" code/frameworks/ember/src/client/ --type=ts

# Check if there's a preview or index file
echo -e "\n=== Files in ember client directory ==="
ls -la code/frameworks/ember/src/client/

# Look for any setDefaultProjectAnnotations calls to understand the pattern
echo -e "\n=== All setDefaultProjectAnnotations usage patterns ==="
rg -B 5 "setDefaultProjectAnnotations\(" code/frameworks/ --type=ts

Repository: storybookjs/storybook

Length of output: 5235


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check the actual behavior - what does renderToCanvas signature expect?
echo "=== Checking RenderToCanvas type definition ==="
rg -n "type RenderToCanvas|interface RenderToCanvas" code/core/src --type=ts -A 5

# Check if render function is supposed to be in ProjectAnnotations
echo -e "\n=== Checking ArgsStoryFn usage ==="
rg -n "ArgsStoryFn" code/core/src/types --type=ts -A 3 | head -30

Repository: storybookjs/storybook

Length of output: 2471


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check if INTERNAL_DEFAULT_PROJECT_ANNOTATIONS needs type casting
echo "=== Checking if similar patterns exist in nextjs-vite ==="
cat -n code/frameworks/nextjs-vite/src/portable-stories.ts | head -50

# Check if there's a type definition or explicit casting
echo -e "\n=== Checking nextjs-vite render.ts for comparison ==="
if [ -f "code/frameworks/nextjs-vite/src/render.ts" ]; then
  head -30 code/frameworks/nextjs-vite/src/render.ts
fi

Repository: storybookjs/storybook

Length of output: 2251


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify types are consistent - check BaseProjectAnnotations
echo "=== Checking BaseProjectAnnotations ==="
rg -n "interface BaseProjectAnnotations|type BaseProjectAnnotations" code/core/src --type=ts -A 15 | head -40

Repository: storybookjs/storybook

Length of output: 104


Fix the JSDoc example + tighten types; verify setDefaultProjectAnnotations receives valid project annotations.

  • JSDoc shows @storybook/preact (line 25) instead of @storybook/ember.
  • Function parameter types use any instead of EmberRenderer, weakening type safety in a public API.
  • import * as INTERNAL_DEFAULT_PROJECT_ANNOTATIONS from './render' creates a namespace containing render and renderToCanvas functions, but render is not part of the ProjectAnnotations interface. The namespace should either be explicitly typed as ProjectAnnotations<EmberRenderer> or restructured to avoid passing non-annotation symbols to setDefaultProjectAnnotations.
 import type {
   NamedOrDefaultProjectAnnotations,
   NormalizedProjectAnnotations,
 } from 'storybook/internal/types';

 import {
   setProjectAnnotations as originalSetProjectAnnotations,
   setDefaultProjectAnnotations,
 } from 'storybook/preview-api';

 import * as INTERNAL_DEFAULT_PROJECT_ANNOTATIONS from './render';
 import type { EmberRenderer } from './types';

 /**
  * Function that sets the globalConfig of your storybook. The global config is the preview module of
  * your .storybook folder.
  *
  * It should be run a single time, so that your global config (e.g. decorators) is applied to your
  * stories when using `composeStories` or `composeStory`.
  *
  * Example:
  *
  * ```jsx
  * // setup-file.js
- * import { setProjectAnnotations } from '@storybook/preact';
+ * import { setProjectAnnotations } from '@storybook/ember';
  * import projectAnnotations from './.storybook/preview';
  *
  * setProjectAnnotations(projectAnnotations);
  * ```
  *
  * @param projectAnnotations - E.g. (import projectAnnotations from '../.storybook/preview')
  */
 export function setProjectAnnotations(
   projectAnnotations:
-    | NamedOrDefaultProjectAnnotations<any>
-    | NamedOrDefaultProjectAnnotations<any>[]
+    | NamedOrDefaultProjectAnnotations<EmberRenderer>
+    | NamedOrDefaultProjectAnnotations<EmberRenderer>[]
 ): NormalizedProjectAnnotations<EmberRenderer> {
   setDefaultProjectAnnotations(INTERNAL_DEFAULT_PROJECT_ANNOTATIONS);
   return originalSetProjectAnnotations(
     projectAnnotations
   ) as NormalizedProjectAnnotations<EmberRenderer>;
 }
🤖 Prompt for AI Agents
In code/frameworks/ember/src/client/portable-stories.ts around lines 11 to 41,
update the JSDoc example to import from '@storybook/ember' instead of
'@storybook/preact', tighten the function parameter types by replacing
NamedOrDefaultProjectAnnotations<any> with
NamedOrDefaultProjectAnnotations<EmberRenderer> (and the array variant), and
ensure setDefaultProjectAnnotations receives a valid
ProjectAnnotations<EmberRenderer> payload by either exporting/constructing an
explicit ProjectAnnotations<EmberRenderer> object from ./render or by
narrowing/casting INTERNAL_DEFAULT_PROJECT_ANNOTATIONS to
ProjectAnnotations<EmberRenderer> (remove or exclude non-annotation exports like
render/renderToCanvas before passing to setDefaultProjectAnnotations).

}
18 changes: 0 additions & 18 deletions code/frameworks/ember/src/client/preview/config.ts

This file was deleted.

4 changes: 0 additions & 4 deletions code/frameworks/ember/src/client/preview/globals.ts

This file was deleted.

48 changes: 0 additions & 48 deletions code/frameworks/ember/src/client/preview/jsondoc.ts

This file was deleted.

Loading
Loading