[]previous and next page

Project structure

How a ShadowJS app is laid out on disk, what each file does, and which package provides which part of the framework.

For agentsUpdated

A ShadowJS app is an ordinary Vite project. The framework adds one plugin, one TypeScript setting and two runtime packages. There is no required folder layout, router convention or config file of its own.

The files every app has#

text
my-app/
  index.html          page shell: <div id="app"> and the module script
  package.json
  tsconfig.json       strict TypeScript, jsx = preserve, jsxImportSource = @shadow-js/web
  vite.config.ts      plugins: [shadow()]
  src/
    main.tsx          entry point: render(() => <App />, #app)
    App.tsx           the root component
    index.css
    vite-env.d.ts     /// <reference types="vite/client" />
File Role
index.html Vite's entry. Contains the mount element and <script type="module" src="/src/main.tsx">.
src/main.tsx The only place that touches the DOM directly. Finds the container and calls render (or hydrate for server-rendered HTML).
src/App.tsx Your root component. Everything else is imported from here.
vite.config.ts Registers the ShadowJS plugin (options).
tsconfig.json Only type-checks; Vite and the plugin do the compiling (settings).

npm create shadow-app writes exactly these files; see Quick start.

A layout that scales#

Nothing below is enforced. It is the structure the repository's own todomvc example uses, which keeps state out of markup and gives each part a clear responsibility:

text
src/
  main.tsx
  App.tsx
  components/         presentational and container components (one per file)
  state/              hooks that own stores: useCounter, useTodos, ...
  model/              plain types and pure functions (no framework imports)
  storage/            persistence, network access

The example below follows that split. The hook owns the state and the operations, the component only renders, App connects them and main.tsx mounts.

src/state/use-counter.tsts
import { useStore } from "@shadow-js/core";

export function useCounter(start = 0) {
  const [count, setCount] = useStore(start);
  return {
    count,
    increment: () => setCount((n) => n + 1),
    reset: () => setCount(start),
  };
}
src/components/CounterView.tsxtsx
export interface CounterViewProps {
  count: number;
  onIncrement: () => void;
  onReset: () => void;
}

export function CounterView(props: CounterViewProps) {
  return (
    <div>
      <p>Count: {props.count}</p>
      <button type="button" onClick={() => props.onIncrement()}>
        Increment
      </button>
      <button type="button" onClick={() => props.onReset()}>
        Reset
      </button>
    </div>
  );
}
src/App.tsxtsx
import { CounterView } from "./components/CounterView";
import { useCounter } from "./state/use-counter";

export function App() {
  const counter = useCounter();
  return (
    <CounterView
      count={counter.count()}
      onIncrement={counter.increment}
      onReset={counter.reset}
    />
  );
}
src/main.tsxtsx
import { render } from "@shadow-js/web";
import { App } from "./App";

const root = document.getElementById("app");
if (root === null) throw new Error("Missing #app element");

render(() => <App />, root);

Note count={counter.count()}: the prop is compiled to a getter, so CounterView re-reads it when the count changes and CounterView's function body still runs once. See the mental model.

With server rendering#

A server-rendered app splits the entry in two and keeps App shared. The tutorial builds this in step 8.

text
src/
  App.tsx             shared
  entry-client.tsx    hydrate(() => <App />, #app)
  entry-server.tsx    renderToString(() => <App />)  (from @shadow-js/web/server)

Where each package fits#

Package Install as You import Provides
@shadow-js/core dependency useStore, useMemo, useEffect, onMount, onCleanup, createContext, useResource, batch, untrack, ... The reactive engine: signals, stores, memos, effects, owners, lists, resources. No DOM.
@shadow-js/core/extend (same package) createSignalNode, createComputation, createOwner, ... The extension API the built-ins are written against; for library authors (reference).
@shadow-js/web dependency render, hydrate, Show, For, Index, Switch, Match, Suspense, ErrorBoundary, lazy, Portal, Dynamic, Title, Meta, ... The DOM runtime, control-flow components, head management and the JSX types (@shadow-js/web/jsx-runtime).
@shadow-js/web/server (same package) renderToString, renderToStringAsync, renderToStream, serialize The server renderer and the helpers the server-target output calls.
@shadow-js/vite dev dependency shadow (default export) The Vite plugin that runs the compiler.
@shadow-js/compiler installed by the plugin compile The JSX compiler (DOM and server targets). Direct use is for tooling.
@shadow-js/testing dev dependency @shadow-js/testing/dom: render, userEvent, fireEvent, waitFor; renderHook, flush, captureWarnings Test helpers on top of Testing Library queries.
@shadow-js/router dependency createRouter, defineRoute, A, Outlet, useParams, ... Client routing. @shadow-js/router/core is the framework-independent part.
@shadow-js/dusk dependency dusk (@shadow-js/dusk/vite), createHandler, serverFn (@shadow-js/dusk/server), serve (@shadow-js/dusk/node) The full-stack layer: file routes, server functions, a secure request handler, a Node server. See Dusk.
@shadow-js/convex, @shadow-js/clerk dependency useQuery, useMutation, ClerkProvider, SignedIn, ... First-class integrations; convex is a peer dependency, and @clerk/clerk-js is an optional peer needed only by mode="bundled".
@shadow-js/forms, @shadow-js/forms/server dependency useForm, Form, Field, FieldArray, useWizard, applyServerErrors, parseFormData, ... Schema-validated forms with typed paths and field arrays. See Forms.
@shadow-js/i18n dependency createI18n, I18nProvider, useI18n, Trans, createI18nMiddleware, ... Typed ICU messages, lazy catalogs, locale routing, SSR with zero hydration mismatch. See Internationalization.
@shadow-js/ui dependency and the shadow-ui command cn, cva, ThemeProvider, useTheme, ... The shadcn-style kit: the CLI copies components built on @shadow-js/primitives into your project. See the UI kit.
create-shadow-app npm create n/a The project scaffolder.

@shadow-js/dusk is the full-stack layer: see Dusk.