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
52 changes: 52 additions & 0 deletions code/core/src/csf/includeConditionalArg.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,56 @@ describe('includeConditionalArg', () => {
});
});
});

describe('function', () => {
it('should return true when function returns true', () => {
expect(
includeConditionalArg(
{ if: (args) => args.a === 1 },
{ a: 1 },
{}
)
).toBe(true);
});

it('should return false when function returns false', () => {
expect(
includeConditionalArg(
{ if: (args) => args.a === 1 },
{ a: 2 },
{}
)
).toBe(false);
});

it('should support multiple conditions via function', () => {
expect(
includeConditionalArg(
{ if: (args) => args.type === 'pessoaFisica' && args.pais === 'Brasil' },
{ type: 'pessoaFisica', pais: 'Brasil' },
{}
)
).toBe(true);
});

it('should support multiple conditions via function returning false', () => {
expect(
includeConditionalArg(
{ if: (args) => args.type === 'pessoaFisica' && args.pais === 'Brasil' },
{ type: 'pessoaFisica', pais: 'Alemanha' },
{}
)
).toBe(false);
});

it('should pass globals to the function', () => {
expect(
includeConditionalArg(
{ if: (_args, globals) => globals.theme === 'dark' },
{},
{ theme: 'dark' }
)
).toBe(true);
});
});
});
15 changes: 9 additions & 6 deletions code/core/src/csf/includeConditionalArg.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* @ts-expect-error (has no typings) */
import { isEqual } from '@ngard/tiny-isequal';

import type { Args, Conditional, Globals, InputType } from './story.ts';
import type { Args, ConditionalFunction, Conditional, Globals, InputType } from './story.ts';

const count = (vals: any[]) => vals.map((v) => typeof v !== 'undefined').filter(Boolean).length;

Expand All @@ -25,20 +25,23 @@ export const testValue = (cond: Omit<Conditional, 'arg' | 'global'>, value: any)
};

/**
* Helper function to include/exclude an arg based on the value of other other args aka "conditional
* args"
* Helper function to include/exclude an arg based on the value of other args aka "conditional
* args". Supports both object-based conditions and function-based conditions.
*/
export const includeConditionalArg = (argType: InputType, args: Args, globals: Globals) => {
if (!argType.if) {
return true;
}

// Support function-based conditions
if (typeof argType.if === 'function') {
return (argType.if as ConditionalFunction)(args, globals);
}

const { arg, global } = argType.if as any;
if (count([arg, global]) !== 1) {
throw new Error(`Invalid conditional value ${JSON.stringify({ arg, global })}`);
}

const value = arg ? args[arg] : globals[global];

return testValue(argType.if!, value);
};
};
3 changes: 2 additions & 1 deletion code/core/src/csf/story.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ type ControlType =

type ConditionalTest = { truthy?: boolean } | { exists: boolean } | { eq: any } | { neq: any };
type ConditionalValue = { arg: string } | { global: string };
export type Conditional = ConditionalValue & ConditionalTest;
export type ConditionalFunction = (args: Args, globals: Globals) => boolean;
export type Conditional = (ConditionalValue & ConditionalTest) | ConditionalFunction;
Comment on lines +61 to +62

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== files ==\n'
git ls-files 'code/core/src/csf/story.ts' 'code/core/src/csf/includeConditionalArg.ts' || true

printf '\n== outline: story.ts ==\n'
ast-grep outline code/core/src/csf/story.ts --view expanded || true

printf '\n== outline: includeConditionalArg.ts ==\n'
ast-grep outline code/core/src/csf/includeConditionalArg.ts --view expanded || true

printf '\n== relevant excerpts ==\n'
sed -n '1,140p' code/core/src/csf/story.ts
printf '\n---\n'
sed -n '1,120p' code/core/src/csf/includeConditionalArg.ts

printf '\n== type-level probe (best effort) ==\n'
node - <<'JS'
const fs = require('fs');
const path = 'code/core/src/csf/includeConditionalArg.ts';
const src = fs.readFileSync(path, 'utf8');
console.log(src.includes("Omit<Conditional, 'arg' | 'global'>") ? 'found Omit<Conditional, ...>' : 'pattern not found');
JS

Repository: storybookjs/storybook

Length of output: 13811


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "testValue\\(" code/core/src -g '*.ts' -g '*.tsx'
printf '\n---\n'
rg -n "type ConditionalFunction|type Conditional =" code/core/src/csf/story.ts
printf '\n---\n'
python3 - <<'PY'
# Lightweight structural probe for the TypeScript utility pattern in question.
# This does not type-check the repo; it just documents the set-theoretic expectation.
print("Omit< (object | function), keys-from-object > is typically {} when keyof of the union collapses to never.")
PY

Repository: storybookjs/storybook

Length of output: 946


🏁 Script executed:

#!/bin/bash
set -euo pipefail

node - <<'JS'
let ts;
try {
  ts = require('typescript');
} catch (e) {
  console.log('typescript-not-installed');
  process.exit(0);
}

const source = `
type ConditionalTest = { truthy?: boolean } | { exists: boolean } | { eq: any } | { neq: any };
type ConditionalValue = { arg: string } | { global: string };
type ConditionalFunction = (args: any, globals: any) => boolean;
type Conditional = (ConditionalValue & ConditionalTest) | ConditionalFunction;
type X = Omit<Conditional, 'arg' | 'global'>;
`;
const fileName = 'x.ts';
const host = ts.createCompilerHost({ noEmit: true, strict: true });
host.getSourceFile = (f, v) => f === fileName ? ts.createSourceFile(f, source, v, true) : undefined;
host.readFile = (f) => f === fileName ? source : undefined;
host.fileExists = (f) => f === fileName;
const program = ts.createProgram([fileName], { noEmit: true, strict: true }, host);
const checker = program.getTypeChecker();
const sf = program.getSourceFile(fileName);
const typeAlias = sf.statements.find(s => ts.isTypeAliasDeclaration(s) && s.name.text === 'X');
const type = checker.getTypeAtLocation(typeAlias);
console.log(checker.typeToString(type));
JS

Repository: storybookjs/storybook

Length of output: 195


🏁 Script executed:

#!/bin/bash
set -euo pipefail

node - <<'JS'
let ts;
try {
  ts = require('typescript');
} catch (e) {
  console.log('typescript-not-installed');
  process.exit(0);
}

const source = `
type ConditionalTest = { truthy?: boolean } | { exists: boolean } | { eq: any } | { neq: any };
type ConditionalValue = { arg: string } | { global: string };
type ConditionalFunction = (args: any, globals: any) => boolean;
type Conditional = (ConditionalValue & ConditionalTest) | ConditionalFunction;
type T = Omit<Conditional, 'arg' | 'global'>;

declare let ok1: T;
ok1 = { truthy: true };
ok1 = { exists: false };
ok1 = { eq: 1 };
ok1 = { neq: 1 };
ok1 = {};
ok1 = { foo: 1 };
ok1 = () => true;
`;

const fileName = 'x.ts';
const options = { noEmit: true, strict: true, target: ts.ScriptTarget.ES2020 };
const host = ts.createCompilerHost(options);
host.getSourceFile = (f, v) => f === fileName ? ts.createSourceFile(f, source, v, true) : undefined;
host.readFile = (f) => f === fileName ? source : undefined;
host.fileExists = (f) => f === fileName;
host.writeFile = () => {};

const program = ts.createProgram([fileName], options, host);
const checker = program.getTypeChecker();
const sf = program.getSourceFile(fileName);

const typeAlias = sf.statements.find(s => ts.isTypeAliasDeclaration(s) && s.name.text === 'T');
const tType = checker.getTypeAtLocation(typeAlias);
const props = checker.getPropertiesOfType(tType).map(p => p.name).sort();
console.log('properties:', JSON.stringify(props));
console.log('typeString:', checker.typeToString(tType));

const diags = ts.getPreEmitDiagnostics(program);
for (const d of diags) {
  const msg = ts.flattenDiagnosticMessageText(d.messageText, '\n');
  console.log(`diag:${d.code}:${msg}`);
}
JS

Repository: storybookjs/storybook

Length of output: 15846


🏁 Script executed:

#!/bin/bash
set -euo pipefail

node - <<'JS'
let ts;
try {
  ts = require('typescript');
} catch (e) {
  console.log('typescript-not-installed');
  process.exit(0);
}

const source = `
type Exclude<T, U> = T extends U ? never : T;
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

type ConditionalTest = { truthy?: boolean } | { exists: boolean } | { eq: any } | { neq: any };
type ConditionalValue = { arg: string } | { global: string };
type ConditionalFunction = (args: any, globals: any) => boolean;
type Conditional = (ConditionalValue & ConditionalTest) | ConditionalFunction;
type T = Omit<Conditional, 'arg' | 'global'>;

declare let t: T;
t = { truthy: true };
t = { exists: false };
t = { eq: 1 };
t = { neq: 1 };
t = {};
t = { foo: 1 };
t = () => true;
`;

const fileName = 'x.ts';
const options = { noEmit: true, strict: true, target: ts.ScriptTarget.ES2020, noLib: true };
const host = ts.createCompilerHost(options);
host.getSourceFile = (f, v) => f === fileName ? ts.createSourceFile(f, source, v, true) : undefined;
host.readFile = (f) => f === fileName ? source : undefined;
host.fileExists = (f) => f === fileName;
host.writeFile = () => {};

const program = ts.createProgram([fileName], options, host);
const checker = program.getTypeChecker();
const sf = program.getSourceFile(fileName);

const typeAlias = sf.statements.find(s => ts.isTypeAliasDeclaration(s) && s.name.text === 'T');
const tType = checker.getTypeAtLocation(typeAlias);
console.log('typeString:', checker.typeToString(tType));
console.log('properties:', JSON.stringify(checker.getPropertiesOfType(tType).map(p => p.name).sort()));

const diags = ts.getPreEmitDiagnostics(program);
for (const d of diags) {
  const msg = ts.flattenDiagnosticMessageText(d.messageText, '\n');
  console.log(`diag:${d.code}:${msg}`);
}
JS

Repository: storybookjs/storybook

Length of output: 15603


Exclude the function branch before applying Omit in code/core/src/csf/includeConditionalArg.ts.

Omit<Conditional, 'arg' | 'global'> collapses to {} because Conditional still includes ConditionalFunction, so testValue no longer enforces the truthy/exists/eq/neq shape. Use Exclude<Conditional, ConditionalFunction> (or an object-only alias) here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@code/core/src/csf/story.ts` around lines 61 - 62, The conditional type in
story.ts still includes the function branch, so applying Omit to Conditional in
includeConditionalArg.ts collapses the object shape and weakens testValue
typing. Update the type used for testValue to exclude ConditionalFunction first,
or introduce an object-only alias (for example via Exclude<Conditional,
ConditionalFunction>) before Omit so the truthy/exists/eq/neq structure remains
enforced.


interface ControlBase {
[key: string]: any;
Expand Down
Loading