@apex-stack/core 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/{build-VHS6KZBK.js → build-XUDXRQ4W.js} +3 -3
  2. package/dist/{chunk-XDKJO6ZC.js → chunk-DVNFDYEO.js} +1 -1
  3. package/dist/{chunk-JLIAISWM.js → chunk-PMLGY6Z3.js} +14 -14
  4. package/dist/cli.js +5 -5
  5. package/dist/client.js +1 -1
  6. package/dist/{dev-G7HPP6KW.js → dev-74CABXDP.js} +1 -1
  7. package/dist/index.d.ts +25 -25
  8. package/dist/index.js +1 -1
  9. package/dist/{server-PTHGOE42.js → server-VQSL2KPO.js} +18 -4
  10. package/dist/{start-3O3E43PT.js → start-TBG2TEOE.js} +2 -2
  11. package/dist/{upgrade-WC5F5FKY.js → upgrade-GCRSV4IE.js} +1 -1
  12. package/package.json +3 -3
  13. package/templates/default/README.md +22 -14
  14. package/templates/default/components/Badge.alpine +6 -0
  15. package/templates/default/components/Button.alpine +10 -0
  16. package/templates/default/components/Card.alpine +6 -0
  17. package/templates/default/components/Counter.alpine +8 -13
  18. package/templates/default/layouts/default.alpine +37 -14
  19. package/templates/default/package.json +2 -1
  20. package/templates/default/pages/about.alpine +29 -0
  21. package/templates/default/pages/blog/[slug].alpine +38 -0
  22. package/templates/default/pages/blog/index.alpine +37 -0
  23. package/templates/default/pages/index.alpine +55 -26
  24. package/templates/default/server/api/posts.ts +20 -0
  25. package/templates/default/services/PostService.ts +51 -0
  26. package/templates/default/shared/types.ts +9 -4
  27. package/templates/default/tests/posts.test.ts +22 -0
  28. package/templates/default/server/api/hello.ts +0 -18
  29. package/templates/default/services/GreetingService.ts +0 -12
  30. package/templates/default/tests/greeting.test.ts +0 -12
@@ -7,8 +7,8 @@ import {
7
7
  renderPage,
8
8
  resolveApexConfig,
9
9
  scanPages
10
- } from "./chunk-XDKJO6ZC.js";
11
- import "./chunk-JLIAISWM.js";
10
+ } from "./chunk-DVNFDYEO.js";
11
+ import "./chunk-PMLGY6Z3.js";
12
12
 
13
13
  // src/commands/build.ts
14
14
  import { cpSync, existsSync as existsSync3, mkdirSync, readdirSync as readdirSync3, rmSync, writeFileSync } from "fs";
@@ -18,7 +18,7 @@ import { defineCommand } from "citty";
18
18
  import { createServer as createViteServer } from "vite";
19
19
 
20
20
  // src/build/buildClient.ts
21
- import { existsSync, readFileSync, readdirSync } from "fs";
21
+ import { existsSync, readdirSync, readFileSync } from "fs";
22
22
  import { join } from "path";
23
23
  import { apex } from "@apex-stack/vite";
24
24
  import { build } from "vite";
@@ -2,7 +2,7 @@ import {
2
2
  clientConfigScript,
3
3
  isApexStore,
4
4
  setRuntimeConfig
5
- } from "./chunk-JLIAISWM.js";
5
+ } from "./chunk-PMLGY6Z3.js";
6
6
 
7
7
  // src/config/resolve.ts
8
8
  import { existsSync, readFileSync } from "fs";
@@ -1,14 +1,3 @@
1
- // src/store.ts
2
- function defineStore(name, factory) {
3
- if (!name || /[^a-zA-Z0-9_$]/.test(name)) {
4
- throw new Error(`defineStore: invalid store name "${name}" \u2014 use letters, digits, _ or $.`);
5
- }
6
- return { __apexStore: true, name, factory };
7
- }
8
- function isApexStore(x) {
9
- return typeof x === "object" && x !== null && x.__apexStore === true;
10
- }
11
-
12
1
  // src/config/runtime.ts
13
2
  function defineConfig(config) {
14
3
  return config;
@@ -37,12 +26,23 @@ function env(key, fallback) {
37
26
  return fallback;
38
27
  }
39
28
 
29
+ // src/store.ts
30
+ function defineStore(name, factory) {
31
+ if (!name || /[^a-zA-Z0-9_$]/.test(name)) {
32
+ throw new Error(`defineStore: invalid store name "${name}" \u2014 use letters, digits, _ or $.`);
33
+ }
34
+ return { __apexStore: true, name, factory };
35
+ }
36
+ function isApexStore(x) {
37
+ return typeof x === "object" && x !== null && x.__apexStore === true;
38
+ }
39
+
40
40
  export {
41
- defineStore,
42
- isApexStore,
43
41
  defineConfig,
44
42
  setRuntimeConfig,
45
43
  clientConfigScript,
46
44
  useRuntimeConfig,
47
- env
45
+ env,
46
+ defineStore,
47
+ isApexStore
48
48
  };
package/dist/cli.js CHANGED
@@ -14,7 +14,7 @@ import { defineCommand as defineCommand2, runMain } from "citty";
14
14
 
15
15
  // src/commands/new.ts
16
16
  import { spawn, spawnSync } from "child_process";
17
- import { cpSync, existsSync, readFileSync, readdirSync, renameSync, writeFileSync } from "fs";
17
+ import { cpSync, existsSync, readdirSync, readFileSync, renameSync, writeFileSync } from "fs";
18
18
  import { basename, join, resolve } from "path";
19
19
  import { fileURLToPath } from "url";
20
20
  import { defineCommand } from "citty";
@@ -147,13 +147,13 @@ var main = defineCommand2({
147
147
  },
148
148
  subCommands: {
149
149
  new: newCommand,
150
- dev: () => import("./dev-G7HPP6KW.js").then((m) => m.devCommand),
151
- build: () => import("./build-VHS6KZBK.js").then((m) => m.buildCommand),
152
- start: () => import("./start-3O3E43PT.js").then((m) => m.startCommand),
150
+ dev: () => import("./dev-74CABXDP.js").then((m) => m.devCommand),
151
+ build: () => import("./build-XUDXRQ4W.js").then((m) => m.buildCommand),
152
+ start: () => import("./start-TBG2TEOE.js").then((m) => m.startCommand),
153
153
  make: () => import("./make-VAYO5GWA.js").then((m) => m.makeCommand),
154
154
  add: () => import("./add-M3YLIFF5.js").then((m) => m.addCommand),
155
155
  theme: () => import("./theme-UUOIV44V.js").then((m) => m.themeCommand),
156
- upgrade: () => import("./upgrade-WC5F5FKY.js").then((m) => m.upgradeCommand),
156
+ upgrade: () => import("./upgrade-GCRSV4IE.js").then((m) => m.upgradeCommand),
157
157
  migrate: () => import("./migrate-X6LIHMIE.js").then((m) => m.migrateCommand),
158
158
  mcp: () => import("./mcp-CH7L4GF3.js").then((m) => m.mcpCommand)
159
159
  },
package/dist/client.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/client.ts
2
- import { registerApexComponent, createAction } from "@apex-stack/kit/client";
2
+ import { createAction, registerApexComponent } from "@apex-stack/kit/client";
3
3
  export {
4
4
  createAction,
5
5
  registerApexComponent
@@ -24,7 +24,7 @@ var devCommand = defineCommand({
24
24
  process.stdout.write(banner());
25
25
  const sp = spinner(`Starting dev server${args.islands ? " (islands mode)" : ""}\u2026`);
26
26
  try {
27
- const { startDevServer } = await import("./server-PTHGOE42.js");
27
+ const { startDevServer } = await import("./server-VQSL2KPO.js");
28
28
  const { port: actual } = await startDevServer({ root, port, islands: Boolean(args.islands) });
29
29
  sp.succeed("Dev server ready");
30
30
  ready([
package/dist/index.d.ts CHANGED
@@ -124,31 +124,6 @@ interface ApexResource {
124
124
  }
125
125
  declare function isApexResource(x: unknown): x is ApexResource;
126
126
 
127
- type StoreState = Record<string, unknown>;
128
- interface ApexStore {
129
- readonly __apexStore: true;
130
- readonly name: string;
131
- readonly factory: () => StoreState;
132
- }
133
- /**
134
- * Define a global, SSR-safe store shared across every page, component, and island.
135
- *
136
- * ```ts
137
- * // stores/cart.ts
138
- * import { defineStore } from '@apex-stack/core'
139
- * export default defineStore('cart', () => ({
140
- * items: [] as string[],
141
- * get count() { return this.items.length },
142
- * add(x: string) { this.items.push(x) },
143
- * }))
144
- * ```
145
- *
146
- * Access it anywhere as `$store.cart` — `$store.cart.count` renders on the server
147
- * and stays reactive after hydration.
148
- */
149
- declare function defineStore(name: string, factory: () => StoreState): ApexStore;
150
- declare function isApexStore(x: unknown): x is ApexStore;
151
-
152
127
  /** The short-circuit value returned by `ctx.redirect(...)`. */
153
128
  interface MiddlewareResult {
154
129
  readonly __apexRedirect: true;
@@ -178,4 +153,29 @@ type Middleware = (ctx: MiddlewareContext) => MiddlewareReturn | Promise<Middlew
178
153
  /** Author a middleware. Identity function — for types + discoverability. */
179
154
  declare function defineMiddleware(fn: Middleware): Middleware;
180
155
 
156
+ type StoreState = Record<string, unknown>;
157
+ interface ApexStore {
158
+ readonly __apexStore: true;
159
+ readonly name: string;
160
+ readonly factory: () => StoreState;
161
+ }
162
+ /**
163
+ * Define a global, SSR-safe store shared across every page, component, and island.
164
+ *
165
+ * ```ts
166
+ * // stores/cart.ts
167
+ * import { defineStore } from '@apex-stack/core'
168
+ * export default defineStore('cart', () => ({
169
+ * items: [] as string[],
170
+ * get count() { return this.items.length },
171
+ * add(x: string) { this.items.push(x) },
172
+ * }))
173
+ * ```
174
+ *
175
+ * Access it anywhere as `$store.cart` — `$store.cart.count` renders on the server
176
+ * and stays reactive after hydration.
177
+ */
178
+ declare function defineStore(name: string, factory: () => StoreState): ApexStore;
179
+ declare function isApexStore(x: unknown): x is ApexStore;
180
+
181
181
  export { type ApexConfig, type ApexResource, type ApexRoute, type ApexRouteConfig, type ApexRouteHandlerContext, type ApexStore, type HttpMethod, type InferInput, type InferOutput, type Middleware, type MiddlewareContext, type MiddlewareResult, type ResourceRoute, type RuntimeConfig, type StoreState, type TypedApexRoute, defineApexRoute, defineConfig, defineMiddleware, defineStore, env, isApexResource, isApexStore, useRuntimeConfig };
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  env,
9
9
  isApexStore,
10
10
  useRuntimeConfig
11
- } from "./chunk-JLIAISWM.js";
11
+ } from "./chunk-PMLGY6Z3.js";
12
12
 
13
13
  // src/api/defineRoute.ts
14
14
  function defineApexRoute(config) {
@@ -16,11 +16,11 @@ import {
16
16
  renderPage,
17
17
  resolveApexConfig,
18
18
  scanPages
19
- } from "./chunk-XDKJO6ZC.js";
20
- import "./chunk-JLIAISWM.js";
19
+ } from "./chunk-DVNFDYEO.js";
20
+ import "./chunk-PMLGY6Z3.js";
21
21
 
22
22
  // src/dev/server.ts
23
- import { existsSync as existsSync2, readdirSync } from "fs";
23
+ import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "fs";
24
24
  import { createServer as createHttpServer } from "http";
25
25
  import { createRequire } from "module";
26
26
  import { join } from "path";
@@ -175,17 +175,31 @@ async function startDevServer(options) {
175
175
  if (alpine) alias.alpinejs = alpine;
176
176
  if (kit) alias["@apex-stack/kit"] = kit;
177
177
  const plugins = [apex({ clientRuntime: "@apex-stack/core/client" })];
178
+ let hasTailwind = false;
178
179
  try {
179
180
  const reqProj = createRequire(join(options.root, "package.json"));
180
181
  const twMod = await import(pathToFileURL(reqProj.resolve("@tailwindcss/vite")).href);
181
182
  const tw = twMod.default ?? twMod;
182
183
  plugins.unshift(tw());
184
+ hasTailwind = true;
183
185
  } catch {
184
186
  }
185
187
  const appCssRel = ["app.css", "styles/app.css", "src/app.css"].find(
186
188
  (p) => existsSync2(join(options.root, p))
187
189
  );
188
- const appCss = appCssRel ? `/${appCssRel}` : void 0;
190
+ let appCss = appCssRel ? `/${appCssRel}` : void 0;
191
+ if (appCssRel && !hasTailwind) {
192
+ const css = readFileSync2(join(options.root, appCssRel), "utf8");
193
+ if (/@import\s+['"]tailwindcss['"]|@tailwind\b/.test(css)) {
194
+ console.warn(
195
+ `
196
+ \u26A0 ${appCssRel} imports Tailwind, but it isn't installed \u2014 skipping it (styles won't apply).
197
+ Fix: npm i -D tailwindcss @tailwindcss/vite
198
+ `
199
+ );
200
+ appCss = void 0;
201
+ }
202
+ }
189
203
  const vite = await createViteServer({
190
204
  root: options.root,
191
205
  appType: "custom",
@@ -11,8 +11,8 @@ import {
11
11
  matchRoute,
12
12
  renderIslandsPage,
13
13
  renderPage
14
- } from "./chunk-XDKJO6ZC.js";
15
- import "./chunk-JLIAISWM.js";
14
+ } from "./chunk-DVNFDYEO.js";
15
+ import "./chunk-PMLGY6Z3.js";
16
16
 
17
17
  // src/commands/start.ts
18
18
  import { existsSync as existsSync2 } from "fs";
@@ -9,7 +9,7 @@ import {
9
9
 
10
10
  // src/commands/upgrade.ts
11
11
  import { spawnSync } from "child_process";
12
- import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "fs";
12
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "fs";
13
13
  import { basename, dirname, join, relative, resolve } from "path";
14
14
  import { fileURLToPath } from "url";
15
15
  import { defineCommand } from "citty";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apex-stack/core",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "The full-stack meta-framework for Alpine.js — CLI and runtime",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -47,8 +47,8 @@
47
47
  "h3": "^1.13.0",
48
48
  "vite": "^6.0.7",
49
49
  "zod": "^4.4.3",
50
- "@apex-stack/kit": "0.2.0",
51
- "@apex-stack/vite": "0.1.6",
50
+ "@apex-stack/kit": "0.3.0",
51
+ "@apex-stack/vite": "0.1.7",
52
52
  "@apex-stack/theme": "0.3.0"
53
53
  },
54
54
  "peerDependencies": {
@@ -2,15 +2,20 @@
2
2
 
3
3
  An [Apex JS](https://apexjs.site) app — HTML-first, server-rendered, AI-native.
4
4
 
5
+ This starter is a small, themed demo: a landing page, a blog (list + dynamic
6
+ `[slug]` detail) served from a sample-data service, an About page with SEO, a
7
+ dark-mode toggle, themeable components, and an API route that's also an MCP tool.
8
+
5
9
  ## Commands
6
10
 
7
11
  ```bash
8
- npm run dev # dev server → http://localhost:3000
9
- npm run dev:islands # static-first islands mode (ship ~zero JS)
10
- npm run build # production build
11
- npm start # run the production server build
12
- npm test # run tests (Vitest)
13
- npm run typecheck # strict type-check
12
+ npm run dev # dev server → http://localhost:3000
13
+ npm run dev:islands # static-first islands mode (ship ~zero JS)
14
+ npm run build # production build (server target: SSR + dynamic routes + API/MCP)
15
+ npm start # run the production server build
16
+ npm run build:static # static build — prerenders static pages (dynamic routes need the server target)
17
+ npm test # run tests (Vitest)
18
+ npm run typecheck # strict type-check
14
19
  ```
15
20
 
16
21
  > `apex` is a project command — run it via `npm run dev`, or install it globally
@@ -21,7 +26,7 @@ npm run typecheck # strict type-check
21
26
  ```
22
27
  pages/ File-based routes (.alpine) — server-rendered, then hydrated.
23
28
  layouts/ Shared page shells; default.alpine wraps every page (<slot/>).
24
- components/ Reusable <PascalCase/> components with scoped styles.
29
+ components/ Reusable <PascalCase/> components, themed with Apex tokens (Button, Card, Badge, Counter).
25
30
  server/api/ Typed routes (defineApexRoute) — each is a REST endpoint AND an MCP tool.
26
31
  services/ Business logic as plain OO classes. Keep routes thin; delegate here.
27
32
  shared/ Types/interfaces shared by the backend and the frontend.
@@ -38,7 +43,7 @@ public/ Static assets served as-is.
38
43
  class in `services/`. Business logic stays testable in isolation and reusable everywhere.
39
44
  - **Types live in `shared/`.** One source of truth; strict TypeScript enforces them across
40
45
  backend and frontend — no drift.
41
- - **Tests by default.** `npm test` runs Vitest (see `tests/greeting.test.ts`).
46
+ - **Tests by default.** `npm test` runs Vitest (see `tests/posts.test.ts`).
42
47
 
43
48
  ## Generators
44
49
 
@@ -53,13 +58,16 @@ apex make middleware auth # → middleware/auth.ts (runs on every request)
53
58
  apex make test billing # → tests/billing.test.ts
54
59
  ```
55
60
 
56
- ## Styling
61
+ ## Styling & theming
57
62
 
58
- - **Scoped by default.** A `<style scoped>` block in an `.alpine` file is scoped to that component.
59
- - **Global / shared styles.** Create `app.css` (also `styles/app.css` or `src/app.css`) in the
60
- project root Apex auto-loads and processes it (Vite HMR in dev, bundled in build).
61
- - **Tailwind.** `npm i tailwindcss @tailwindcss/vite`, then `@import "tailwindcss";` at the top of
62
- `app.css`. Apex auto-detects `@tailwindcss/vite` no extra config.
63
+ - **Tailwind + theme, preinstalled.** `app.css` already imports Tailwind and defines the Apex
64
+ theme tokens (`--color-primary`, `--radius-radius`, fonts, and a `dark` variant). Components use
65
+ token classes like `bg-primary` / `text-on-surface` / `rounded-radius`, so they all restyle at once.
66
+ - **Restyle everything:** `apex theme --primary "#4f46e5" --radius 0.5rem` rewrites the managed
67
+ `/* apex-theme */` block in `app.css`. Or design it visually at https://apexjs.site/theme.html.
68
+ - **Add components:** `apex add <name>` copies a themed component into `components/`. Browse them at
69
+ https://apexjs.site/ui.html.
70
+ - **Scoped styles** still work too: a `<style scoped>` block in an `.alpine` file is scoped to it.
63
71
 
64
72
  ## Config & environment
65
73
 
@@ -0,0 +1,6 @@
1
+ <!-- Apex component adapted from Penguin UI (MIT). -->
2
+ <template x-data>
3
+ <span class="inline-flex items-center gap-1 rounded-radius border border-outline bg-surface-alt px-2 py-0.5 text-xs font-medium text-on-surface dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark">
4
+ <slot>Badge</slot>
5
+ </span>
6
+ </template>
@@ -0,0 +1,10 @@
1
+ <!-- Apex component adapted from Penguin UI (MIT). Styles via theme tokens
2
+ (bg-primary / rounded-radius / …) — needs Tailwind + @apex-stack/theme. -->
3
+ <template x-data>
4
+ <button
5
+ type="button"
6
+ class="inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-radius bg-primary border border-primary px-4 py-2 text-sm font-medium tracking-wide text-on-primary transition hover:opacity-75 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary active:opacity-100 active:outline-offset-0 disabled:opacity-75 disabled:cursor-not-allowed dark:bg-primary-dark dark:border-primary-dark dark:text-on-primary-dark dark:focus-visible:outline-primary-dark"
7
+ >
8
+ <slot>Button</slot>
9
+ </button>
10
+ </template>
@@ -0,0 +1,6 @@
1
+ <!-- Apex component adapted from Penguin UI (MIT). -->
2
+ <template>
3
+ <div class="flex flex-col gap-4 rounded-radius border border-outline bg-surface-alt p-6 text-on-surface dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark">
4
+ <slot></slot>
5
+ </div>
6
+ </template>
@@ -1,15 +1,10 @@
1
+ <!-- A small interactive component, themed with Apex tokens. `start`/`label` are
2
+ props; the count lives in client state and updates on click (hydrated). -->
1
3
  <template x-data="{ count: Number(start) }">
2
- <button class="counter" @click="count++" x-text="label + ': ' + count"></button>
4
+ <button
5
+ type="button"
6
+ class="inline-flex items-center gap-2 rounded-radius border border-outline bg-surface-alt px-4 py-2 text-sm font-medium text-on-surface transition hover:opacity-75 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary dark:border-outline-dark dark:bg-surface-dark-alt dark:text-on-surface-dark dark:focus-visible:outline-primary-dark"
7
+ @click="count++"
8
+ x-text="label + ': ' + count"
9
+ ></button>
3
10
  </template>
4
-
5
- <style scoped>
6
- .counter {
7
- padding: 0.45rem 0.9rem;
8
- border: 1px solid #6366f1;
9
- border-radius: 0.5rem;
10
- background: #eef2ff;
11
- color: #3730a3;
12
- cursor: pointer;
13
- font: inherit;
14
- }
15
- </style>
@@ -1,16 +1,39 @@
1
+ <!-- The default layout wraps every page. It's themed entirely with Apex tokens
2
+ (bg-surface / text-on-surface / dark:*), so restyling the whole app is one
3
+ `apex theme` command away. <slot></slot> is where the page renders. -->
1
4
  <template>
2
- <header class="site-header">
3
- <a class="brand" href="/">{{name}}</a>
4
- </header>
5
- <main>
6
- <slot></slot>
7
- </main>
8
- <footer class="site-footer">Built with Apex JS</footer>
9
- </template>
5
+ <div class="flex min-h-svh flex-col bg-surface text-on-surface dark:bg-surface-dark dark:text-on-surface-dark">
6
+ <header class="border-b border-outline dark:border-outline-dark">
7
+ <nav
8
+ class="mx-auto flex max-w-5xl items-center gap-2 px-6 py-4"
9
+ x-data="{ dark: false }"
10
+ x-init="dark = localStorage.getItem('theme') === 'dark'; document.documentElement.classList.toggle('dark', dark)"
11
+ >
12
+ <a href="/" class="mr-auto font-title text-lg font-bold text-on-surface-strong dark:text-on-surface-dark-strong">
13
+ {{name}}
14
+ </a>
15
+ <a href="/" class="rounded-radius px-3 py-1.5 text-sm font-medium hover:bg-surface-alt dark:hover:bg-surface-dark-alt">Home</a>
16
+ <a href="/blog" class="rounded-radius px-3 py-1.5 text-sm font-medium hover:bg-surface-alt dark:hover:bg-surface-dark-alt">Blog</a>
17
+ <a href="/about" class="rounded-radius px-3 py-1.5 text-sm font-medium hover:bg-surface-alt dark:hover:bg-surface-dark-alt">About</a>
18
+ <button
19
+ type="button"
20
+ aria-label="Toggle dark mode"
21
+ class="ml-1 rounded-radius border border-outline p-2 text-on-surface hover:bg-surface-alt dark:border-outline-dark dark:text-on-surface-dark dark:hover:bg-surface-dark-alt"
22
+ @click="dark = !dark; document.documentElement.classList.toggle('dark', dark); localStorage.setItem('theme', dark ? 'dark' : 'light')"
23
+ >
24
+ <svg x-show="!dark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="size-4" aria-hidden="true"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
25
+ <svg x-cloak x-show="dark" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-4" aria-hidden="true"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
26
+ </button>
27
+ </nav>
28
+ </header>
29
+
30
+ <main class="mx-auto w-full max-w-5xl flex-1 px-6 py-10">
31
+ <slot></slot>
32
+ </main>
10
33
 
11
- <style scoped>
12
- .site-header { padding: 1rem 1.5rem; border-bottom: 1px solid #e2e8f0; }
13
- .brand { font-weight: 700; color: #2563eb; text-decoration: none; }
14
- main { max-width: 44rem; margin: 0 auto; padding: 2.5rem 1.5rem; font-family: system-ui, -apple-system, "Segoe UI", sans-serif; line-height: 1.6; color: #1e293b; }
15
- .site-footer { padding: 1.5rem; text-align: center; color: #64748b; font-size: 0.9rem; border-top: 1px solid #e2e8f0; }
16
- </style>
34
+ <footer class="border-t border-outline px-6 py-6 text-center text-sm text-on-surface/70 dark:border-outline-dark dark:text-on-surface-dark/70">
35
+ Built with
36
+ <a href="https://apexjs.site" class="font-medium text-primary dark:text-primary-dark">Apex JS</a>
37
+ </footer>
38
+ </div>
39
+ </template>
@@ -5,7 +5,8 @@
5
5
  "scripts": {
6
6
  "dev": "apex dev",
7
7
  "dev:islands": "apex dev --islands",
8
- "build": "apex build",
8
+ "build": "apex build --server",
9
+ "build:static": "apex build",
9
10
  "start": "apex start",
10
11
  "test": "vitest run",
11
12
  "test:watch": "vitest",
@@ -0,0 +1,29 @@
1
+ <script server lang="ts">
2
+ export function loader() {
3
+ return {}
4
+ }
5
+
6
+ // SEO for this page — server-rendered <title> + meta description.
7
+ export function head() {
8
+ return {
9
+ title: 'About · {{name}}',
10
+ meta: [{ name: 'description', content: 'What this Apex JS starter demonstrates.' }],
11
+ }
12
+ }
13
+ </script>
14
+
15
+ <template x-data>
16
+ <h1 class="font-title text-3xl font-extrabold text-on-surface-strong dark:text-on-surface-dark-strong">About this starter</h1>
17
+ <p class="mt-3 max-w-2xl">This app was scaffolded with <code>create-apexjs</code>. It's a small tour of what Apex gives you out of the box:</p>
18
+
19
+ <ul class="mt-6 grid gap-3 sm:grid-cols-2">
20
+ <li><Card><b class="text-on-surface-strong dark:text-on-surface-dark-strong">File routing</b><span class="text-sm">Pages, dynamic <code>[slug]</code>, and a shared layout — see <code>pages/</code>.</span></Card></li>
21
+ <li><Card><b class="text-on-surface-strong dark:text-on-surface-dark-strong">SSR + hydration</b><span class="text-sm">Loaders render real HTML; Alpine hydrates it with no flash.</span></Card></li>
22
+ <li><Card><b class="text-on-surface-strong dark:text-on-surface-dark-strong">Themed components</b><span class="text-sm">Add more with <code>apex add &lt;name&gt;</code>; restyle with <code>apex theme</code>.</span></Card></li>
23
+ <li><Card><b class="text-on-surface-strong dark:text-on-surface-dark-strong">AI-native API</b><span class="text-sm">Routes double as MCP tools at <code>/mcp</code>.</span></Card></li>
24
+ </ul>
25
+
26
+ <p class="mt-8 text-sm text-on-surface/80 dark:text-on-surface-dark/80">
27
+ Edit any file under <code>pages/</code>, <code>components/</code>, or <code>app.css</code> and save — the dev server updates instantly.
28
+ </p>
29
+ </template>
@@ -0,0 +1,38 @@
1
+ <script server lang="ts">
2
+ import { PostService } from '../../services/PostService'
3
+
4
+ const posts = new PostService()
5
+
6
+ // Dynamic route: pages/blog/[slug].alpine → /blog/:slug. The matched param
7
+ // arrives in loader({ params }).
8
+ export function loader({ params }: { params: Record<string, string> }) {
9
+ const post = posts.bySlug(params.slug)
10
+ return { post: post ?? null }
11
+ }
12
+
13
+ export function head({ data }: { data: { post: { title: string } | null } }) {
14
+ return { title: `${data.post ? data.post.title : 'Not found'} · {{name}}` }
15
+ }
16
+ </script>
17
+
18
+ <template x-data>
19
+ <a href="/blog" class="text-sm text-primary hover:opacity-75 dark:text-primary-dark">← Back to blog</a>
20
+
21
+ <template x-if="post">
22
+ <article class="mt-4">
23
+ <div class="flex items-center gap-2 text-sm text-on-surface/70 dark:text-on-surface-dark/70">
24
+ <Badge><span x-text="post.author"></span></Badge>
25
+ <span x-text="post.date"></span>
26
+ </div>
27
+ <h1 class="mt-3 font-title text-3xl font-extrabold text-on-surface-strong dark:text-on-surface-dark-strong" x-text="post.title"></h1>
28
+ <p class="mt-4 text-lg leading-relaxed" x-text="post.body"></p>
29
+ </article>
30
+ </template>
31
+
32
+ <template x-if="!post">
33
+ <div class="mt-8">
34
+ <h1 class="font-title text-2xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">Post not found</h1>
35
+ <p class="mt-2 text-on-surface/80 dark:text-on-surface-dark/80">That post doesn't exist. Try the <a href="/blog" class="text-primary dark:text-primary-dark">blog index</a>.</p>
36
+ </div>
37
+ </template>
38
+ </template>
@@ -0,0 +1,37 @@
1
+ <script server lang="ts">
2
+ import { PostService } from '../../services/PostService'
3
+
4
+ const posts = new PostService()
5
+
6
+ export function loader() {
7
+ return { posts: posts.all() }
8
+ }
9
+
10
+ // head() drives <title> and meta tags — SEO, rendered on the server.
11
+ export function head() {
12
+ return { title: 'Blog · {{name}}' }
13
+ }
14
+ </script>
15
+
16
+ <template x-data>
17
+ <h1 class="font-title text-3xl font-extrabold text-on-surface-strong dark:text-on-surface-dark-strong">Blog</h1>
18
+ <p class="mt-2 text-on-surface/80 dark:text-on-surface-dark/80">Sample posts served from a service — no database required.</p>
19
+
20
+ <!-- Apex components work inside x-for/x-if — <Card> here is re-created per item
21
+ on the client, fully styled. That's the "Alpine Extreme" bit. -->
22
+ <div class="mt-8 grid gap-4 sm:grid-cols-2">
23
+ <template x-for="p in posts" :key="p.slug">
24
+ <a :href="'/blog/' + p.slug" class="block transition hover:opacity-75">
25
+ <Card>
26
+ <div class="flex items-center gap-2 text-xs text-on-surface/70 dark:text-on-surface-dark/70">
27
+ <span x-text="p.author"></span>
28
+ <span>·</span>
29
+ <span x-text="p.date"></span>
30
+ </div>
31
+ <h2 class="font-title text-lg font-bold text-on-surface-strong dark:text-on-surface-dark-strong" x-text="p.title"></h2>
32
+ <p class="text-sm" x-text="p.excerpt"></p>
33
+ </Card>
34
+ </a>
35
+ </template>
36
+ </div>
37
+ </template>
@@ -1,38 +1,67 @@
1
1
  <script server lang="ts">
2
+ import { PostService } from '../services/PostService'
3
+
4
+ const posts = new PostService()
5
+
6
+ // Runs on the server. Its return value becomes the page's x-data — so the
7
+ // HTML is rendered from real data before a single byte of JS runs.
2
8
  export function loader() {
3
9
  return {
4
10
  title: 'Welcome to {{name}}',
5
- tagline: 'The meta-framework for Alpine.js — server-rendered, then hydrated.',
11
+ tagline: 'A full-stack, server-rendered app built on Alpine.js — themed, typed, and AI-native.',
12
+ recent: posts.recent(3),
6
13
  }
7
14
  }
8
15
  </script>
9
16
 
10
- <template x-data="{ open: false }">
11
- <h1 x-text="title"></h1>
12
- <p class="tagline" x-text="tagline"></p>
13
-
14
- <section class="card">
15
- <p>
16
- This was rendered on the server from <code>pages/index.alpine</code>, wrapped in
17
- <code>layouts/default.alpine</code>, then hydrated by Alpine. Edit and save to see it live.
18
- </p>
19
- <button type="button" @click="open = !open" x-text="open ? 'Hide' : 'Show me how'"></button>
20
- <div x-show="open" x-transition class="details">
21
- <p>The <code>loader()</code> runs on the server and hands its data to Alpine's <code>x-data</code>.</p>
22
- <p>Open <code>server/api/hello.ts</code>: a thin route → <code>GreetingService</code>, exposed as REST <em>and</em> an MCP tool.</p>
17
+ <template x-data="{ show: false }">
18
+ <!-- Hero -->
19
+ <section class="py-8 text-center">
20
+ <h1 class="font-title text-4xl font-extrabold tracking-tight text-on-surface-strong sm:text-5xl dark:text-on-surface-dark-strong" x-text="title"></h1>
21
+ <p class="mx-auto mt-4 max-w-2xl text-lg" x-text="tagline"></p>
22
+ <div class="mt-6 flex flex-wrap justify-center gap-3">
23
+ <a href="/blog"><Button>Read the blog</Button></a>
24
+ <a href="https://apexjs.site" class="inline-flex items-center justify-center rounded-radius border border-outline px-4 py-2 text-sm font-medium text-on-surface transition hover:opacity-75 dark:border-outline-dark dark:text-on-surface-dark">Docs</a>
23
25
  </div>
24
26
  </section>
25
27
 
26
- <p class="hint">A reusable component that hydrates in the browser:</p>
27
- <Counter start="0" label="Clicks" />
28
- </template>
28
+ <!-- Feature cards -->
29
+ <section class="mt-6 grid gap-4 sm:grid-cols-3">
30
+ <Card>
31
+ <h3 class="font-title text-lg font-bold text-on-surface-strong dark:text-on-surface-dark-strong">Server-rendered</h3>
32
+ <p class="text-sm">Every page is real HTML from a <code>loader()</code>, then hydrated by Alpine — fast and indexable.</p>
33
+ </Card>
34
+ <Card>
35
+ <h3 class="font-title text-lg font-bold text-on-surface-strong dark:text-on-surface-dark-strong">Themed</h3>
36
+ <p class="text-sm">All components use theme tokens. Restyle the whole app with one <code>apex theme</code> command.</p>
37
+ </Card>
38
+ <Card>
39
+ <h3 class="font-title text-lg font-bold text-on-surface-strong dark:text-on-surface-dark-strong">AI-native</h3>
40
+ <p class="text-sm">Every typed API route is also an MCP tool your AI can call. See <code>server/api/posts.ts</code>.</p>
41
+ </Card>
42
+ </section>
29
43
 
30
- <style scoped>
31
- h1 { color: #2563eb; font-size: 2.25rem; margin-bottom: 0.25rem; }
32
- .tagline { font-size: 1.1rem; color: #475569; margin-top: 0; }
33
- .card { margin-top: 1.5rem; padding: 1.25rem; border: 1px solid #e2e8f0; border-radius: 0.75rem; background: #f8fafc; }
34
- .card button { margin-top: 0.5rem; padding: 0.55rem 1rem; font-weight: 600; color: #fff; background: #2563eb; border: none; border-radius: 0.5rem; cursor: pointer; }
35
- .details { margin-top: 1rem; }
36
- code { padding: 0.1rem 0.35rem; background: #e2e8f0; border-radius: 0.35rem; font-size: 0.9em; }
37
- .hint { margin-top: 1.5rem; color: #64748b; font-size: 0.95rem; }
38
- </style>
44
+ <!-- Recent posts, from the loader -->
45
+ <section class="mt-10">
46
+ <h2 class="font-title text-xl font-bold text-on-surface-strong dark:text-on-surface-dark-strong">Recent posts</h2>
47
+ <ul class="mt-3 divide-y divide-outline dark:divide-outline-dark">
48
+ <template x-for="p in recent" :key="p.slug">
49
+ <li class="py-3">
50
+ <a :href="'/blog/' + p.slug" class="font-medium text-primary hover:opacity-75 dark:text-primary-dark" x-text="p.title"></a>
51
+ <p class="text-sm text-on-surface/80 dark:text-on-surface-dark/80" x-text="p.excerpt"></p>
52
+ </li>
53
+ </template>
54
+ </ul>
55
+ </section>
56
+
57
+ <!-- A hydrated, interactive component -->
58
+ <section class="mt-10 flex flex-col items-start gap-3">
59
+ <p class="text-sm text-on-surface/80 dark:text-on-surface-dark/80">Hydrated in the browser — click it:</p>
60
+ <Counter start="0" label="Clicks" />
61
+ <button type="button" class="text-sm text-primary hover:opacity-75 dark:text-primary-dark" @click="show = !show" x-text="show ? 'Hide details' : 'How does this work?'"></button>
62
+ <p x-show="show" x-transition class="max-w-2xl text-sm text-on-surface/80 dark:text-on-surface-dark/80">
63
+ This page is <code>pages/index.alpine</code>, wrapped in <code>layouts/default.alpine</code>.
64
+ The server ran <code>loader()</code>, rendered the HTML, and Alpine took over in the browser — the counter and this toggle are proof.
65
+ </p>
66
+ </section>
67
+ </template>
@@ -0,0 +1,20 @@
1
+ import { defineApexRoute } from '@apex-stack/core'
2
+ import { z } from 'zod'
3
+ import { PostService } from '../../services/PostService'
4
+
5
+ const posts = new PostService()
6
+
7
+ /**
8
+ * A route is a thin adapter: validate input, delegate to a service, return the
9
+ * result. Because `mcp: true`, this is ALSO an MCP tool named "posts" at /mcp —
10
+ * one definition, REST + AI-callable. Try:
11
+ * curl "http://localhost:3000/api/posts"
12
+ * curl "http://localhost:3000/api/posts?slug=hello-apex"
13
+ */
14
+ export default defineApexRoute({
15
+ method: 'GET',
16
+ description: 'List blog posts, or fetch one by slug',
17
+ input: { slug: z.string().optional() },
18
+ mcp: true,
19
+ handler: ({ input }) => (input.slug ? (posts.bySlug(input.slug) ?? null) : posts.all()),
20
+ })
@@ -0,0 +1,51 @@
1
+ import type { Post } from '../shared/types'
2
+
3
+ /**
4
+ * A service holds business logic as a plain class — testable in isolation and
5
+ * reusable from routes, page loaders, and jobs. Here it stands in for a database
6
+ * with in-memory sample data; swap the array for a real query (see the `db/`
7
+ * folder and `defineResource`) and nothing else in the app changes.
8
+ */
9
+ const POSTS: Post[] = [
10
+ {
11
+ slug: 'hello-apex',
12
+ title: 'Hello, Apex',
13
+ excerpt: 'Why Alpine deserved a full-stack meta-framework.',
14
+ author: 'Ada Lovelace',
15
+ date: '2026-02-01',
16
+ body: 'Apex renders your pages on the server as real, indexable HTML, then Alpine hydrates them in the browser — no client-side framework tax, no flash. This whole app is server-rendered from .alpine files and made interactive by Alpine.',
17
+ },
18
+ {
19
+ slug: 'ssr-then-hydrate',
20
+ title: 'SSR first, hydrate second',
21
+ excerpt: 'Real HTML on the first byte. Interactivity right after.',
22
+ author: 'Grace Hopper',
23
+ date: '2026-02-08',
24
+ body: 'Each page has a loader() that runs on the server. Its return value is handed to Alpine as x-data, so the markup you see is the markup search engines and users get instantly — then the same state powers client interactivity.',
25
+ },
26
+ {
27
+ slug: 'routes-are-tools',
28
+ title: 'Every route is also an MCP tool',
29
+ excerpt: 'Ship an API your AI can call — from one definition.',
30
+ author: 'Alan Turing',
31
+ date: '2026-02-15',
32
+ body: 'Open server/api/posts.ts: one defineApexRoute is a validated REST endpoint AND an MCP tool at /mcp. Point an AI client at it and it can list your posts with no extra glue.',
33
+ },
34
+ ]
35
+
36
+ export class PostService {
37
+ /** All posts, newest first. */
38
+ all(): Post[] {
39
+ return [...POSTS].sort((a, b) => b.date.localeCompare(a.date))
40
+ }
41
+
42
+ /** The N most recent posts. */
43
+ recent(n: number): Post[] {
44
+ return this.all().slice(0, n)
45
+ }
46
+
47
+ /** A single post by its slug, or undefined. */
48
+ bySlug(slug: string): Post | undefined {
49
+ return POSTS.find((p) => p.slug === slug)
50
+ }
51
+ }
@@ -1,11 +1,16 @@
1
1
  /**
2
2
  * Shared types — the single source of truth for shapes used across the app,
3
- * on the BACKEND (routes, services) and the FRONTEND. Import from '../shared/types'.
3
+ * on the BACKEND (routes, services) and the FRONTEND (pages, components).
4
+ * Import from '../shared/types'.
4
5
  *
5
6
  * Defining types here (instead of inline) is what keeps a growing codebase clean:
6
7
  * one place to change a shape, and the compiler enforces it everywhere it's used.
7
8
  */
8
- export interface Greeting {
9
- message: string
10
- at: string
9
+ export interface Post {
10
+ slug: string
11
+ title: string
12
+ excerpt: string
13
+ author: string
14
+ date: string
15
+ body: string
11
16
  }
@@ -0,0 +1,22 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { PostService } from '../services/PostService'
3
+
4
+ // Services are plain classes → unit-test them in isolation, no server needed.
5
+ describe('PostService', () => {
6
+ const posts = new PostService()
7
+
8
+ it('lists posts newest first', () => {
9
+ const dates = posts.all().map((p) => p.date)
10
+ expect(dates.length).toBeGreaterThan(0)
11
+ expect(dates).toEqual([...dates].sort((a, b) => b.localeCompare(a)))
12
+ })
13
+
14
+ it('limits recent()', () => {
15
+ expect(posts.recent(2)).toHaveLength(2)
16
+ })
17
+
18
+ it('finds a post by slug', () => {
19
+ expect(posts.bySlug('hello-apex')?.title).toBe('Hello, Apex')
20
+ expect(posts.bySlug('nope')).toBeUndefined()
21
+ })
22
+ })
@@ -1,18 +0,0 @@
1
- import { defineApexRoute } from '@apex-stack/core'
2
- import { z } from 'zod'
3
- import { GreetingService } from '../../services/GreetingService'
4
-
5
- const greetings = new GreetingService()
6
-
7
- /**
8
- * A route is a thin adapter: validate input, delegate to a service, return the
9
- * result. Because `mcp: true`, this is ALSO an MCP tool named "hello" at /mcp —
10
- * one definition, REST + AI-callable.
11
- */
12
- export default defineApexRoute({
13
- method: 'GET',
14
- description: 'Greet someone by name',
15
- input: { name: z.string() },
16
- mcp: true,
17
- handler: ({ input }) => greetings.greet(input.name),
18
- })
@@ -1,12 +0,0 @@
1
- import type { Greeting } from '../shared/types'
2
-
3
- /**
4
- * A service holds business logic as a plain class — testable in isolation and
5
- * reusable from routes, page loaders, and jobs. Keep routes/loaders thin: they
6
- * validate input and delegate to a service. This is the clean-code backbone.
7
- */
8
- export class GreetingService {
9
- greet(name: string): Greeting {
10
- return { message: `Hello, ${name}!`, at: new Date().toISOString() }
11
- }
12
- }
@@ -1,12 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
- import { GreetingService } from '../services/GreetingService'
3
-
4
- // Services are plain classes, so they test in isolation — no server needed.
5
- // Generate more with: apex make test <name>
6
- describe('GreetingService', () => {
7
- it('greets by name', () => {
8
- const g = new GreetingService().greet('Apex')
9
- expect(g.message).toBe('Hello, Apex!')
10
- expect(typeof g.at).toBe('string')
11
- })
12
- })