@happyvertical/smrt-template-sveltekit 0.37.5 → 0.37.7

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/AGENTS.md CHANGED
@@ -12,8 +12,10 @@ Base SvelteKit project template used by `smrt init`. Scaffolds a full-stack, mul
12
12
 
13
13
  - `template/src/hooks.server.ts` — pre-wires `enableTenancy()`, `createSessionHandler({ enterTenantContext: true })`, and a subdomain → tenantId handle, sequenced in that order
14
14
  - `template/src/lib/server/tenancy.ts` — pluggable tenant resolver (`subdomainStrategy`, `pathPrefixStrategy`, `headerStrategy`, `createTenantResolver`)
15
- - `template/src/lib/server/smrt.ts` — centralized SmrtClassOptions / collection factory
15
+ - `template/src/lib/server/smrt.ts` — centralized SmrtClassOptions / collection factory; imports the plugin-generated `smrt-register.js` (guarded — the file is gitignored and regenerated on every dev/build run) and seeds `.smrt/manifest.json` so server runtimes get package-qualified registrations + scanned field metadata (without them, writes silently drop domain columns)
16
+ - `template/vite.config.ts` — smrtPlugin() + smrtConsumer(), plus explicit `oxc: { decorator: { legacy: true } }` (Vite 8's oxc transform does not reliably honor `experimentalDecorators` through the SvelteKit tsconfig `extends` chain)
16
17
  - `template/src/lib/objects/Item.ts` — example `@smrt()` object
18
+ - `template/src/routes/+page.server.ts` + `+page.svelte` — reference SSR data loading: server load queries collections directly (opt-in read cache via `cache: { ttl }`), declares `depends('smrt:items')`, and a form action + `invalidate('smrt:items')` demonstrates post-mutation refresh
17
19
  - `template/src/app.d.ts` — `App.Locals` extends `SessionLocals` from `@happyvertical/smrt-users/sveltekit`
18
20
 
19
21
  ## Test Infrastructure
@@ -23,6 +25,7 @@ Base SvelteKit project template used by `smrt init`. Scaffolds a full-stack, mul
23
25
 
24
26
  ## Key Patterns
25
27
 
28
+ - **SSR data loading convention**: reference pages load collection data in `+page.server.ts` (serialized into the HTML, hydrated without a duplicate client fetch). Loads declare `depends('smrt:<collection>')` (REST route segment naming: `/api/items` → `smrt:items`); mutations call `invalidate('smrt:<collection>')` to re-run them.
26
29
  - **Pluggable tenant resolver**: `tenancy.ts` exports strategy functions + a `createTenantResolver()` factory. Consumers swap strategies by editing one line.
27
30
  - **File copying with placeholder substitution**: project name is replaced in template files during generation.
28
31
  - **No template-internal test pollution**: `__tests__/` and the `.svelte-kit/` stub are package-level and excluded from `copyTemplate` output.
package/README.md CHANGED
@@ -7,6 +7,7 @@ SvelteKit project template with SMRT framework integration. Scaffolds a full-sta
7
7
  - SvelteKit 2.x with Svelte 5 and TypeScript
8
8
  - `smrtPlugin()` Vite integration for automatic REST API route generation
9
9
  - Example `@smrt()` object (`Item.ts`) with barrel export
10
+ - Reference SSR data loading: `+page.server.ts` server load with the `depends('smrt:<collection>')` / `invalidate('smrt:<collection>')` refresh convention and opt-in collection read caching
10
11
  - Server-side SMRT initialization (`src/lib/server/smrt.ts`)
11
12
  - `smrt.config.ts` with SQLite database and optional AI provider
12
13
  - `.env.example` with starter environment variables
@@ -81,9 +82,11 @@ template/
81
82
  │ │ ├── index.ts # Barrel export
82
83
  │ │ └── Item.ts # Example @smrt() object
83
84
  │ └── server/
84
- │ ├── smrt.ts # Server-side SMRT initialization
85
+ │ ├── smrt.ts # Server-side SMRT initialization + manifest seeding
86
+ │ ├── smrt-register.ts # Object registration (generated by smrtPlugin, gitignored)
85
87
  │ └── tenancy.ts # Pluggable tenant resolver
86
88
  └── routes/
89
+ ├── +page.server.ts # Home page server load + demo mutation (reference pattern)
87
90
  └── +page.svelte # Home page
88
91
  ```
89
92
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-template-sveltekit",
3
- "version": "0.37.5",
3
+ "version": "0.37.7",
4
4
  "description": "SvelteKit project template with SMRT framework integration",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -29,7 +29,7 @@
29
29
  "directory": "packages/template-sveltekit"
30
30
  },
31
31
  "peerDependencies": {
32
- "@happyvertical/smrt-core": "0.37.5"
32
+ "@happyvertical/smrt-core": "0.37.7"
33
33
  },
34
34
  "devDependencies": {
35
35
  "vitest": "^4.1.9"
@@ -16,6 +16,15 @@ tools, and agent/developer knowledge artifacts.
16
16
 
17
17
  ## Package Guidance
18
18
 
19
+ - Load page data in `+page.server.ts` server loads that query collections
20
+ directly (see `src/routes/+page.server.ts`); never fetch `/api/*` from
21
+ `$effect`/`onMount` for initial page data.
22
+ - Declare `depends('smrt:<collection>')` in loads (route segment naming:
23
+ `/api/items` → `smrt:items`) and call `invalidate('smrt:<collection>')`
24
+ after mutations to refresh in place.
25
+ - Opt read-heavy SSR reads into the collection cache with
26
+ `list({ cache: { ttl } })`; skip it for per-user data and admin editors
27
+ (see README "Data loading").
19
28
  - Keep SMRT object relationship metadata close to the `@smrt()` decorator.
20
29
  - Use `knowledge: false` only for objects that should stay out of authored
21
30
  agent context while remaining in the runtime manifest.
@@ -34,16 +34,137 @@ A SvelteKit application with SMRT framework integration for rapid development of
34
34
  │ │ │ ├── index.ts # Export all objects here
35
35
  │ │ │ └── Item.ts # Example SMRT object
36
36
  │ │ └── server/
37
- │ │ ├── smrt.ts # SMRT configuration
37
+ │ │ ├── smrt.ts # SMRT configuration + local manifest seeding
38
+ │ │ ├── smrt-register.ts # Object registration (generated by smrtPlugin, gitignored)
38
39
  │ │ └── tenancy.ts # Pluggable tenant resolver
39
40
  │ └── routes/
40
41
  │ ├── api/ # Auto-generated API routes (don't edit!)
42
+ │ ├── +page.server.ts # Home page server load + demo mutation (reference pattern)
41
43
  │ └── +page.svelte # Home page
42
44
  ├── smrt.config.ts # Root SMRT config
43
45
  ├── vite.config.ts # Vite + SMRT plugin config
44
46
  └── svelte.config.js # SvelteKit config
45
47
  ```
46
48
 
49
+ ## Data loading (SSR + hydration)
50
+
51
+ The home page (`src/routes/+page.server.ts` + `+page.svelte`) is the reference
52
+ pattern for getting collection data onto a page. Copy it for your own routes.
53
+
54
+ **Query collections in a server `load`, not with `fetch` on mount.** A server
55
+ load runs during SSR, reads the database directly through the collection API
56
+ (no HTTP round-trip through your own `/api/*` routes), and SvelteKit
57
+ serializes the result into the initial HTML. The client hydrates that data
58
+ without re-fetching — the page renders with its data on first paint, and the
59
+ first client render issues no duplicate request.
60
+
61
+ ```typescript
62
+ // src/routes/+page.server.ts
63
+ import type { Item } from '$lib/objects/Item';
64
+ import { getCollection } from '$lib/server/smrt';
65
+ import type { PageServerLoad } from './$types';
66
+
67
+ export const load: PageServerLoad = async ({ depends }) => {
68
+ depends('smrt:items'); // see "Refreshing after mutations" below
69
+
70
+ const items = await getCollection<Item>('Item');
71
+ const rows = await items.list({ orderBy: 'created_at DESC', limit: 50 });
72
+
73
+ // Return plain serializable objects, not class instances.
74
+ return {
75
+ items: rows.map((item) => ({ id: item.id, title: item.title, status: item.status })),
76
+ };
77
+ };
78
+ ```
79
+
80
+ ```svelte
81
+ <!-- src/routes/+page.svelte -->
82
+ <script lang="ts">
83
+ import type { PageProps } from './$types';
84
+
85
+ let { data }: PageProps = $props();
86
+ </script>
87
+
88
+ {#each data.items as item (item.id)}
89
+ <p>{item.title}</p>
90
+ {/each}
91
+ ```
92
+
93
+ Avoid the mount-time fetch anti-pattern (`$effect(() => { fetch('/api/items') ... })`):
94
+ it renders an empty page first, waterfalls a second request on every
95
+ navigation, and skips SvelteKit's load/hydration machinery entirely. Reserve
96
+ client-side `fetch` of the generated `/api/*` routes for after-load
97
+ interactions (mutations, polling, external clients) — not for initial page
98
+ data.
99
+
100
+ ### Refreshing after mutations (`depends` / `invalidate`)
101
+
102
+ Convention: each load declares one dependency key per collection it reads,
103
+ named `smrt:<collection>` — where `<collection>` is the generated REST route
104
+ segment (`/api/items` → `smrt:items`). After a mutation, client code calls
105
+ `invalidate('smrt:<collection>')` and every load that declared that key
106
+ re-runs, updating the page in place:
107
+
108
+ ```svelte
109
+ <script lang="ts">
110
+ import { invalidate } from '$app/navigation';
111
+
112
+ async function createItem(title: string) {
113
+ await fetch('/api/items', {
114
+ method: 'POST',
115
+ headers: { 'content-type': 'application/json' },
116
+ body: JSON.stringify({ title }),
117
+ });
118
+ await invalidate('smrt:items'); // re-runs every load that declared depends('smrt:items')
119
+ }
120
+ </script>
121
+ ```
122
+
123
+ The home page demonstrates the same flow with a progressively-enhanced form
124
+ action: `use:enhance` applies the action result with
125
+ `update({ invalidateAll: false })`, then calls `invalidate('smrt:items')` so
126
+ only the loads that depend on items re-run (instead of SvelteKit's default
127
+ invalidate-everything). Note that the generated `/api/*` routes are
128
+ fail-closed — mutations 401 unless the request is authenticated or the object
129
+ opts in via `@smrt({ api: { public: true } })` — which is why the demo uses a
130
+ form action running server-side.
131
+
132
+ ### Caching SSR reads
133
+
134
+ Server-side reads can opt into the collection read cache, per call or per
135
+ model:
136
+
137
+ ```typescript
138
+ // Per call — this load only
139
+ const rows = await items.list({ where: { status: 'published' }, cache: { ttl: 60_000 } });
140
+
141
+ // Per model — every read of this collection
142
+ @smrt({ cache: { ttl: 60_000 } })
143
+ export class Item extends SmrtObject { /* ... */ }
144
+ ```
145
+
146
+ Cached rows are keyed by the final SQL + parameters and live for `ttl`
147
+ milliseconds. Every mutation that goes through SMRT (`create()`, `save()`,
148
+ `delete()`) invalidates the table's cached entries in this process
149
+ automatically — so the post-mutation `invalidate('smrt:...')` re-run sees
150
+ fresh rows, as the home page demonstrates.
151
+
152
+ Use it for read-heavy, write-rare data rendered for many visitors: catalogs,
153
+ published content, navigation. Do **not** cache:
154
+
155
+ - **Per-user data** (dashboards, "my items"): each user's query shape churns
156
+ the cache for little reuse, and any staleness is personally visible.
157
+ - **Admin editors / read-your-writes flows**: editors must always see the
158
+ row they just changed, including across processes.
159
+ - **Multi-replica deployments without `crossProcess: true`**: the cache is
160
+ per-process, so a write on one replica leaves the others stale until TTL.
161
+ Set `cache: { ttl, crossProcess: true }` (uses the database adapter's
162
+ notification capability, e.g. Postgres LISTEN/NOTIFY) or keep the TTL
163
+ short enough to tolerate.
164
+
165
+ Pass `cache: false` on a specific call to bypass a model-level cache where
166
+ freshness matters.
167
+
47
168
  ## Multi-tenancy
48
169
 
49
170
  This template ships with multi-tenancy pre-wired. Out of the box you get:
@@ -20,8 +20,8 @@
20
20
  "vite": "^8.1.2"
21
21
  },
22
22
  "dependencies": {
23
- "@happyvertical/smrt-core": "^0.37.5",
24
- "@happyvertical/smrt-tenancy": "^0.37.5",
25
- "@happyvertical/smrt-users": "^0.37.5"
23
+ "@happyvertical/smrt-core": "^0.37.7",
24
+ "@happyvertical/smrt-tenancy": "^0.37.7",
25
+ "@happyvertical/smrt-users": "^0.37.7"
26
26
  }
27
27
  }
@@ -25,7 +25,7 @@
25
25
  */
26
26
 
27
27
  import { sequence } from '@sveltejs/kit/hooks';
28
- import type { Handle, RequestEvent } from '@sveltejs/kit';
28
+ import type { Handle } from '@sveltejs/kit';
29
29
 
30
30
  import { createSessionHandler } from '@happyvertical/smrt-users/sveltekit';
31
31
  import {
@@ -59,9 +59,10 @@ enableTenancy();
59
59
  */
60
60
  const tenancyHandle = createSvelteKitHandle({
61
61
  resolveTenantId: async (event) => {
62
- // The createSvelteKitHandle adapter passes a structural event; our
63
- // resolver accepts that same shape.
64
- const result = await resolveTenant(event as RequestEvent);
62
+ // The createSvelteKitHandle adapter passes a structural event that is
63
+ // directly assignable to the resolver's `TenantResolverEvent` shape
64
+ // (`url` + `request.headers`), so no cast is needed.
65
+ const result = await resolveTenant(event);
65
66
  return result.tenantId;
66
67
  },
67
68
  }) as unknown as Handle;
@@ -5,11 +5,41 @@
5
5
  * Import this file in your routes to get properly configured collections.
6
6
  */
7
7
 
8
- import { ObjectRegistry, type SmrtClassOptions } from '@happyvertical/smrt-core';
8
+ import { existsSync } from 'node:fs';
9
+ import { join } from 'node:path';
9
10
 
10
- // Import all SMRT objects to register them
11
+ import {
12
+ ObjectRegistry,
13
+ type SmrtClassOptions,
14
+ type SmrtObject,
15
+ } from '@happyvertical/smrt-core';
16
+ import { loadManifestFromPathSync } from '@happyvertical/smrt-core/manifest';
17
+
18
+ // Import all SMRT objects to register them.
11
19
  import '../objects/index.js';
12
20
 
21
+ // Upgrade those registrations to package-qualified identities
22
+ // (`<package>:<Class>`) — required to link each class to its scanned field
23
+ // metadata below. smrtPlugin() generates this module on every dev/build run
24
+ // (it is gitignored, like the generated API routes); guard the import so a
25
+ // fresh checkout that has not run the plugin yet still boots.
26
+ try {
27
+ await import('./smrt-register.js');
28
+ } catch {
29
+ // Not generated yet — the first `vite dev` / `vite build` will create it.
30
+ }
31
+
32
+ // Hydrate field metadata for this app's own objects from the local manifest
33
+ // written by smrtPlugin() (`.smrt/manifest.json`). The decorator import above
34
+ // registers the classes, but their scanned field metadata (plain properties
35
+ // like `title: string = ''`) lives in the manifest — without seeding it, the
36
+ // runtime only knows the framework base fields and server-side writes would
37
+ // silently drop your domain columns.
38
+ const localManifestPath = join(process.cwd(), '.smrt', 'manifest.json');
39
+ if (existsSync(localManifestPath)) {
40
+ loadManifestFromPathSync(localManifestPath);
41
+ }
42
+
13
43
  declare global {
14
44
  // eslint-disable-next-line no-var
15
45
  var __smrtGetRequestScopedDatabase:
@@ -67,7 +97,7 @@ export function getSmrtConfig(className: string): SmrtClassOptions {
67
97
  * const products = await getCollection<Product>('Product');
68
98
  * const items = await products.list();
69
99
  */
70
- export async function getCollection<T>(className: string) {
100
+ export async function getCollection<T extends SmrtObject>(className: string) {
71
101
  const config = getSmrtConfig(className);
72
102
  const objectOverride = objectOverrides[className];
73
103
  const requestScopedDb = objectOverride?.db
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Home page server load + demo mutation.
3
+ *
4
+ * This file is the reference data-loading pattern for SMRT + SvelteKit apps:
5
+ *
6
+ * 1. **Query collections in a server `load`** — not with a client-side
7
+ * `fetch` on mount. SvelteKit runs this during SSR, serializes the
8
+ * returned data into the initial HTML, and hydrates it on the client
9
+ * without re-fetching. The page renders with its data on first paint;
10
+ * there is no empty-then-fetch waterfall and no duplicate first-render
11
+ * request. Server loads read the database directly through the
12
+ * collection API — they do not round-trip through your own
13
+ * `/api/*` routes.
14
+ *
15
+ * 2. **Declare one dependency key per collection** with
16
+ * `depends('smrt:<collection>')`, where `<collection>` matches the
17
+ * generated REST route segment (`/api/items` → `smrt:items`). After a
18
+ * mutation, client code calls `invalidate('smrt:<collection>')`
19
+ * (see `+page.svelte`) and every load that declared the key re-runs.
20
+ *
21
+ * 3. **Opt read-heavy SSR reads into the collection read cache** with
22
+ * `cache: { ttl }`. Mutations that go through SMRT (`create()`,
23
+ * `save()`, `delete()`) invalidate the table's cache entries
24
+ * in-process automatically, so the post-mutation `invalidate()`
25
+ * re-runs this load against fresh rows. See "Data loading" in the
26
+ * README for multi-replica caveats and when NOT to cache.
27
+ */
28
+
29
+ import { fail } from '@sveltejs/kit';
30
+ import type { Item } from '$lib/objects/Item';
31
+ import { getCollection } from '$lib/server/smrt';
32
+ import type { Actions, PageServerLoad } from './$types';
33
+
34
+ export const load: PageServerLoad = async ({ depends }) => {
35
+ // Convention: `smrt:<collection>`. Registered before the query so even a
36
+ // failed load (e.g. database not initialized yet) re-runs on invalidation.
37
+ depends('smrt:items');
38
+
39
+ try {
40
+ const items = await getCollection<Item>('Item');
41
+ const rows = await items.list({
42
+ orderBy: 'created_at DESC',
43
+ limit: 50,
44
+ // Opt this SSR read into the collection read cache (issue #1499).
45
+ // Safe here because all writes go through SMRT, which invalidates the
46
+ // table's entries in this process. Running multiple replicas? Add
47
+ // `crossProcess: true` or drop the cache — see the README.
48
+ cache: { ttl: 30_000 },
49
+ });
50
+
51
+ return {
52
+ // Return plain serializable objects: SvelteKit encodes load data into
53
+ // the initial HTML with devalue, so pick the fields the page needs
54
+ // rather than returning class instances. Persisted rows always carry a
55
+ // UUID `id`; the filter narrows the field's `string | null | undefined`
56
+ // type instead of coercing — a '' fallback would produce colliding
57
+ // `{#each}` keys if an id were ever missing.
58
+ items: rows
59
+ .filter((item): item is Item & { id: string } => Boolean(item.id))
60
+ .map((item) => ({
61
+ id: item.id,
62
+ title: item.title,
63
+ status: item.status,
64
+ })),
65
+ loadError: null as string | null,
66
+ };
67
+ } catch (e) {
68
+ // First run before `smrt db:setup` has created the tables: render the
69
+ // page with guidance instead of a 500.
70
+ return {
71
+ items: [] as { id: string; title: string; status: string }[],
72
+ loadError: e instanceof Error ? e.message : 'Failed to load items',
73
+ };
74
+ }
75
+ };
76
+
77
+ export const actions: Actions = {
78
+ /**
79
+ * Demo mutation for the invalidation convention: creates an Item, then the
80
+ * client (`+page.svelte`) calls `invalidate('smrt:items')` so the load
81
+ * above re-runs and the new row appears without a full page reload.
82
+ *
83
+ * NOTE: this demo action is intentionally unauthenticated so the refresh
84
+ * flow works out of the box. Real mutations should be gated on
85
+ * `locals.user` / permissions (the generated `/api/*` routes are already
86
+ * fail-closed: they 401 unless authenticated or `@smrt({ api: { public } })`).
87
+ */
88
+ create: async ({ request }) => {
89
+ const form = await request.formData();
90
+ const title = String(form.get('title') ?? '').trim();
91
+
92
+ if (!title) {
93
+ return fail(400, { title, error: 'Title is required' });
94
+ }
95
+
96
+ try {
97
+ const items = await getCollection<Item>('Item');
98
+ await items.create({ title });
99
+ return { created: title };
100
+ } catch (e) {
101
+ return fail(500, {
102
+ title,
103
+ error: e instanceof Error ? e.message : 'Failed to create item',
104
+ });
105
+ }
106
+ },
107
+ };
@@ -1,23 +1,15 @@
1
1
  <script lang="ts">
2
- let items: any[] = $state([]);
3
- let loading = $state(true);
4
- let error: string | null = $state(null);
5
-
6
- $effect(() => {
7
- fetchItems();
8
- });
9
-
10
- async function fetchItems() {
11
- try {
12
- const response = await fetch('/api/items');
13
- const data = await response.json();
14
- items = data.items || [];
15
- } catch (e) {
16
- error = e instanceof Error ? e.message : 'Failed to fetch items';
17
- } finally {
18
- loading = false;
19
- }
20
- }
2
+ import { enhance } from '$app/forms';
3
+ import { invalidate } from '$app/navigation';
4
+ import type { PageProps } from './$types';
5
+
6
+ // `data` comes from the server load in `+page.server.ts`. It is fetched
7
+ // during SSR, serialized into the initial HTML, and hydrated here — the
8
+ // first client render issues NO fetch for it. It re-runs only when
9
+ // something calls `invalidate('smrt:items')` (below) or on navigation.
10
+ let { data, form }: PageProps = $props();
11
+
12
+ let creating = $state(false);
21
13
  </script>
22
14
 
23
15
  <svelte:head>
@@ -30,15 +22,14 @@
30
22
  <section>
31
23
  <h2>Items</h2>
32
24
 
33
- {#if loading}
34
- <p>Loading...</p>
35
- {:else if error}
36
- <p class="error">{error}</p>
37
- {:else if items.length === 0}
38
- <p>No items yet. Create your first item!</p>
25
+ {#if data.loadError}
26
+ <p class="error">{data.loadError}</p>
27
+ <p>If this is a fresh project, initialize the database with <code>smrt db:setup</code>.</p>
28
+ {:else if data.items.length === 0}
29
+ <p>No items yet. Create your first item below!</p>
39
30
  {:else}
40
31
  <ul>
41
- {#each items as item}
32
+ {#each data.items as item (item.id)}
42
33
  <li>
43
34
  <strong>{item.title}</strong>
44
35
  <span class="status">{item.status}</span>
@@ -46,12 +37,58 @@
46
37
  {/each}
47
38
  </ul>
48
39
  {/if}
40
+
41
+ <!--
42
+ Post-mutation refresh convention: submit the mutation (a form action
43
+ here, but a fetch to a generated /api route works the same way), then
44
+ call `invalidate('smrt:items')`. Every load that declared
45
+ `depends('smrt:items')` re-runs and the list updates in place.
46
+ Without JavaScript this form still works — SvelteKit falls back to a
47
+ full-page POST + re-render.
48
+ -->
49
+ <form
50
+ method="POST"
51
+ action="?/create"
52
+ use:enhance={() => {
53
+ creating = true;
54
+ return async ({ result, update }) => {
55
+ try {
56
+ // Apply the action result (sets `form`, resets the input) but
57
+ // skip SvelteKit's default invalidateAll() — we re-run only the
58
+ // loads that depend on this collection.
59
+ await update({ invalidateAll: false });
60
+ if (result.type === 'success') {
61
+ await invalidate('smrt:items');
62
+ }
63
+ } finally {
64
+ // Always re-enable the form, even if update()/invalidate()
65
+ // throws (network error, aborted navigation, …).
66
+ creating = false;
67
+ }
68
+ };
69
+ }}
70
+ >
71
+ <input
72
+ name="title"
73
+ placeholder="New item title"
74
+ aria-label="New item title"
75
+ required
76
+ />
77
+ <button type="submit" disabled={creating}>
78
+ {creating ? 'Creating…' : 'Create item'}
79
+ </button>
80
+ </form>
81
+
82
+ {#if form?.error}
83
+ <p class="error">{form.error}</p>
84
+ {/if}
49
85
  </section>
50
86
 
51
87
  <section>
52
88
  <h2>Getting Started</h2>
53
89
  <ul>
54
90
  <li>Edit <code>src/lib/objects/Item.ts</code> to customize your SMRT object</li>
91
+ <li>Load data in <code>+page.server.ts</code> files — see this page's load for the SSR + <code>depends</code>/<code>invalidate</code> pattern</li>
55
92
  <li>Run <code>smrt objects</code> to see registered objects</li>
56
93
  <li>Run <code>smrt generate-routes</code> to regenerate API routes</li>
57
94
  <li>API routes are auto-generated in <code>src/routes/api/</code></li>
@@ -90,6 +127,35 @@
90
127
  margin-left: 8px;
91
128
  }
92
129
 
130
+ form {
131
+ display: flex;
132
+ gap: 0.5rem;
133
+ margin-top: 1rem;
134
+ }
135
+
136
+ input {
137
+ flex: 1;
138
+ padding: 0.5rem;
139
+ border: 1px solid #ccc;
140
+ border-radius: var(--smrt-radius-sm, 4px);
141
+ font-size: 1em;
142
+ }
143
+
144
+ button {
145
+ padding: 0.5rem 1rem;
146
+ border: none;
147
+ border-radius: var(--smrt-radius-sm, 4px);
148
+ background: #667eea;
149
+ color: white;
150
+ font-size: 1em;
151
+ cursor: pointer;
152
+ }
153
+
154
+ button:disabled {
155
+ opacity: 0.6;
156
+ cursor: wait;
157
+ }
158
+
93
159
  code {
94
160
  background: #e0e0e0;
95
161
  padding: 2px 6px;
@@ -1,8 +1,21 @@
1
1
  import { sveltekit } from '@sveltejs/kit/vite';
2
+ import { smrtConsumer } from '@happyvertical/smrt-core/consumer-plugin';
2
3
  import { smrtPlugin } from '@happyvertical/smrt-core/vite-plugin';
3
4
  import { defineConfig } from 'vite';
4
5
 
5
6
  export default defineConfig({
7
+ // Lower `@smrt()` legacy (experimentalDecorators) decorators explicitly.
8
+ // tsconfig.json sets experimentalDecorators, but Vite's oxc transform does
9
+ // not reliably honor it through the SvelteKit `extends
10
+ // "./.svelte-kit/tsconfig.json"` chain — raw `@decorator class` syntax then
11
+ // reaches the SSR runtime and throws `SyntaxError: Invalid or unexpected
12
+ // token` on the first request.
13
+ oxc: {
14
+ decorator: {
15
+ legacy: true,
16
+ emitDecoratorMetadata: true,
17
+ },
18
+ },
6
19
  plugins: [
7
20
  sveltekit(),
8
21
  smrtPlugin({
@@ -17,5 +30,9 @@ export default defineConfig({
17
30
  configFileName: 'smrt.ts',
18
31
  },
19
32
  }),
33
+ // Required alongside smrtPlugin(): this project depends on external SMRT
34
+ // packages (@happyvertical/smrt-users, smrt-tenancy, …), and the consumer
35
+ // plugin emits `.smrt/register.js` so their classes load for CLI/runtime.
36
+ smrtConsumer(),
20
37
  ],
21
38
  });