Skip to content

feat: unify smartui-storybook into smartui-cli (built-in storybook command + TUI) - #1

Closed
chaitanyas-maker wants to merge 59 commits into
prodfrom
feat/unify-storybook
Closed

feat: unify smartui-storybook into smartui-cli (built-in storybook command + TUI)#1
chaitanyas-maker wants to merge 59 commits into
prodfrom
feat/unify-storybook

Conversation

@chaitanyas-maker

Copy link
Copy Markdown
Owner

Unify @lambdatest/smartui-storybook into @lambdatest/smartui-cli

Prototype for team review. This folds the standalone Storybook CLI into the main CLI as a built-in storybook command, plus a terminal UI. Opened inside my fork for discussion — not for direct merge to upstream. Validated end-to-end against the live SmartUI cloud.

Why

The two packages both declare bin.smartui, so they collide on PATH — installing @lambdatest/smartui-storybook silently clobbers @lambdatest/smartui-cli (and vice-versa). Users need two installs for one tool. This PR makes one binary own the name and ships Storybook as a first-class command.

Approach — faithful relocation (not a rewrite)

Rather than re-plumb Storybook onto the CLI's createBuild/finalizeBuild lifecycle (which depends on an unverified backend question — see Open Questions), this vendors the proven, shipping @lambdatest/smartui-storybook v1.1.32 engine verbatim into the CLI under src/storybookVendor/, hitting the same /storybook/* endpoints it already uses in production. Behaviour is byte-identical to the standalone tool; only the collision and the double-install are removed.

What changed

  • New command smartui storybook <url-or-dir> — URL mode (live server) and DIR mode (static build), identical grammar to the old package.
  • New generator smartui config:create-storybook + a storybook block added to the ajv ConfigSchema (and anyOf, since top-level is additionalProperties:false).
  • Vendored engine src/storybookVendor/** (12 .cjs modules) — the unique Storybook logic (story discovery, DOM/CSSOM serialize, filtering, zip+render) relocated and bundled by tsup.
  • Terminal UI src/lib/tui.cjs — gradient figlet banner, boxed launch summary, and an end-of-run results box (dashboard link + screenshot/approval/change counts). Degrades gracefully with no TTY (CI-safe).
  • Deps added: puppeteer, jsdom, archiver, form-data, cli-table3, proxy agents (engine); figlet, gradient-string, boxen, log-symbols, ora (TUI). pnpm-lock.yaml updated.

Notable engineering notes

  • Vendored .js.cjs so esbuild bundles them as CommonJS under the CLI's "type":"module" (otherwise module.exports is silently dropped).
  • Fixed 3 sloppy-mode implicit globals (res, filename, githubURL) that only crash once bundled into strict mode.
  • The results summary box is emitted on process.beforeExit because the engine doesn't fully await its polling — this guarantees it prints last.

Testing (live)

Built a 5-component / 17-story demo Storybook and ran it against SmartUI:

  • Baseline: 51 screenshots, 0 changes.
  • After a one-line rebrand (2 theme tokens): 24 changes / 9 unchanged — semantic-colored components correctly matched; brand-colored ones flagged, with per-story mismatch %.
  • smartui --help, storybook --help, config:create-storybook, and a clean npm i -g <tarball> global install all verified.

Open questions for eng

  1. Backend endpoint ownership — the deeper, "elegant" integration (reuse createBuild/finalizeBuild) needs confirmation that the CLI's visualui/1.0 client can reach the /storybook/* routes. This PR sidesteps it by vendoring; that refactor is the documented follow-up.
  2. Versioning / deprecation — proposed 5.0.0 (MAJOR) + a deprecation shim on @lambdatest/smartui-storybook. Not done here.
  3. Tests — needs the project's own test suite + a security pass before any real release.

Not included (deliberately)

Live publish, official version bump, deprecation shim, docs updates.

🤖 Generated with Claude Code

Shrinish Vhanbatte and others added 2 commits June 1, 2026 15:15
Replace long-lived NPM_TOKEN with GitHub Actions OIDC. Adds
id-token: write permission and --provenance flag on publish;
removes NODE_AUTH_TOKEN env. Trusted publisher must be configured
on npmjs.com (LambdaTest org > smartui-cli > Publishing access)
before this workflow can succeed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
chore: switch npm publish to OIDC trusted publishing

@sushobhit-lt sushobhit-lt left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Automated review of the storybook-unification PR. Scope: the hand-written TS/.cjs glue (vendored engine reviewed for defects but treated as a faithful relocation). Lint/syntax: the repo has no ESLint/Prettier config and no lint script; tsconfig.json is strict with noUncheckedIndexedAccess. No parse-level syntax errors found in the changed source. Main findings are in the URL-mode capture path (sendDoM in src/storybookVendor/commands/utils/dom.cjs and the URL branch of storybook.cjs), which appears untested — the described live test looks like it exercised DIR/static mode only. Details inline.

await page.goto(storyInfo.url, { waitUntil: 'networkidle0' });
const html = await page.content();

dom = new JSDOM(html, {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[bug] dom is assigned with no declaration keyword (implicit global) — and so are clone (line 34) and element (line 37, the for…of binding). This is the exact class of defect the PR description says was fixed for res/filename/githubURL ("only crash once bundled into strict mode"), but these three were missed. esbuild bundles the ESM entry (src/index.ts), so this code runs strict → an assignment to an undeclared identifier throws ReferenceError: dom is not defined, breaking the whole URL-mode capture path. Even in sloppy mode it leaks JSDOM instances onto the global object on every story iteration. Declare it:

Suggested change
dom = new JSDOM(html, {
const dom = new JSDOM(html, {

url: storybookUrl,
resources: 'usable'
});
clone = new JSDOM(html);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[bug] Same implicit-global issue — declare clone:

Suggested change
clone = new JSDOM(html);
const clone = new JSDOM(html);

clone = new JSDOM(html);

// Serialize DOM
for(element of clone.window.document.querySelectorAll('img')) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[bug] Same implicit-global issue — the loop variable element is undeclared:

Suggested change
for(element of clone.window.document.querySelectorAll('img')) {
for (const element of clone.window.document.querySelectorAll('img')) {


// Convert browsers and resolutions arrays to string
let resolutions = [];
storybookConfig.resolutions.forEach(element => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[bug] In URL mode with no -c flag, storybookConfig falls back to defaultSmartUIConfig.storybook, which defines viewports but no resolutions key (see utils/config.cjs). So smartui storybook <url> without a config file throws TypeError: Cannot read properties of undefined (reading 'forEach') right here. (With a config file, validateConfig normalizes viewportsresolutions, which is why DIR-mode testing wouldn't surface it.) Guard with the viewports fallback:

Suggested change
storybookConfig.resolutions.forEach(element => {
(storybookConfig.resolutions || storybookConfig.viewports || []).forEach(element => {

@@ -0,0 +1,105 @@
// @ts-nocheck

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[nit] // @ts-nocheck disables all type checking for this new file in an otherwise strict project, so future regressions here (e.g. the options/target handling, the regex grab helper) won't be caught by tsup/tsc. Prefer typing the command callback params (commander infers them) and dropping the blanket suppression, or scope suppressions to the few lines that actually need them.

chaitanyas-maker pushed a commit that referenced this pull request Jul 10, 2026
Two bugs that broke the URL-mode capture path (surfaced by live URL testing,
confirmed by @sushobhit-lt review on PR #1):

- storybook.ts: merge root global flags via optsWithGlobals() so `--config`
  actually reaches the engine (local -c/--config collided with the global one),
  letting validateConfig normalize viewports->resolutions.
- dom.cjs: declare `dom`/`clone`/`element` with const — they were implicit
  globals that throw ReferenceError once bundled into strict mode (same class
  as the earlier res/filename/githubURL fixes; these three were missed).
- storybook.cjs: guard the URL-mode resolutions loop with a viewports fallback
  so `smartui storybook <url>` with no -c flag doesn't crash on the default
  config (per review).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CwQRZwr64F9fLQAW4V1bWy
@chaitanyas-maker

Copy link
Copy Markdown
Owner Author

Thanks @sushobhit-lt — sharp review; all three [bug] items are addressed in 850ee15:

  • dom.cjs:30/34/37 — declared dom / clone / element with const (as suggested). You're right these were the same implicit-global class as the res/filename/githubURL fixes and were missed by the first scan.
  • storybook.cjs:23 — added the (resolutions || viewports || []) fallback guard so smartui storybook <url> with no -c flag doesn't crash on the default config.
  • storybook.ts — also fixed a related root cause you'll want to know about: the command's local -c/--config collided with the root global, so --config never reached the engine and validateConfig never ran. Now merged via optsWithGlobals(). (Your storybook.cjs guard covers the no-config path; this covers the with-config path.)

You were exactly right that the live test only exercised DIR/static mode — I've since run URL mode end-to-end. After these fixes it completes the full pipeline (discover → puppeteer render → serialize → POST /storybook/render → build → finalize), but the backend returns Total Screenshots: 0 for both 17-story and 1-story runs. That points at the DOM-upload render path rather than the CLI glue — likely the same "backend endpoint ownership" open question in the PR description. DIR mode is fully green (51 screenshots, 24 diffs).

@ts-nocheck nit: acknowledged. Keeping it for now only because the vendored .cjs import has no type declarations; I'll scope it down / type the callback params as a follow-up rather than blanket-suppress.

@chaitanyas-maker

Copy link
Copy Markdown
Owner Author

Moved upstream: this is now proposed as a draft PR against LambdaTest/smartui-cliLambdaTest#527 (includes the fixes from @sushobhit-lt's review here). Continuing review there.

shrinishLT and others added 24 commits July 13, 2026 17:09
…, fullPage box synthesis

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… no longer suppresses selectors

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…elector-group flattening

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…enshot

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat(TE-20613): ignoreColors snapshot option — selectors, coordinates, fullPage
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
launchBrowsers() launched the capture browser with { headless } only, never
applying constants.LAUNCH_ARGS (used by the discovery path at
processSnapshot.ts). navigator.webdriver stayed true, so WAF-protected sites
(e.g. michaelkors.co.uk via Akamai) served an Access Denied page that was
screenshotted and uploaded as a successful build.

- utils.ts: add args: constants.LAUNCH_ARGS to shared launchOptions
- utils.ts: Edge launches inline with [...LAUNCH_ARGS, '--headless=new'];
  stop mutating shared launchOptions (leaked --headless=new onto the later
  mobile-Android Chromium launch)
- constants.ts: fix truncated CHROME_USER_AGENT (Safari/537.3 -> 537.36)

Firefox/WebKit ignore the Chromium-only flags (verified on Playwright 1.59.1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tag nesting rewrite + expression-safe IIFE wrap

Bundle built from smartui-cli-dom-serializer TE-21960 (LambdatestIncPrivate/smartui-cli-dom-serializer#13):
- rewrites button-in-button / a-in-a (invalid HTML from JS-built DOMs) to
  <div data-smartui-original-tag=...> so the serialized string survives
  re-parsing at render without flattening/reparenting (phantom clicked/open UI)
- bundle now deterministically starts with '(' via post-build IIFE wrap
  (page.evaluate-safe; replaces the rebuild-until-good-roll ritual)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…hat blocks OIDC trusted publishing

setup-node with registry-url writes an .npmrc with _authToken=${NODE_AUTH_TOKEN}
and exports the literal placeholder XXXXX-XXXXX-XXXXX-XXXXX when no token env is
set (visible in run 29919621698's step env). PR LambdaTest#525 removed the NPM_TOKEN env
from publish steps, so publishes now authenticate with the placeholder — npm
masks the auth failure as '404 @lambdatest/smartui-cli@4.1.72 is not in this
registry', and the configured token prevents the OIDC exchange from running at
all. Dropping registry-url removes the token config so pnpm publish uses
trusted publishing; pnpm pinned to v10 (OIDC support).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix(release): remove placeholder NODE_AUTH_TOKEN blocking OIDC publish (4.1.72 unpublished)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ase-oidc-auth

Revert "fix(release): remove placeholder NODE_AUTH_TOKEN blocking OIDC publish (4.1.72 unpublished)"
…m-oidc-stage

Revert "chore: switch npm publish to OIDC trusted publishing"
shrinishLT and others added 27 commits July 24, 2026 17:21
…essChrome WAF block

Headless Chromium leaks 'HeadlessChrome' into the Sec-CH-UA client hint. WAFs
like Akamai 403 on it (michaelkors.co.uk served Access Denied only on Chrome;
Firefox/WebKit send no client hints and were never blocked). Seed the clean
constants.REQUEST_HEADERS Sec-CH-UA on the capture page for Chromium engines
only (chrome/edge); user-supplied requestHeaders still override. Verified
end-to-end: michaelkors 403 -> 200, real homepage rendered.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…s them

Playwright's Linux WebKit build can't decode AVIF, so sites serving AVIF (e.g.
michaelkors.co.uk, 44 avif images) render blank with alt-text on the capture VM;
macOS WebKit and Chromium decode AVIF and were unaffected. For WebKit captures
only, reject avif/webp on image requests (Accept: image/avif;q=0,image/webp;q=0,
image/*,*/*;q=0.8) so the origin content-negotiates down to JPEG/PNG. Uses
route.fallback so it composes with the CAPTURE_RENDERING_ERRORS / basicAuth
handlers. Verified against the live site: images served as JPEG/PNG, no avif.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…WebKit route

/simplify cleanups on the capture-hardening diff:
- add isChromiumEngine/isWebkitEngine predicates in utils, replacing inline
  browserName string checks at the Sec-CH-UA seed and WebKit AVIF route
- Edge launch reuses chromiumLaunchOptions base instead of re-listing LAUNCH_ARGS
- collapse the WebKit route's duplicated route.fallback into one call

No behavior change (allHeaders() kept — request.headers() would drop cookies).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n, scope client hints, harden WebKit Accept

- utils.ts: revert launchBrowsers to stage — the Sec-CH-UA override (not launch
  args) fixes Access Denied; applying LAUNCH_ARGS to chrome/android capture (which
  never had args) would diff every existing baseline and also hit the double
  --disable-features bug. Keep only the engine predicates.
- screenshot.ts: gate the CAPTURE_RENDERING_ERRORS route's REQUEST_HEADERS on
  isChromiumEngine so Chromium client hints no longer leak onto WebKit/Firefox.
- screenshot.ts: WebKit image route now only rewrites Accept when it actually
  requests avif/webp (preserves a user's custom Accept) and early-returns non-image
  requests.
- constants.ts: fix malformed sec-ch-ua-mobile '"?0"' -> '?0'.
- trim long comments per repo convention.

Verified: Sec-CH-UA override with plain headless (navigator.webdriver=true) -> MK 200
real page; WebKit route still forces JPEG/PNG (no avif).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(TE-21958): webscanner capture — fix Chrome Access-Denied (Sec-CH-UA) and WebKit AVIF blank images
… hang the build

On rate-limited/WAF-protected sites the target can crash mid-capture (page.goto:
Page crashed / setViewportSize: Target crashed). page.close()/context.close()
then never resolve on the dead target, so the capture promise never settles,
Promise.allSettled never returns and finalizeBuild is unreachable — the build
stays 'running' until the job times out (seen as ~30 minute scans). Evidence
from an instrumented run: 12 'closing page/context' vs 11 'page/context closed'.

Race the teardown against BROWSER_CLOSE_TIMEOUT (15s) and continue if it does
not return; closeBrowsers() force-kills the browser process afterwards.

Scope: this fixes only the hang, so the build completes quickly. Screenshots may
still show Access Denied after the first few captures — that is the customer's
WAF rate limiting and is resolved only once the HYE VMs are whitelisted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(TE-23055): bound page/context teardown so a crashed target cannot hang the web scanner build
Added moved status to pass build result
…command

Vendor the proven @lambdatest/smartui-storybook v1.1.32 engine into the single
smartui binary (faithful relocation hitting the same /storybook/* backend), add
config:create-storybook, and resolve the colliding-bin problem. One install, one
binary. Version 5.0.0-unified.1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CwQRZwr64F9fLQAW4V1bWy
…ent, log-symbols)

Gradient figlet banner + launch config box + end-of-run summary box (dashboard
link, screenshot/approval/change counts) around the storybook command. Summary
deferred to process 'beforeExit' so it lands after the engine's polling. Verified
with a live baseline build (11 stories -> 33 screenshots) on SmartUI cloud.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CwQRZwr64F9fLQAW4V1bWy
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CwQRZwr64F9fLQAW4V1bWy
Two bugs that broke the URL-mode capture path (surfaced by live URL testing,
confirmed by @sushobhit-lt review on PR #1):

- storybook.ts: merge root global flags via optsWithGlobals() so `--config`
  actually reaches the engine (local -c/--config collided with the global one),
  letting validateConfig normalize viewports->resolutions.
- dom.cjs: declare `dom`/`clone`/`element` with const — they were implicit
  globals that throw ReferenceError once bundled into strict mode (same class
  as the earlier res/filename/githubURL fixes; these three were missed).
- storybook.cjs: guard the URL-mode resolutions loop with a viewports fallback
  so `smartui storybook <url>` with no -c flag doesn't crash on the default
  config (per review).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CwQRZwr64F9fLQAW4V1bWy
…TE-24909)

URL mode fetched stories.json only. Storybook 8 dropped that file and serves
index.json instead, so the request 404s, discovery reports "Cannot fetch
stories" and the run ends with 0 screenshots. DIR mode already read either file.

Try index.json first and fall back to stories.json, accepting both payload
shapes (entries for v8, stories for v7) and both title fields (title for v8,
kind for v7). Verified against a real Storybook 8.6 index (17 stories) served
with index.json only and with stories.json only: before the change the first
case failed with 404, after it resolves 17 stories from either file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017LYhe6ASMZLy7LSuAJ8pqQ
…po (TE-24912, TE-24910)

Two auth-adjacent problems in the vendored engine:

PROJECT_NAME (TE-24912). The engine only accepted a pre-issued PROJECT_TOKEN,
while the rest of the CLI accepts PROJECT_NAME and auto-creates the project.
Resolve PROJECT_NAME through the same /visualui/1.0/token/verify call the core
client uses, then continue on the resulting token, so downstream code is
unchanged.

No git repo (TE-24910). The duplicate-build check keys off branch and commit.
Outside a git repo both are empty, the API rejects the call, and the CLI prints
"Cannot fetch latest build of the project. Error: Request failed with status
code 401", which reads like an auth failure. Skip the check with a message
naming the real cause, matching how the core CLI skips git details when it is
not a git repo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017LYhe6ASMZLy7LSuAJ8pqQ
…nch failures (TE-24911)

This branch switched the lockfile to pnpm, and pnpm 10 blocks dependency install
scripts by default. puppeteer's postinstall never runs, so its Chromium is never
downloaded and URL mode dies on a WS endpoint timeout that says nothing about the
missing browser.

Declare the packages whose install scripts are required in
pnpm.onlyBuiltDependencies, and wrap puppeteer.launch so a failure names the
likely cause and the command that fixes it.

The lockfile change is the playwright bump that came in from stage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017LYhe6ASMZLy7LSuAJ8pqQ
… refusing (TE-23033)

config:create and config:create-storybook both default to .smartui.json, so
running the second after the first only produced "SmartUI Storybook config
already exists". The schema already allows one file to carry both a web and a
storybook block, so write the block into the existing file instead. An existing
storybook block is still left alone and reported.

Also give the storybook browsers uniqueItems rule its own message, which the
enum catch-all was swallowing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017LYhe6ASMZLy7LSuAJ8pqQ
@chaitanyas-maker

Copy link
Copy Markdown
Owner Author

Superseded by LambdaTest#527, which now carries the same branch rebased onto current stage. This PR targets prod, so after the rebase its diff also shows every stage-only commit, which makes it misleading to review. Closing in favour of LambdaTest#527.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants