Skip to content

Latest commit

 

History

History
398 lines (297 loc) · 14.5 KB

File metadata and controls

398 lines (297 loc) · 14.5 KB

Getting started: UI source to a running Solid application

This guide shows the complete two-pass flow. It deliberately keeps the UI-library build and application build separate because they consume different inputs and produce different artifacts.

What each repository is

pathscale/ui or your UI repo
  A: authored recipes, CSS, types and .layout.tsx templates
                              |
                              | B: solid-layouts library compiler
                              v
@pathscale/ui package
  C: generated TSX, compiled recipes, generated entries and manifest
                              |
application source D --------+ E: solid-layouts application compiler
                              v
normal Solid/Rsbuild output F

solid-layouts supplies B, E, and the shared runtime. It does not supply your component designs. UI/ is your component-library repository.

For Pathscale:

  • The full authored migration is the merged pathscale/ui PR #221; the current Layouts-only package is @pathscale/ui@2.0.0.
  • The working compiler fixture is Test-UI/ in this repository. It contains Icon and Button so every generated file remains easy to inspect while the application proof exercises real component markup changes.
  • The current real application consumer is Chuzz PR #9.

Do not point an application at raw UI source and do not copy generated files into an application. The UI release runs B and publishes C; applications consume only that package.

Current availability

The complete public package set is live:

  • solid-layouts@0.1.2: shared runtime
  • solid-layouts-oxc@0.1.6: native compiler hosts, CLI, and linter
  • rsbuild-plugin-solid-layouts@0.1.3: stable Rsbuild facade
  • @pathscale/ui@2.0.0: compiled Layout UI package C

Use registry package resolution for normal work. Object-shaped layout sources and an explicit runtime path remain development-only overrides for compiler fixtures.

Install the application dependencies

An application needs the compiled UI, the small runtime, and the build plugin:

bun add @pathscale/ui solid-layouts
bun add -d rsbuild-plugin-solid-layouts

The application does not install a local UI checkout or configure a runtime path. solid-layouts-oxc arrives through the plugin and selects the native package for the host platform.

Producer: author components as A

The producer owns the recipe, Layout template, CSS, and public component name. It does not hand-write the package entry.

1. Declare the recipe

Test-UI/src/components/icon/Icon.recipe.ts:

import { recipe } from "solid-layouts";

export const icon = recipe({
  component: "icon",
  element: "span",
  slots: { root: { base: "icon" } },
  props: { name: {}, width: {}, height: {} },
});

The recipe declares what can affect presentation and which slots the Layout may render. B compiles its static lookup table once; the runtime does not reconstruct the table for every Icon instance.

Presentation parameters belong in the producer recipe and Layout, not in application CSS. For example, the Test-UI Button exposes variant, size, squareSize, justify, and radius; a consumer can request <Button variant="outline" squareSize={32}> without rebuilding Button geometry in a local stylesheet. Application CSS should remain only for relationships the component cannot own, such as revealing a child control when its parent row is hovered.

Flex spacing follows the same rule. Application code uses named Layout parameters:

<Flex
  as="div"
  align="start"
  gap="md"
  paddingInline="md"
  paddingBlock="md"
>
  {children}
</Flex>

The recipe in A owns the concrete utility classes and spacing values. Strings such as px-3.5, py-3, and gap-4 must not be repeated in D.

JSX names are case-sensitive

Solid TSX and the application compiler distinguish intrinsic elements from component bindings by case:

<button>Native HTML</button>
<Button>Compiled Layout component</Button>

<button> is valid native HTML and deliberately bypasses Layout resolution. <Button> resolves the exact imported export from C and is processed by E. If an author accidentally writes <button> when they intended <Button>, E cannot infer that intent or report a missing template because the lowercase element is already valid HTML. Use the exact exported capitalization; custom linting can enforce project rules for controls that must use the UI package.

2. Author the Layout template

Test-UI/src/components/icon/Icon.layout.tsx contains template syntax:

import "./Icon.css";
import { createMemo } from "solid-js";
import { twMerge } from "tailwind-merge";
import type { Layout } from "solid-layouts";
import type { IComponentBaseProps, ComponentColor } from "../../types";
import { icon } from "./Icon.recipe";

export type IconProps = IComponentBaseProps & {
  width?: number;
  height?: number;
  color?: ComponentColor;
  name?: string;
};

const Icon: Layout<typeof icon, IconProps> = () => {
  const width = local.width ?? 24;
  const height = local.height ?? 24;

  const classes = createMemo(() =>
    twMerge(slot.root.class, local.name, local.class, local.className),
  );

  return (
    <span
      {...slot.root}
      {...{ class: classes() }}
      style={{
        width: `${width}px`,
        height: `${height}px`,
        ...(typeof local.style === "object" ? local.style : {}),
      }}
      data-theme={local.dataTheme}
    />
  );
};

export const IconLayout = Icon;

This file is intentionally not valid ordinary TSX. local and slot have no runtime declarations and the zero-parameter function cannot receive them. B is load-bearing: it rewrites this template before anything is packaged.

3. Use the discoverable source contract

No authored component manifest is required. B discovers every *.layout.tsx below src, reads its Layout<typeof recipe, Props> annotation and recipe import, and requires the matching public NameLayout export:

src/components/icon/
  Icon.layout.tsx
  Icon.recipe.ts

src/components/button/
  Button.layout.tsx
  Button.recipe.ts

The default source is src, the default output is bundle, and the generated public entry is index.ts. The relationship is explicit in the source rather than repeated in JSON. A missing or aliased recipe import, ambiguous Layout annotation, unexported props type, duplicate component name, or missing NameLayout export is a build error.

An explicit layouts.library.json remains an optional escape hatch for nonstandard paths and adjacent source-mode generation. Ordinary generated libraries should not need one.

4. Build B and produce C

Install the published compiler and run the library pass:

bun add -d solid-layouts-oxc
bun run build:layouts

The script executes solid-layouts-library --pack. B reads A and creates:

Test-UI/bundle/
  package.json
  index.ts
  layouts.manifest.json
  types.ts
  components/icon/
    Icon.css
    Icon.generated.tsx
    Icon.recipe.ts
  components/button/
    Button.css
    Button.generated.tsx
    Button.recipe.ts

Test-UI/artifacts/
  pathscale-test-ui-0.0.0.tgz

The authored .layout.tsx is not shipped. Its generated counterpart has a real function signature and model references:

const Icon: Layout<typeof icon, IconProps> = ({ slot, children }, p) => {
  const width = p.width ?? 24;
  const height = p.height ?? 24;
  // ...
};

B also owns bundle/index.ts:

import { defineComponent as __defineLayoutComponent } from "solid-layouts/application-boundary";
import { IconLayout } from "./components/icon/Icon.generated";
import { icon } from "./components/icon/Icon.recipe";

export const Icon = __defineLayoutComponent({
  recipe: icon,
  layout: IconLayout,
});

The boundary import is deliberate. Normal module resolution cannot satisfy it. E must validate C and resolve the boundary during the application build; removing E does not silently fall back to normal TSX.

The generated package.json includes:

{
  "name": "@pathscale/test-ui",
  "exports": {
    ".": {
      "types": "./index.ts",
      "import": "./index.ts"
    },
    "./layouts": "./layouts.manifest.json",
    "./package.json": "./package.json"
  },
  "solidLayouts": "./layouts.manifest.json"
}

solidLayouts is E's package-level discovery point. C is the artifact to publish to npm; A is not.

Consumer: use C from application D

Application code remains ordinary Solid TSX:

import { Icon } from "@pathscale/ui";

export function SaveAction() {
  return <Icon name="icon-[mdi--content-save]" width={18} />;
}

The application does not import a recipe, generated Layout, manifest, or compiler boundary. It imports the public component from C.

Final package configuration

After solid-layouts, solid-layouts-oxc, and the compiled @pathscale/ui C package are published, an Rsbuild application uses package resolution:

import { defineConfig } from "@rsbuild/core";
import { pluginBabel } from "@rsbuild/plugin-babel";
import { pluginSolid } from "@rsbuild/plugin-solid";
import { pluginSolidLayoutsApplication } from "rsbuild-plugin-solid-layouts";

export default defineConfig({
  plugins: [
    pluginSolidLayoutsApplication({
      layouts: ["@pathscale/ui"],
    }),
    pluginBabel({ include: /\.(?:jsx|tsx|ts)$/ }),
    pluginSolid(),
  ],
});

Order is load-bearing. E must parse application TSX before Babel/Solid lowers JSX to runtime calls.

At setup E:

  1. Resolves @pathscale/ui/package.json from the application.
  2. Requires solidLayouts.
  3. Reads and validates the manifest and every referenced entry, recipe, and generated Layout.
  4. Creates the exact public export index once for the build.
  5. Checks application imports against that index.
  6. Rewrites validated Layout-package imports to C's resolved public entry.
  7. Resolves solid-layouts/application-boundary to the shared runtime.

Local compiler-fixture override

Compiler fixtures may provide the package identity plus C's directory and runtime explicitly:

import { pluginSolidLayoutsApplication } from "../../../../solid-layouts/packages/solid-layouts-oxc/application.js";

pluginSolidLayoutsApplication({
  layouts: [
    {
      module: "@pathscale/test-ui",
      root: "../../../../solid-layouts/Test-UI/bundle",
    },
  ],
  runtime: "../../../../solid-layouts/packages/solid-layouts/src/index.ts",
})

TypeScript needs a path for the local C types, and the bundler needs the local runtime path. The bundler does not need a C alias: E rewrites @pathscale/test-ui to the validated absolute C entry. The object-shaped Layout source and runtime option are checkout overrides, not a second compiler mode and not the Chuzz configuration.

Inspect the Chuzz consumer

Chuzz PR #9 uses only published package resolution:

git clone https://github.com/pathscale/chuzz.git
git -C chuzz fetch origin pull/9/head:solid-layouts-ui
git -C chuzz switch solid-layouts-ui
cd chuzz/apps/chuzz/frontend
bun install --frozen-lockfile

Its rsbuild.config.ts contains only layouts: ["@pathscale/ui"]. Chuzz imports the published Button, Flex, Tabs, Disclosure, Modal, Surface, Text, ColorSwatch, and other components from @pathscale/ui. The application keeps CSS only for application-specific chrome structure and component relationships that the reusable recipes do not own.

Expected failures

There is no graceful fallback. These failures mean the contract is working:

Failure Where it stops Why
.layout.tsx does not name/import exactly one recipe B A cannot be joined deterministically
Layout renders an undeclared slot B Recipe and markup disagree
Recipe declares a slot the Layout never renders B C would carry dead or mistyped identity
Configured package cannot be resolved E setup C is absent
Package has no solidLayouts field E setup The package is not a valid C
Manifest is malformed, unsupported, or names another package E setup C cannot be trusted
Manifest points at a missing entry/recipe/Layout/export E setup C is internally inconsistent
Application imports a public export absent from the manifest E source pass D asked for a component C cannot prove
E is removed Normal module resolution C's compiler boundary intentionally remains unresolved

Tests and exact commands

Runtime

cd packages/solid-layouts
bun run test
bun run typecheck

This runs 143 tests under Solid's browser condition. The condition matters: Bun otherwise resolves Solid's server build, where reactivity tests can pass without exercising updates.

Native compiler and conformance corpus

cd packages/solid-layouts-oxc
cargo fmt --all --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace

The 54 Rust tests include parser/diagnostic tests, recipe compilation, exact import matching and rewriting, template rewriting, and the input/output fixture corpus under fixtures/.

JavaScript library/application hosts

cd packages/solid-layouts-oxc
./scripts/build-binding.sh
bun run test:library
bun run test:application

The library-host tests compile a temporary producer and assert generated TSX, entry boundaries, missing-recipe failures, and slot mismatches. The application-host tests resolve a real C fixture and cover absent exports, missing discovery metadata, corrupt/unsupported manifests, missing generated files, and entry/manifest disagreement.

Generated C freshness

cd Test-UI
bun ../packages/solid-layouts-oxc/bin/solid-layouts-library.js
cd ..
git diff --exit-code -- Test-UI/bundle

CI performs all four groups and fails if the committed C fixture no longer matches B.

Moving from Test-UI to the complete Pathscale UI

The safe migration order is:

  1. Check out PR #221 as A.
  2. Move one colocated Name.layout.tsx and Name.recipe.ts pair at a time.
  3. Run B and inspect that component's generated C diff.
  4. Add library-host failures for any new syntax shape before accepting it.
  5. Keep generated entries and manifests owned by B; do not restore hand-written wiring.
  6. Pack C and inspect its contents before publishing.
  7. Configure E against that C package in Chuzz.
  8. Move one Chuzz import at a time and require a successful application build.
  9. Confirm that removing E and importing a nonexistent manifest export both fail.
  10. Measure bundle and per-instance cost only after correctness is established.

The key boundary is always the same: application D consumes C. It never consumes raw A.