create-apexjs 0.1.5 → 0.1.6

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Andre Corugda
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.js CHANGED
@@ -1,12 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { cpSync, existsSync, readdirSync, readFileSync, renameSync, writeFileSync } from "fs";
4
+ import { spawnSync } from "child_process";
5
+ import { cpSync, existsSync, readFileSync, readdirSync, renameSync, writeFileSync } from "fs";
5
6
  import { basename, join, resolve } from "path";
6
7
  import { fileURLToPath } from "url";
7
- import { spawnSync } from "child_process";
8
8
  import { defineCommand, runMain } from "citty";
9
9
  var TEMPLATE_DIR = fileURLToPath(new URL("../templates/default", import.meta.url));
10
+ function substituteName(dir, name) {
11
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
12
+ const p = join(dir, entry.name);
13
+ if (entry.isDirectory()) substituteName(p, name);
14
+ else {
15
+ const txt = readFileSync(p, "utf8");
16
+ if (txt.includes("{{name}}")) writeFileSync(p, txt.replaceAll("{{name}}", name));
17
+ }
18
+ }
19
+ }
10
20
  function detectPackageManager() {
11
21
  const ua = process.env.npm_config_user_agent || "";
12
22
  if (ua.startsWith("pnpm")) return "pnpm";
@@ -35,9 +45,22 @@ var main = defineCommand({
35
45
  description: "Scaffold a new Apex JS app"
36
46
  },
37
47
  args: {
38
- dir: { type: "positional", required: false, description: "Target directory", default: "apex-app" },
39
- install: { type: "boolean", default: true, description: "Install dependencies (use --no-install to skip)" },
40
- git: { type: "boolean", default: true, description: "Initialize a git repository (use --no-git to skip)" }
48
+ dir: {
49
+ type: "positional",
50
+ required: false,
51
+ description: "Target directory",
52
+ default: "apex-app"
53
+ },
54
+ install: {
55
+ type: "boolean",
56
+ default: true,
57
+ description: "Install dependencies (use --no-install to skip)"
58
+ },
59
+ git: {
60
+ type: "boolean",
61
+ default: true,
62
+ description: "Initialize a git repository (use --no-git to skip)"
63
+ }
41
64
  },
42
65
  run({ args }) {
43
66
  const target = resolve(process.cwd(), args.dir);
@@ -51,12 +74,7 @@ var main = defineCommand({
51
74
  cpSync(TEMPLATE_DIR, target, { recursive: true });
52
75
  const gitignore = join(target, "_gitignore");
53
76
  if (existsSync(gitignore)) renameSync(gitignore, join(target, ".gitignore"));
54
- for (const rel of ["package.json", "README.md"]) {
55
- const file = join(target, rel);
56
- if (existsSync(file)) {
57
- writeFileSync(file, readFileSync(file, "utf8").replaceAll("{{name}}", name));
58
- }
59
- }
77
+ substituteName(target, name);
60
78
  console.log(`
61
79
  ${c.cyan("Apex JS")} app created in ${args.dir}`);
62
80
  const pm = detectPackageManager();
@@ -71,26 +89,32 @@ var main = defineCommand({
71
89
  }
72
90
  let installed = false;
73
91
  if (args.install) {
74
- console.log(`
92
+ console.log(
93
+ `
75
94
  Installing dependencies with ${c.cyan(pm)}\u2026 ${c.dim("(first install can take a minute)")}
76
- `);
95
+ `
96
+ );
77
97
  const installArgs = pm === "npm" ? ["install", "--no-audit", "--no-fund"] : ["install"];
78
98
  installed = run(pm, installArgs, target);
79
99
  if (!installed) {
80
- console.log(`
100
+ console.log(
101
+ `
81
102
  ${c.yellow("\u26A0")} Dependency install failed \u2014 run it yourself after cd'ing in.
82
- `);
103
+ `
104
+ );
83
105
  }
84
106
  }
85
107
  const runPrefix = pm === "npm" ? "npm run" : pm;
86
108
  const steps = [`cd ${args.dir}`];
87
109
  if (!installed) steps.push(pm === "yarn" ? "yarn" : `${pm} install`);
88
- steps.push(`${runPrefix} dev ${c.dim("# start the dev server \u2192 http://localhost:3000")}`);
110
+ steps.push(
111
+ `${runPrefix} dev ${c.dim("# start the dev server \u2192 http://localhost:3000")}`
112
+ );
89
113
  console.log(`
90
114
  ${installed ? c.green("Ready.") : "Next steps:"}
91
115
  ${steps.map((s) => ` ${s}`).join("\n")}
92
116
 
93
- ${c.yellow("Run the CLI with:")} ${c.cyan(runPrefix + " dev")} ${c.dim("(or npx apex dev)")}
117
+ ${c.yellow("Run the CLI with:")} ${c.cyan(`${runPrefix} dev`)} ${c.dim("(or npx apex dev)")}
94
118
  A bare ${c.cyan("apex")} won't resolve \u2014 it's a local dependency, like ${c.cyan("next")} or ${c.cyan("vite")}.
95
119
  ${c.dim("Prefer a global command? ")}${c.cyan("npm i -g @apex-stack/core")}${c.dim(" \u2192 then `apex dev` works anywhere.")}
96
120
  ${c.dim("Islands mode:")} ${runPrefix} dev:islands
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-apexjs",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Scaffold a new Apex JS app",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -28,15 +28,15 @@
28
28
  "dist",
29
29
  "templates"
30
30
  ],
31
- "scripts": {
32
- "build": "tsup",
33
- "dev": "tsup --watch",
34
- "typecheck": "tsc --noEmit"
35
- },
36
31
  "dependencies": {
37
32
  "citty": "^0.1.6"
38
33
  },
39
34
  "engines": {
40
35
  "node": ">=20.19"
36
+ },
37
+ "scripts": {
38
+ "build": "tsup",
39
+ "dev": "tsup --watch",
40
+ "typecheck": "tsc --noEmit"
41
41
  }
42
- }
42
+ }
@@ -1,33 +1,55 @@
1
1
  # {{name}}
2
2
 
3
- An [Apex JS](https://github.com/andrecorugda/apexjs) app — a meta-framework for
4
- Alpine.js that renders on the server and hydrates in the browser.
3
+ An [Apex JS](https://apexjs.site) app — HTML-first, server-rendered, AI-native.
5
4
 
6
- ## Getting started
5
+ ## Commands
7
6
 
8
7
  ```bash
9
- npm install
10
- npm run dev
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
11
14
  ```
12
15
 
13
- Then open [http://localhost:3000](http://localhost:3000).
16
+ > `apex` is a project command — run it via `npm run dev`, or install it globally
17
+ > (`npm i -g @apex-stack/core`) to use `apex dev` directly.
14
18
 
15
- ## Islands mode
16
-
17
- Ship interactive JavaScript only where you need it:
19
+ ## Project structure
18
20
 
19
- ```bash
20
- apex dev --islands
21
+ ```
22
+ pages/ File-based routes (.alpine) — server-rendered, then hydrated.
23
+ layouts/ Shared page shells; default.alpine wraps every page (<slot/>).
24
+ components/ Reusable <PascalCase/> components with scoped styles.
25
+ server/api/ Typed routes (defineApexRoute) — each is a REST endpoint AND an MCP tool.
26
+ services/ Business logic as plain OO classes. Keep routes thin; delegate here.
27
+ shared/ Types/interfaces shared by the backend and the frontend.
28
+ stores/ Global, SSR-safe state — $store.x, reactive across pages/islands.
29
+ composables/ Reusable client logic (useX) for <script client> blocks.
30
+ tests/ Vitest tests. `npm test` runs them.
31
+ db/ Optional: a database + resources. See db/README.md.
32
+ public/ Static assets served as-is.
21
33
  ```
22
34
 
23
- ## Project structure
35
+ ## Conventions (clean code)
36
+
37
+ - **Thin routes → services.** A route/loader validates input and delegates to a service
38
+ class in `services/`. Business logic stays testable in isolation and reusable everywhere.
39
+ - **Types live in `shared/`.** One source of truth; strict TypeScript enforces them across
40
+ backend and frontend — no drift.
41
+ - **Tests by default.** `npm test` runs Vitest (see `tests/greeting.test.ts`).
24
42
 
25
- - `pages/*.alpine` — single-file components. The `<script server>` block runs on
26
- the server; its `loader()` return value becomes the Alpine `x-data` scope.
27
- - `server/api/*.ts` — API routes defined with `defineApexRoute`.
43
+ ## Generators
28
44
 
29
- ## AI-native API
45
+ ```bash
46
+ apex make page about
47
+ apex make component Card
48
+ apex make api todos
49
+ apex make service Billing # → services/BillingService.ts (OO class)
50
+ apex make store cart
51
+ apex make layout marketing
52
+ apex make test billing # → tests/billing.test.ts
53
+ ```
30
54
 
31
- Every route in `server/api/*.ts` is a REST endpoint **and** an MCP tool at the
32
- same time. Set `mcp: true` on a route (see `server/api/hello.ts`) and it is
33
- automatically exposed to AI agents at the `/mcp` endpoint — no extra wiring.
55
+ Full docs: https://apexjs.site
@@ -0,0 +1,15 @@
1
+ <template x-data="{ count: Number(start) }">
2
+ <button class="counter" @click="count++" x-text="label + ': ' + count"></button>
3
+ </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>
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Reusable client logic. Import it in a <script client> block and use it in x-data:
3
+ *
4
+ * <script client> import { useToggle } from '../composables/useToggle' </script>
5
+ * <template x-data="useToggle(true)"> <button @click="toggle()" x-text="on"></button> </template>
6
+ */
7
+ export function useToggle(initial = false) {
8
+ return {
9
+ on: initial,
10
+ toggle() {
11
+ this.on = !this.on
12
+ },
13
+ }
14
+ }
@@ -0,0 +1,18 @@
1
+ # db/
2
+
3
+ Data is opt-in. To add a database with resources that are REST **and** MCP by default:
4
+
5
+ ```bash
6
+ npm i @apex-stack/data @libsql/client # install only the driver you use
7
+ ```
8
+
9
+ Then add `db/schema.ts` (Drizzle tables), `db/index.ts` (`createDb` + `applyMigrations`),
10
+ and a resource in `server/api/*.ts`:
11
+
12
+ ```ts
13
+ import { defineResource } from '@apex-stack/data'
14
+ export default defineResource('posts', { db, table: schema.posts, insert: { title: z.string() } })
15
+ ```
16
+
17
+ That one line gives you `GET/POST/PATCH/DELETE /api/posts` plus the MCP tools
18
+ `posts_list/get/create/update/delete`. See https://apexjs.site/data.html
@@ -0,0 +1,16 @@
1
+ <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>
10
+
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>
@@ -4,11 +4,20 @@
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "apex dev",
7
- "dev:islands": "apex dev --islands"
7
+ "dev:islands": "apex dev --islands",
8
+ "build": "apex build",
9
+ "start": "apex start",
10
+ "test": "vitest run",
11
+ "test:watch": "vitest",
12
+ "typecheck": "tsc --noEmit"
8
13
  },
9
14
  "dependencies": {
10
- "alpinejs": "^3.14.8",
11
15
  "@apex-stack/core": "latest",
16
+ "alpinejs": "^3.14.8",
12
17
  "zod": "^4.0.0"
18
+ },
19
+ "devDependencies": {
20
+ "typescript": "^5.6.0",
21
+ "vitest": "^3.0.0"
13
22
  }
14
23
  }
@@ -1,107 +1,38 @@
1
1
  <script server lang="ts">
2
2
  export function loader() {
3
3
  return {
4
- title: 'Welcome to Apex JS',
4
+ title: 'Welcome to {{name}}',
5
5
  tagline: 'The meta-framework for Alpine.js — server-rendered, then hydrated.',
6
6
  }
7
7
  }
8
8
  </script>
9
9
 
10
10
  <template x-data="{ open: false }">
11
- <main>
12
- <h1 x-text="title"></h1>
13
- <p class="tagline" x-text="tagline"></p>
11
+ <h1 x-text="title"></h1>
12
+ <p class="tagline" x-text="tagline"></p>
14
13
 
15
- <section class="card">
16
- <p>
17
- This page was rendered on the server from
18
- <code>pages/index.alpine</code>, then hydrated by Alpine in the browser.
19
- Edit it and save to see your changes live.
20
- </p>
21
-
22
- <button type="button" @click="open = !open" x-text="open ? 'Hide the details' : 'Show me how'"></button>
23
-
24
- <div x-show="open" x-transition class="details">
25
- <p>
26
- The <code>loader()</code> in the server block runs on the server and
27
- hands its data straight to Alpine's <code>x-data</code> scope — no
28
- fetch, no boilerplate.
29
- </p>
30
- <p>
31
- Next, open <code>server/api/hello.ts</code>: it's a REST endpoint
32
- <em>and</em> an MCP tool at <code>/mcp</code> at the same time.
33
- </p>
34
- </div>
35
- </section>
36
-
37
- <p class="hint">
38
- Run <code>apex dev --islands</code> to ship interactive islands only where
39
- you need them.
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.
40
18
  </p>
41
- </main>
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>
23
+ </div>
24
+ </section>
25
+
26
+ <p class="hint">A reusable component that hydrates in the browser:</p>
27
+ <Counter start="0" label="Clicks" />
42
28
  </template>
43
29
 
44
30
  <style scoped>
45
- main {
46
- max-width: 40rem;
47
- margin: 4rem auto;
48
- padding: 0 1.5rem;
49
- font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
50
- line-height: 1.6;
51
- color: #1e293b;
52
- }
53
-
54
- h1 {
55
- color: #2563eb;
56
- font-size: 2.5rem;
57
- margin-bottom: 0.5rem;
58
- }
59
-
60
- .tagline {
61
- font-size: 1.15rem;
62
- color: #475569;
63
- margin-top: 0;
64
- }
65
-
66
- .card {
67
- margin-top: 2rem;
68
- padding: 1.5rem;
69
- border: 1px solid #e2e8f0;
70
- border-radius: 0.75rem;
71
- background: #f8fafc;
72
- }
73
-
74
- button {
75
- margin-top: 0.5rem;
76
- padding: 0.6rem 1.1rem;
77
- font-size: 1rem;
78
- font-weight: 600;
79
- color: #fff;
80
- background: #2563eb;
81
- border: none;
82
- border-radius: 0.5rem;
83
- cursor: pointer;
84
- transition: background 0.15s ease;
85
- }
86
-
87
- button:hover {
88
- background: #1d4ed8;
89
- }
90
-
91
- .details {
92
- margin-top: 1rem;
93
- }
94
-
95
- code {
96
- padding: 0.1rem 0.35rem;
97
- font-size: 0.9em;
98
- background: #e2e8f0;
99
- border-radius: 0.35rem;
100
- }
101
-
102
- .hint {
103
- margin-top: 2rem;
104
- font-size: 0.95rem;
105
- color: #64748b;
106
- }
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; }
107
38
  </style>
File without changes
@@ -1,10 +1,18 @@
1
1
  import { defineApexRoute } from '@apex-stack/core'
2
2
  import { z } from 'zod'
3
+ import { GreetingService } from '../../services/GreetingService'
3
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
+ */
4
12
  export default defineApexRoute({
5
13
  method: 'GET',
6
- description: 'Say hello to someone by name',
14
+ description: 'Greet someone by name',
7
15
  input: { name: z.string() },
8
16
  mcp: true,
9
- handler: ({ input }) => ({ message: `Hello, ${input.name}!` }),
17
+ handler: ({ input }) => greetings.greet(input.name),
10
18
  })
@@ -0,0 +1,12 @@
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
+ }
@@ -0,0 +1,11 @@
1
+ /**
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'.
4
+ *
5
+ * Defining types here (instead of inline) is what keeps a growing codebase clean:
6
+ * one place to change a shape, and the compiler enforces it everywhere it's used.
7
+ */
8
+ export interface Greeting {
9
+ message: string
10
+ at: string
11
+ }
@@ -0,0 +1,9 @@
1
+ import { defineStore } from '@apex-stack/core'
2
+
3
+ // Global, SSR-safe state shared across pages, components, and islands: $store.ui
4
+ export default defineStore('ui', () => ({
5
+ menuOpen: false,
6
+ toggleMenu() {
7
+ this.menuOpen = !this.menuOpen
8
+ },
9
+ }))
@@ -0,0 +1,12 @@
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
+ })
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "noUncheckedIndexedAccess": true,
8
+ "noImplicitOverride": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "verbatimModuleSyntax": true,
12
+ "noEmit": true
13
+ },
14
+ "include": ["server", "services", "shared", "stores", "composables", "tests", "db"]
15
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from 'vitest/config'
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ['tests/**/*.test.ts'],
6
+ },
7
+ })