@owlmeans/create-app 0.1.18-rc.13 → 0.1.18-rc.14

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/build/bin.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/create-app",
3
- "version": "0.1.18-rc.13",
3
+ "version": "0.1.18-rc.14",
4
4
  "license": "MIT",
5
5
  "description": "Scaffold a minimal fullstack OwlMeans Common app — common + api + web workspaces, shadcn UI navigation/layout, no auth, and a session-scoped in-memory resource. Deploys agent skills via @owlmeans/agent-skills by default.",
6
6
  "type": "module",
@@ -29,7 +29,7 @@
29
29
  "template"
30
30
  ],
31
31
  "dependencies": {
32
- "@owlmeans/agent-skills": "^0.1.18-rc.10"
32
+ "@owlmeans/agent-skills": "^0.1.18-rc.11"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@owlmeans/dep-config": "workspace:*",
@@ -14,8 +14,11 @@ A minimal OwlMeans app is a **bun-workspace monorepo with three packages**:
14
14
  ```
15
15
  sources/
16
16
  ├── common/ # shared entrypoints (routes), AJV schemas, types, config — the single source of truth
17
+ │ # entrypoints.ts
17
18
  ├── api/ # @owlmeans/server-app backend; handlers attached to the shared entrypoints
19
+ │ # context.ts, entrypoints.ts, app/<area>/*, index.ts
18
20
  └── web/ # @owlmeans/web-panel + shadcn UI; screens attached to the same entrypoints
21
+ # context.ts, entrypoints.ts, nav.ts, layout/, screens/, index.tsx
19
22
  ```
20
23
 
21
24
  The full, runnable walkthrough (scaffolded **and** manual) lives in
@@ -27,8 +30,8 @@ The full, runnable walkthrough (scaffolded **and** manual) lives in
27
30
  Declare each route once in `common` as an **entrypoint**, then `elevate()` it on each side:
28
31
 
29
32
  ```ts
30
- // common/modules.ts — declaration + validation, no implementation
31
- export const sessionModules = [
33
+ // common/entrypoints.ts — declaration + validation, no implementation
34
+ export const sessionEntrypoints = [
32
35
  entrypoint(route(session.base, '/session')),
33
36
  entrypoint(route(session.list, '/:sid/items', { parent: session.base, method: RouteMethod.GET }),
34
37
  filter(params<SessionParams>(SessionParamsSchema))),
@@ -38,17 +41,25 @@ export const sessionModules = [
38
41
  ```
39
42
 
40
43
  ```ts
41
- // api/modules.ts — attach handlers
42
- elevate(sessionModules, session.list, handlers.list)
43
- export const appModules = [...modules, ...sessionModules] // `modules` = framework defaults
44
+ // api/entrypoints.ts — attach handlers
45
+ elevate(sessionEntrypoints, session.list, handlers.list)
46
+ export const appEntrypoints = [...entrypoints, ...sessionEntrypoints] // `entrypoints` = framework defaults
44
47
  ```
45
48
 
46
49
  ```ts
47
- // web/modules.ts — attach screen components, plus call-only elevation for backend routes
48
- elevate(modules, session.list) // callable from the client
49
- modules.push(entrypoint(route(web.session, '/session', frontend({ parent: BASE })), handler(SessionScreen)))
50
+ // web/entrypoints.ts — attach screen components, plus call-only elevation for backend routes
51
+ const entrypoints = [...baseEntrypoints, ...sessionEntrypoints] // `baseEntrypoints` from web-panel
52
+ elevate(entrypoints, session.list) // callable from the client
53
+ entrypoints.push(entrypoint(route(web.session, '/session', frontend({ parent: BASE })), handler(SessionScreen)))
54
+ export const appEntrypoints = entrypoints
50
55
  ```
51
56
 
57
+ A route declaration is plain data: its `path` is the SEGMENT it contributes under its `parent`, and
58
+ nothing ever rewrites it. `session.list` reads `/:sid/items` under `session.base`'s `/session`,
59
+ under the api service's `base: 'api'` — so the address is `GET /api/session/:sid/items`, computed
60
+ on demand by whoever asks. `elevate` is idempotent, so re-elevating an alias is allowed and guards
61
+ given at elevation are added to the declared ones.
62
+
52
63
  Change a route or schema in `common` and both sides stay in sync. See [[entrypoint]], [[route]],
53
64
  [[server-app]], [[web-client]], [[web-panel]].
54
65
 
@@ -61,6 +72,8 @@ prefixes API routes with `/api`:
61
72
  const cfg = service({ type: AppType.Frontend, service: APP_WEB, host: 'localhost', port: 3001 })
62
73
  service({ type: AppType.Backend, service: APP_API, host: 'localhost', port: 3000, base: 'api' }, cfg)
63
74
  cfg.debug = { all: true }
75
+ cfg.alias = APP
76
+ cfg.security = { unsecure: true } // local dev serves the API over plain HTTP
64
77
  export const commonConfig = cfg
65
78
  ```
66
79
 
@@ -75,9 +88,20 @@ appendStaticResource(context, SESSION_ITEMS) // @owlmeans/static-resource —
75
88
  ```
76
89
 
77
90
  Handlers use `handleRequest` / `handleBody` / `handleParams` (validated payload, then context, then
78
- req). Read/write `ctx.getStaticResource<T>(alias)` — full CRUD (`get/load/list/create/save/delete`).
79
- **`static-resource.list()` takes no criteria** — list all and filter in JS (e.g. by `sessionId`).
80
- `main(context, appModules)` starts the server. See [[static-resource]], [[server-app]], [[resource]].
91
+ req). Read/write `ctx.getStaticResource<T>(alias)` — the full resource contract
92
+ (`get/load/list/count/create/update/save/delete/take/purge`), so the resource answers the whole
93
+ question rather than the handler filtering afterwards:
94
+
95
+ ```ts
96
+ const { items } = await resource.list(
97
+ { sessionId: params.sid },
98
+ { sort: [{ field: 'createdAt', order: 'desc' }] }
99
+ )
100
+ ```
101
+
102
+ `list` returns `{ items, total }`; the in-memory backends are unpaged unless a `size` is asked for.
103
+ `main(context, appEntrypoints)` starts the server. See [[static-resource]], [[server-app]],
104
+ [[resource]].
81
105
 
82
106
  Swap `@owlmeans/static-resource` for [[mongo-resource]] / [[redis-resource]] when you need
83
107
  persistence — the handler shape is identical.
@@ -86,19 +110,36 @@ persistence — the handler shape is identical.
86
110
 
87
111
  `@owlmeans/web-panel`'s `PanelApp` is shadcn/Tailwind v4 (no MUI). The **app provides** the shadcn
88
112
  primitives at the `@` alias — `web-panel` references `@/lib/utils` and
89
- `@/components/ui/{alert,button,card,input,label,progress}`; copy those into `src/`. Render with
90
- `provide` from `@owlmeans/web-client`:
113
+ `@/components/ui/{alert,button,card,input,label,navigation-menu,progress}`; copy those into `src/`.
114
+ Routing resolves itself from the active router plugin, so `PanelApp` takes no router prop:
91
115
 
92
116
  ```tsx
93
- basicRender(<PanelApp context={context} provide={provide} />)
117
+ basicRender(<PanelApp context={context} />)
94
118
  ```
95
119
 
96
120
  `vite.config.ts` sets `@`→`src`, `@tailwindcss/vite`, and dedupes the owlmeans/react singletons.
97
121
  `index.css` is `@import "tailwindcss";` + a shadcn `@theme` token block (replaces `@owlmeans/owl-theme`).
98
122
  A parent `BASE` route renders the layout via `handler(LayoutComponent)`; `HOME` is its default child.
99
- Screens call the backend with `context.entrypoint(alias).call({ params, body })` → `[data, outcome]`.
123
+ `index.tsx` calls `context.registerEntrypoints(appEntrypoints)` and `context.serviceRoute(...)` for
124
+ each service, then renders.
125
+
126
+ Screens address the backend through three explicit verbs on the entrypoint:
127
+
128
+ ```tsx
129
+ const items = await ctx.entrypoint<ClientEntrypoint<Item[]>>(session.list).call({ params: { sid } })
130
+ const { value, outcome } = await ctx.entrypoint<ClientEntrypoint<Item>>(session.add).invoke({ body })
131
+ const href = await ctx.entrypoint<ClientEntrypoint<string>>(web.about).url()
132
+ ```
133
+
134
+ `call` resolves to the VALUE and throws the reply's error; `invoke` gives `{ value, outcome }` when
135
+ the outcome decides what happens next; `url` gives the address (`{ absolute: true }` forces a fully
136
+ qualified one). A screen entrypoint answers `url()` and refuses `call()` — a screen is navigated to.
100
137
  See [[web-panel]], [[web-client]], [[shadcn-web]], [[client-entrypoint]].
101
138
 
139
+ The screen keeps nothing in component state: `makeContext` registers a `@owlmeans/state` resource,
140
+ the fetch writes what came back into it with `store.replace(items)`, and `useStoreList` renders the
141
+ live subscription. See [[state]].
142
+
102
143
  ## Authentication
103
144
 
104
145
  This shape is intentionally **auth-free**. To add it: `@owlmeans/server-auth` + `@owlmeans/client-auth`
@@ -7,9 +7,11 @@ export const list = handleParams<SessionParams>(async (params, context) => {
7
7
  const ctx = context as Context
8
8
  const resource = ctx.getStaticResource<SessionItem>(SESSION_ITEMS)
9
9
 
10
- // The static resource lists every record; filter to this session and sort newest first.
11
- const { items } = await resource.list<SessionItem>()
10
+ // The resource answers the whole question this session's items, newest first.
11
+ const { items } = await resource.list(
12
+ { sessionId: params.sid },
13
+ { sort: [{ field: 'createdAt', order: 'desc' }] }
14
+ )
15
+
12
16
  return items
13
- .filter(item => item.sessionId === params.sid)
14
- .sort((a, b) => (a.createdAt < b.createdAt ? 1 : -1))
15
17
  })
@@ -7,7 +7,7 @@ export const remove = handleParams<ItemParams>(async (params, context) => {
7
7
  const ctx = context as Context
8
8
  const resource = ctx.getStaticResource<SessionItem>(SESSION_ITEMS)
9
9
 
10
- const existing = await resource.load<SessionItem>(params.id)
10
+ const existing = await resource.load(params.id)
11
11
  // Only remove the item if it belongs to the requesting session.
12
12
  if (existing == null || existing.sessionId !== params.sid) {
13
13
  return { removed: false }
@@ -10,9 +10,5 @@ export const makeContext = <C extends Config, T extends Context<C>>(cfg: C): T =
10
10
  // data lives in process memory and is cleared when the api restarts.
11
11
  appendStaticResource<C, T>(context, SESSION_ITEMS)
12
12
 
13
- // A child context has to inherit THIS factory, not the layer's — otherwise a derived context
14
- // is built without the resource registered above and every lookup on it throws.
15
- context.makeContext = makeContext as typeof context.makeContext
16
-
17
13
  return context
18
14
  }
@@ -0,0 +1,11 @@
1
+ import { elevate, entrypoints } from '@owlmeans/server-app'
2
+ import { session, sessionEntrypoints } from '__APP_SLUG__-common'
3
+ import * as handlers from './app/session/index.js'
4
+
5
+ // Attach handler implementations to the shared entrypoint declarations.
6
+ elevate(sessionEntrypoints, session.base)
7
+ elevate(sessionEntrypoints, session.list, handlers.list)
8
+ elevate(sessionEntrypoints, session.add, handlers.add)
9
+ elevate(sessionEntrypoints, session.remove, handlers.remove)
10
+
11
+ export const appEntrypoints = [...entrypoints, ...sessionEntrypoints]
@@ -1,9 +1,9 @@
1
1
  import { main } from '@owlmeans/server-app'
2
2
  import config from './config.js'
3
3
  import { makeContext } from './context.js'
4
- import { appModules } from './modules.js'
4
+ import { appEntrypoints } from './entrypoints.js'
5
5
  import type { Config, Context } from './types.js'
6
6
 
7
7
  const context = makeContext<Config, Context>(config)
8
8
 
9
- main<{}, Config, Context>(context, appModules)
9
+ main<{}, Config, Context>(context, appEntrypoints)
@@ -9,7 +9,7 @@ import type { AddItemPayload, ItemParams, SessionParams } from './types.js'
9
9
  * elevates them with screen components and calls them. Routes resolve under the
10
10
  * api service `base` (`/api`), so e.g. `session.list` → `GET /api/session/:sid/items`.
11
11
  */
12
- export const sessionModules = [
12
+ export const sessionEntrypoints = [
13
13
  entrypoint(route(session.base, '/session')),
14
14
  entrypoint(
15
15
  route(session.list, '/:sid/items', { parent: session.base, method: RouteMethod.GET }),
@@ -2,4 +2,4 @@ export * from './consts.js'
2
2
  export * from './types.js'
3
3
  export * from './schemas.js'
4
4
  export * from './config.js'
5
- export * from './modules.js'
5
+ export * from './entrypoints.js'
@@ -15,9 +15,5 @@ export const makeContext = <C extends Config, T extends Context<C>>(cfg: C): T =
15
15
  // the app in a module of its own.
16
16
  appendStateResource<C, T>(context, SESSION_STATE)
17
17
 
18
- // A child context has to inherit THIS factory, not the layer's. Without the line, a derived
19
- // context is built without the resource registered above and every lookup on it throws.
20
- context.makeContext = makeContext as typeof context.makeContext
21
-
22
18
  return context
23
19
  }
@@ -0,0 +1,22 @@
1
+ import { BASE, elevate, entrypoint, entrypoints as baseEntrypoints, frontend, handler, HOME, route } from '@owlmeans/web-panel'
2
+ import { session, sessionEntrypoints, web } from '__APP_SLUG__-common'
3
+ import { MainLayout } from './layout/main.js'
4
+ import { AboutScreen } from './screens/about.js'
5
+ import { HomeScreen } from './screens/home.js'
6
+ import { SessionScreen } from './screens/session.js'
7
+
8
+ const entrypoints = [...baseEntrypoints, ...sessionEntrypoints]
9
+
10
+ // Backend entrypoints — elevated without a component so the client can call them.
11
+ elevate(entrypoints, session.base)
12
+ elevate(entrypoints, session.list)
13
+ elevate(entrypoints, session.add)
14
+ elevate(entrypoints, session.remove)
15
+
16
+ // Frontend layout + screens. BASE renders the shared layout; HOME is its default child.
17
+ entrypoints.push(entrypoint(route(BASE, '/', frontend()), handler(MainLayout)))
18
+ entrypoints.push(entrypoint(route(HOME, '/', frontend({ default: true, parent: BASE })), handler(HomeScreen)))
19
+ entrypoints.push(entrypoint(route(web.session, '/session', frontend({ parent: BASE })), handler(SessionScreen)))
20
+ entrypoints.push(entrypoint(route(web.about, '/about', frontend({ parent: BASE })), handler(AboutScreen)))
21
+
22
+ export const appEntrypoints = entrypoints
@@ -2,12 +2,12 @@ import './index.css'
2
2
  import { APP_API, APP_WEB } from '__APP_SLUG__-common'
3
3
  import config from './config.js'
4
4
  import { makeContext } from './context.js'
5
- import { appModules } from './modules.js'
5
+ import { appEntrypoints } from './entrypoints.js'
6
6
  import { render } from './render.js'
7
7
  import type { Config, Context } from './types.js'
8
8
 
9
9
  const context = makeContext<Config, Context>(config)
10
- context.registerEntrypoints(appModules)
10
+ context.registerEntrypoints(appEntrypoints)
11
11
 
12
12
  context.serviceRoute(APP_WEB, true)
13
13
  context.serviceRoute(APP_API, true)
@@ -33,15 +33,15 @@ export const SessionScreen: FC = () => {
33
33
  */
34
34
  const items = useStoreList<SessionItem>({ query: {}, resource: SESSION_STATE })
35
35
 
36
- // The server is the source of truth; the store is what the screen reads. Fetch once, write
37
- // what came back into the store, and let the subscription render it.
36
+ // The server is the source of truth; the store is what the screen reads. Fetch once, install
37
+ // what came back, and let the subscription render it. `replace` rather than a save per item:
38
+ // the endpoint answers with the session's whole set, so an item removed on another tab has to
39
+ // leave the store too, and one write wakes the subscribers once instead of once per record.
38
40
  const load = async () => {
39
- const [data] = await ctx
41
+ const data = await ctx
40
42
  .entrypoint<ClientEntrypoint<SessionItem[]>>(session.list)
41
43
  .call({ params: { sid } })
42
- for (const item of data ?? []) {
43
- await store.save(item)
44
- }
44
+ await store.replace(data ?? [])
45
45
  }
46
46
 
47
47
  useEffect(() => { void load() }, [])
@@ -50,7 +50,7 @@ export const SessionScreen: FC = () => {
50
50
  if (text.trim() === '') return
51
51
  setBusy(true)
52
52
  try {
53
- const [item] = await ctx
53
+ const item = await ctx
54
54
  .entrypoint<ClientEntrypoint<SessionItem>>(session.add)
55
55
  .call({ params: { sid }, body: { text } })
56
56
  setText('')
@@ -1,11 +0,0 @@
1
- import { elevate, modules } from '@owlmeans/server-app'
2
- import { session, sessionModules } from '__APP_SLUG__-common'
3
- import * as handlers from './app/session/index.js'
4
-
5
- // Attach handler implementations to the shared entrypoint declarations.
6
- elevate(sessionModules, session.base)
7
- elevate(sessionModules, session.list, handlers.list)
8
- elevate(sessionModules, session.add, handlers.add)
9
- elevate(sessionModules, session.remove, handlers.remove)
10
-
11
- export const appModules = [...modules, ...sessionModules]
@@ -1,22 +0,0 @@
1
- import { BASE, elevate, entrypoint, frontend, handler, HOME, modules as baseModules, route } from '@owlmeans/web-panel'
2
- import { session, sessionModules, web } from '__APP_SLUG__-common'
3
- import { MainLayout } from './layout/main.js'
4
- import { AboutScreen } from './screens/about.js'
5
- import { HomeScreen } from './screens/home.js'
6
- import { SessionScreen } from './screens/session.js'
7
-
8
- const modules = [...baseModules, ...sessionModules]
9
-
10
- // Backend entrypoints — elevated without a component so the client can call them.
11
- elevate(modules, session.base)
12
- elevate(modules, session.list)
13
- elevate(modules, session.add)
14
- elevate(modules, session.remove)
15
-
16
- // Frontend layout + screens. BASE renders the shared layout; HOME is its default child.
17
- modules.push(entrypoint(route(BASE, '/', frontend()), handler(MainLayout)))
18
- modules.push(entrypoint(route(HOME, '/', frontend({ default: true, parent: BASE })), handler(HomeScreen)))
19
- modules.push(entrypoint(route(web.session, '/session', frontend({ parent: BASE })), handler(SessionScreen)))
20
- modules.push(entrypoint(route(web.about, '/about', frontend({ parent: BASE })), handler(AboutScreen)))
21
-
22
- export const appModules = modules