Skip to content

Add extended traits, project config, and setup plugin#79

Merged
obj-p merged 21 commits intomainfrom
worktree-config-traits-setup
Apr 12, 2026
Merged

Add extended traits, project config, and setup plugin#79
obj-p merged 21 commits intomainfrom
worktree-config-traits-setup

Conversation

@obj-p
Copy link
Copy Markdown
Owner

@obj-p obj-p commented Apr 11, 2026

Summary

Three complementary features that give developers the fidelity of running their app with the nimbleness of SwiftUI previews — eliminating the need for micro apps / dev apps.

  • Extended traits: locale, layoutDirection, legibilityWeight added to preview_configure and preview_start, with new variant presets (rtl, ltr, boldText). Empty string clears a trait. Locale injection validated against code injection. Layout direction and legibility weight validated against enum sets.
  • Project config (.previewsmcp.json): auto-discovered config file for platform, device, trait defaults, and quality. Precedence: explicit param > config > default. Gives AI agents project intent without inference.
  • Setup plugin (PreviewsSetupKit): protocol-based system replacing micro apps. setUp() runs once per session (outside hot-reload path) for SDK init, auth, fonts, DI container setup. wrap() runs every render for theme providers and environment values. PreviewsMCP builds the setup package independently via SetupBuilder — the user's app target has zero dependency on PreviewsMCP. Trait modifiers applied outside wrap so explicit overrides always win.

Architecture

.previewsmcp.json (setup.packagePath → PreviewSetup/)
        │
        ▼
SetupBuilder ──swift build──► PreviewSetup/.build/
        │                          ├── Modules/ToDoPreviewSetup.swiftmodule
        │                          └── libToDoPreviewSetup.a (archived by SetupBuilder)
        ▼
BridgeGenerator ──── import ToDoPreviewSetup ────► bridge.dylib
        │               @_cdecl("previewSetUp")      │
        │               ToDoPreviewSetup.wrap()       │
        ▼                                             ▼
    Host app calls previewSetUp once ──► createPreviewView on every reload

The user's app Package.swift is completely untouched. Setup works across SPM, Xcode, and Bazel.

Changes

  • Phase 1: Extended traits — 3 new properties, validation (including injection prevention), presets, .environment() modifiers, MCP schemas, CLI flags
  • Phase 2: Project config — ProjectConfig type + ProjectConfigLoader, MCP/CLI integration with --config flag, quality field used in snapshot/variants
  • Phase 3: Setup plugin — PreviewsSetupKit library, SetupBuilder (builds setup package, archives .o → .a, extracts compiler flags), @_cdecl("previewSetUp") with async bridge, hasCalledSetUp in both host apps, full pipeline wiring
  • Phase 4: Documentation — CLAUDE.md + README.md updated
  • Phase 5: Example — shared .previewsmcp.json at examples/, standalone PreviewSetup/ package with observable state + preview banner, tested across all 4 build systems
  • Security fixes: locale injection prevention, setup identifier validation
  • Review fixes: trait clearing via empty string, config quality wiring, MCP schema enum constraints

Test coverage

  • 67 unit tests passing (55 trait/setup + 10 config + 2 security)
  • Integration tested across SPM, xcodeproj, xcworkspace, Bazel with setup plugin active

Test plan

  • swift test --filter BridgeGeneratorTraitsTests — 57 tests for traits + setup code gen + security
  • swift test --filter ProjectConfigTests — 10 tests for config decoding + directory walking
  • All 4 example projects render with setup banner (spm, xcodeproj, xcworkspace, bazel)
  • Trait variants (light/dark/rtl/boldText) produce correct snapshots
  • Dark + RTL traits compose correctly with setup wrapper
  • Empty-string trait clearing works
  • Locale injection rejected (quotes, backslashes, newlines)
  • Invalid setup identifiers rejected (non-Swift-identifier characters)

🤖 Generated with Claude Code

@obj-p obj-p marked this pull request as ready for review April 11, 2026 19:18
@obj-p obj-p force-pushed the worktree-config-traits-setup branch from dc95d12 to cec52c0 Compare April 11, 2026 20:48
obj-p and others added 19 commits April 11, 2026 17:51
…roject config

Phase 1: Extended built-in traits
- Add locale, layoutDirection, legibilityWeight to PreviewTraits with validation
- New variant presets: "rtl", "ltr", "boldText"
- Empty string clears a trait (resolves to nil)
- BridgeGenerator emits .environment() modifiers for new traits
- MCP tool schemas updated for preview_start, preview_configure, preview_variants
- CLI flags added to run, snapshot commands
- 18 new unit tests (60 total across traits + config)

Phase 2: Project config file (.previewsmcp.json)
- ProjectConfig type with platform, device, traits, quality, setup fields
- ProjectConfigLoader walks up directories to auto-discover config
- MCP server loads and caches config per source file directory
- CLI commands accept --config flag (explicit or auto-discover)
- Precedence: explicit param > config > built-in default
- 10 new unit tests for config decoding and directory walking

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 3: Setup plugin — replaces micro apps / dev apps

- New `PreviewsSetupKit` SPM library with `PreviewSetup` protocol
  - `setUp()` (async throws): runs once per session for SDK init, auth, fonts
  - `wrap(_:)`: wraps every preview for theme providers, environment values
- BridgeGenerator generates `@_cdecl("previewSetUp")` entry point with
  Task+semaphore bridge for async setUp
- Trait modifiers applied OUTSIDE wrap so explicit overrides take precedence
- Both host apps (macOS + iOS) gain `hasCalledSetUp` flag — setUp called
  on first dylib load only, completely outside the hot-reload path
- Setup params threaded through BuildContext, PreviewSession,
  IOSPreviewSession, BuildHelpers, MCPServer, and RunCommand
- Config `setup.moduleName`/`setup.typeName` wired from .previewsmcp.json
- Standalone mode warns when setup is configured but no build system found
- 5 new unit tests for setup code generation (65 total)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- CLAUDE.md: update Trait Injection with all 5 traits, add Project Config
  and Setup Plugin sections, update Architecture for PreviewsSetupKit
- README.md: add locale/layoutDirection/legibilityWeight examples, rtl/ltr/boldText
  presets, project config section, setup plugin section with adoption guide
- Add example .previewsmcp.json to SPM example project

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Security fixes:
- Validate locale values reject quotes, backslashes, and newlines to prevent
  code injection via string interpolation in generated Swift source
- Validate setupModule/setupType are valid Swift identifiers before
  interpolation into generated import statements and function calls

Feature completeness:
- Thread setupModule/setupType from config to SnapshotCommand and
  VariantsCommand (both macOS and iOS paths were missing setup params)
- Apply config quality to MCP snapshot and variants handlers
  (was hardcoded to 0.85, now respects .previewsmcp.json quality field)
- Remove enum constraints from preview_configure schema for
  layoutDirection and legibilityWeight so empty-string clearing works

Tests:
- Add locale injection rejection test
- Add setup identifier validation test (67 total)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds a ToDoPreviewSetup target demonstrating the PreviewSetup protocol:
- AppTheme with custom brand colors and fonts via EnvironmentKey
- setUp() with commented examples of Firebase, auth, and DI setup
- wrap() injects theme and tint into every preview
- .previewsmcp.json updated to reference the setup target

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Move .previewsmcp.json to examples/ so all example variants (spm,
  xcodeproj, xcworkspace, bazel) inherit it via directory walking
- Move setup target to examples/PreviewSetup/ as a standalone package
  so it's independent of any specific example project
- Restore spm/Package.swift to its original dependencies (no PreviewsMCP dep)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…/spm/

The shared .previewsmcp.json at examples/ should not include setup config
since only the SPM example has the ToDoPreviewSetup module. The xcodeproj,
xcworkspace, and bazel examples would fail trying to import it.

Fix: shared config has platform/traits/quality only. SPM example gets its
own config with the setup section. Config discovery walks up directories,
so the SPM config (closer) is found first and the shared one is used by
the other examples.

Tested all four examples: spm, xcodeproj, xcworkspace, bazel — all pass
with snapshot, traits (dark + RTL), and variants (light/dark/rtl presets).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The setup plugin was incorrectly requiring the user's app target to
declare a dependency on PreviewsSetupKit. The correct architecture:
PreviewsMCP discovers and builds the setup package itself.

New SetupBuilder:
- Reads setup.packagePath from .previewsmcp.json (relative to config dir)
- Runs `swift build` on the setup package independently
- Extracts .swiftmodule and library paths from the build output
- Passes compiler flags (-I, -L, -l) to the bridge compilation

The user's app target has zero knowledge of PreviewsMCP or PreviewsSetupKit.
Only the setup package (a separate SPM package) depends on PreviewsSetupKit.

Changes:
- New SetupBuilder in PreviewsCore — builds setup package, returns flags
- ProjectConfig.SetupConfig gains packagePath field
- PreviewSession gains setupCompilerFlags for bridge compilation
- All CLI commands and MCP handlers use SetupBuilder instead of passing
  raw module/type names
- Tested end-to-end: SPM example with setup, xcodeproj/bazel without

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SPM doesn't create static archives for library targets — it leaves
loose .o files. The bridge linker needs .a archives to resolve symbols.

SetupBuilder now archives all targets' .o files (including transitive
dependencies) into lib<Target>.a before passing -l flags, matching the
same pattern used by SPMBuildSystem.archiveDependencyTargets().

This also means setup packages with their own SPM dependencies will
link correctly — all dependency targets get archived and linked.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the print-statement example with a compelling demonstration:

- PreviewEnvironment: @observable singleton configured in setUp() with
  mock user state (name, subscription tier, feature flags). Persists
  across hot-reload because setUp() runs once per session.

- PreviewBanner: overlay at bottom of every preview showing the mock
  user ("dev@example.com PRO") — visible proof that the setup plugin
  is active and what state it configured.

- Brand theme: purple accent tint applied via wrap().

Demonstrates the three key value props:
1. setUp() does real work (configures observable state)
2. wrap() makes it visible (banner + tint on every preview)
3. Composes with traits (dark mode, RTL, dynamic type all work)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All four examples (spm, xcodeproj, xcworkspace, bazel) now share the
same setup plugin config. PreviewsMCP builds the setup package
independently via SetupBuilder — the example's build system (SPM,
Xcode, Bazel) doesn't need to know about it.

Tested: all four examples render with the PreviewBanner and brand tint.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- swift-format --in-place across Sources/, Tests/, examples/PreviewSetup/
- Document previewSetUp lifecycle in docs/communication-protocol.md

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Important fixes:
- Config quality now resolves for both macOS and iOS sessions in MCP
  snapshot/variants handlers via new configQualityForSession() helper
- CLI SnapshotCommand applies config quality instead of hardcoding 0.85
- CLI VariantsCommand --quality flag falls back to config then 0.85

Suggestions fixed:
- isValidSwiftIdentifier regex rejects trailing/double dots:
  ^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$
- SetupBuilder caches iOS SDK path (single xcrun call instead of two)

67 tests pass, all examples verified.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Config-sourced traits (from .previewsmcp.json) previously bypassed
validation via TraitsConfig.toPreviewTraits(), which constructed
PreviewTraits directly. A malicious locale in the config file could
have injected code through the string literal interpolation in
BridgeGenerator.traitModifiers().

Fix: toPreviewTraits() now calls PreviewTraits.validated() which
rejects locale values containing quotes, backslashes, and newlines.
Invalid config traits fall back to empty PreviewTraits().

Also fix standalone mode leaking setupCompilerFlags — set extraFlags
to [] since setup is explicitly not supported without a build system.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Other examples don't commit their lock files. Removes noise from diffs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The spm and PreviewSetup examples are SPM packages that generate
Package.resolved during builds. Other examples already have
.gitignore files for their generated artifacts (xcodeproj, bazel).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ge.swift

The README incorrectly showed adding the setup target to the user's
app Package.swift. The correct architecture: the setup package is a
separate standalone package that PreviewsMCP builds independently via
SetupBuilder. The user's app target has zero dependency on PreviewsMCP.

Updated README with:
- Standalone package directory structure
- Separate Package.swift for the setup package
- packagePath in config example
- Explanation that PreviewsMCP builds it independently

Updated CLAUDE.md to mention packagePath and SetupBuilder.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Use raw string literals instead of multiline strings inside Data()
initializers to avoid line-break disagreements between local and CI
swift-format versions (510 vs 602).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SetupBuilder builds the setup package for iOS simulator when the
preview target is iOS. The setup package depends on PreviewsMCP to
get PreviewsSetupKit. Without an iOS platform declaration, SPM falls
back to the default iOS deployment target, which is lower than what
swift-syntax and MCP SDK require — causing build failures in CI.

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

SetupBuilder was building the example setup package for iOS, which
depended on the full PreviewsMCP package. SPM resolved the entire
dependency tree (swift-syntax, MCP SDK, swift-nio, etc.) adding 10+
minutes to the iOS integration test, causing a 600-second timeout.

Fix: bundle a local copy of PreviewsSetupKit (37 lines, SwiftUI only)
inside the example setup package. Build time drops from 10+ minutes
to 2 seconds — zero transitive dependencies to resolve.

This mirrors what real-world users should do: depend on PreviewsSetupKit
as a lightweight package, not the full PreviewsMCP.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@obj-p obj-p force-pushed the worktree-config-traits-setup branch from 6a55383 to 7ede7a1 Compare April 12, 2026 00:45
…ory walk

1. Remove dead setupModuleName/setupCompilerFlags from BuildContext —
   setup data flows through PreviewSession from SetupBuilder.Result,
   these fields were never populated by any build system

2. Log warning to stderr when config traits fail validation instead of
   silently returning empty traits (e.g., "colorScheme": "Blue" typo)

3. Eliminate double directory walk — ProjectConfigLoader.find() now
   returns Result(config, directory) so the config directory is
   available without a second walk for SetupBuilder

4. Add provenance comment to LocalPreviewsSetupKit copy noting it
   should be kept in sync with Sources/PreviewsSetupKit

5. Tests updated to verify config directory is returned correctly

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@obj-p obj-p merged commit 2b8494f into main Apr 12, 2026
4 checks passed
@obj-p obj-p deleted the worktree-config-traits-setup branch April 12, 2026 02:29
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.

1 participant