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
19 changes: 19 additions & 0 deletions .changeset/parse-tssatisfies-named-default-export.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
"@ladle/react": patch
---

fix(parse): unwrap `TSSatisfiesExpression` / `TSAsExpression` in named-identifier default-export init

Ladle's `getDefaultExport` walker handles `TSSatisfiesExpression` / `TSAsExpression` when the _direct declaration_ is wrapped, e.g. `export default { … } satisfies Meta<…>;`. It did not unwrap the same expression when it appears as the **init** of a named identifier that is then re-exported by name:

```ts
const meta = {
title: "Components/Button",
component: Button,
} satisfies Meta<typeof Button>;
export default meta;
```

`objNode` ended up pointing at the `TSSatisfiesExpression` rather than the underlying `ObjectExpression`. The subsequent `objNode.properties.forEach` iterated over an empty array (the satisfies node has no `.properties`), so the `title` and `meta` fields silently never reached the result, and story files using this pattern were dropped from the index entirely.

Mirrors the existing direct-declaration unwrap pattern. Adds two new unit tests covering `satisfies` and `as` in this position.
13 changes: 13 additions & 0 deletions packages/ladle/lib/cli/vite-plugin/parse/get-default-export.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,19 @@ const getDefaultExport = (result, astPath) => {
) {
objNode = astPath.node.declaration.expression;
}
// Unwrap TSSatisfiesExpression / TSAsExpression that appears as the *init*
// of a named identifier default-export, e.g.
// const meta = { ... } satisfies Meta<typeof X>;
// export default meta;
// The named-identifier branch above resolves `objNode` to the init
// expression, but does not strip the `satisfies` wrapper.
if (
objNode &&
(objNode.type === "TSAsExpression" ||
objNode.type === "TSSatisfiesExpression")
) {
objNode = objNode.expression;
}
objNode &&
objNode.properties.forEach((/** @type {any} */ prop) => {
if (prop.type === "ObjectProperty" && prop.key.name === "title") {
Expand Down
40 changes: 40 additions & 0 deletions packages/ladle/tests/parse/get-default-export.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,43 @@ test("Throw an error if default export is not serializable", async () => {
`Can't parse the default title and meta of file.js. Meta must be serializable and title a string literal.`,
);
});

test("Get default export through named identifier with `satisfies`", async () => {
// CSF v3 / Storybook style:
// const meta = { ... } satisfies Meta<typeof X>;
// export default meta;
// Without the unwrap, `objNode` lands on the TSSatisfiesExpression
// (whose `.properties` is undefined), so title and meta silently never
// reach the result.
expect(
parseWithFn(
`const meta = { title: 'Title', meta: { some: 'foo' } } satisfies SomeType;
export default meta;`,
{},
getDefaultExport,
"ExportDefaultDeclaration",
"foo.stories.tsx",
),
).toEqual(
getOutput({
exportDefaultProps: { title: "Title", meta: { some: "foo" } },
}),
);
});

test("Get default export through named identifier with `as`", async () => {
expect(
parseWithFn(
`const meta = { title: 'Title', meta: { flag: true } } as SomeType;
export default meta;`,
{},
getDefaultExport,
"ExportDefaultDeclaration",
"foo.stories.tsx",
),
).toEqual(
getOutput({
exportDefaultProps: { title: "Title", meta: { flag: true } },
}),
);
});
Loading