create-mikstack 0.1.24 → 0.1.25

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-mikstack",
3
- "version": "0.1.24",
3
+ "version": "0.1.25",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -8,10 +8,14 @@ SvelteKit app with Drizzle ORM, Zero (real-time sync), and better-auth.
8
8
  - **Database**: PostgreSQL via Drizzle ORM
9
9
  - **Real-time**: Zero (@rocicorp/zero) for client-side sync
10
10
  - **Auth**: better-auth with magic link
11
- - **Styling**: See `src/app.css`
11
+ - **UI**: @mikstack/ui components (Button, Input, FormField, Textarea, Separator)
12
+ - **Styling**: Design tokens in `src/app.css` — oklch colors, spacing/radius variables, dark mode via `prefers-color-scheme`
12
13
  <!-- {{#if:i18n}} -->
13
14
  - **i18n**: @mikstack/svelte-lingui (Lingui-based)
14
15
  <!-- {{/if:i18n}} -->
16
+ <!-- {{#if:testing}} -->
17
+ - **Testing**: Vitest + @testcontainers/postgresql for integration tests
18
+ <!-- {{/if:testing}} -->
15
19
 
16
20
  ## Key Patterns
17
21
 
@@ -28,11 +32,19 @@ const data = $derived(query.data);
28
32
  await z.mutate(mutators.myMutation(args));
29
33
  ```
30
34
 
35
+ Mutators verify ownership before update/delete via `tx.run()`:
36
+
37
+ ```typescript
38
+ const entity = await tx.run(zql.note.where("id", args.id).one());
39
+ if (!entity || entity.userId !== ctx.userID) return;
40
+ ```
41
+
31
42
  ### Auth
32
43
 
33
44
  Server: `src/lib/server/auth.ts` (better-auth config)
34
45
  Client: `src/lib/auth-client.ts` (magic link + client helpers)
35
46
  Session is available in `locals.user` and `locals.session` via `src/hooks.server.ts`.
47
+ Routes under `(app)/` require authentication — unauthenticated requests redirect to `/sign-in`.
36
48
 
37
49
  ### Forms (@mikstack/form)
38
50
 
@@ -85,9 +97,23 @@ Email delivery tracking and retries are handled automatically.
85
97
 
86
98
  ### Database
87
99
 
88
- Schema: `src/lib/server/db/schema.ts`
100
+ Schema: `src/lib/server/db/schema.ts` (uses `casing: "snake_case"` — no explicit column names needed)
89
101
  Connection: `src/lib/server/db/index.ts` (lazy-initialized via Proxy)
90
102
 
103
+ <!-- {{#if:testing}} -->
104
+ ### Testing
105
+
106
+ Test utils: `src/lib/server/db/test-utils.ts` — `createTestDatabase()` spins up a PostgreSQL container via @testcontainers/postgresql.
107
+
108
+ ```typescript
109
+ import { createTestDatabase, stopTestDatabase, type TestDatabase } from "$lib/server/db/test-utils";
110
+
111
+ let testDb: TestDatabase;
112
+ beforeAll(async () => { testDb = await createTestDatabase(); });
113
+ afterAll(async () => { await stopTestDatabase(testDb); });
114
+ ```
115
+
116
+ <!-- {{/if:testing}} -->
91
117
  <!-- {{#if:i18n}} -->
92
118
  ### i18n
93
119
 
@@ -95,7 +121,7 @@ Setup: `src/lib/i18n.ts` — initialized in root layout
95
121
  Config: `lingui.config.ts`
96
122
  Catalogs: `src/locales/{locale}.po`
97
123
 
98
- Use `useLingui()` for translations, `<T>` component for rich text:
124
+ Use `useLingui()` for translations in Svelte components:
99
125
 
100
126
  ```svelte
101
127
  <script lang="ts">
@@ -106,9 +132,23 @@ Use `useLingui()` for translations, `<T>` component for rich text:
106
132
  <h1>{t`Hello world`}</h1>
107
133
  ```
108
134
 
135
+ Use `<T>` component for rich text with embedded elements:
136
+
137
+ ```svelte
138
+ <T>Read the <a href="/docs">documentation</a></T>
139
+ ```
140
+
141
+ The extractor and Svelte preprocessor handle the transformation automatically.
142
+
109
143
  Run `{{pmRun}} i18n:extract` to extract messages into `.po` catalogs.
110
144
  <!-- {{/if:i18n}} -->
111
145
 
146
+ ### Deployment
147
+
148
+ - `Dockerfile` — multi-stage build for the Node adapter
149
+ - `docker-compose.yml` — local dev (PostgreSQL + zero-cache)
150
+ - `docker-compose.prod.yml` — production (PostgreSQL + zero-cache + app)
151
+
112
152
  ## Commands
113
153
 
114
154
  - `{{pmRun}} dev` — start dev server
@@ -121,3 +161,6 @@ Run `{{pmRun}} i18n:extract` to extract messages into `.po` catalogs.
121
161
  <!-- {{#if:i18n}} -->
122
162
  - `{{pmRun}} i18n:extract` — extract messages to .po catalogs
123
163
  <!-- {{/if:i18n}} -->
164
+ <!-- {{#if:testing}} -->
165
+ - `{{pmRun}} test` — run tests (requires Docker for testcontainers)
166
+ <!-- {{/if:testing}} -->
@@ -24,7 +24,12 @@ function createAuth() {
24
24
  await notif.send({
25
25
  type: "magic-link",
26
26
  recipientEmail: email,
27
+ // {{#if:i18n}}
28
+ data: { url, locale: getRequestEvent().cookies.get("locale") },
29
+ // {{/if:i18n}}
30
+ // {{#if:!i18n}}
27
31
  data: { url },
32
+ // {{/if:!i18n}}
28
33
  });
29
34
  },
30
35
  }),
@@ -1,5 +1,6 @@
1
1
  import { defineMutators, defineMutator } from "@rocicorp/zero";
2
2
  import * as v from "valibot";
3
+ import { zql } from "./schema";
3
4
 
4
5
  export const mutators = defineMutators({
5
6
  note: {
@@ -19,7 +20,9 @@ export const mutators = defineMutators({
19
20
  ),
20
21
  update: defineMutator(
21
22
  v.object({ id: v.string(), title: v.string(), content: v.string() }),
22
- async ({ tx, args }) => {
23
+ async ({ tx, ctx, args }) => {
24
+ const note = await tx.run(zql.note.where("id", args.id).one());
25
+ if (!note || note.userId !== ctx.userID) return;
23
26
  await tx.mutate.note.update({
24
27
  id: args.id,
25
28
  title: args.title,
@@ -28,7 +31,9 @@ export const mutators = defineMutators({
28
31
  });
29
32
  },
30
33
  ),
31
- delete: defineMutator(v.object({ id: v.string() }), async ({ tx, args }) => {
34
+ delete: defineMutator(v.object({ id: v.string() }), async ({ tx, ctx, args }) => {
35
+ const note = await tx.run(zql.note.where("id", args.id).one());
36
+ if (!note || note.userId !== ctx.userID) return;
32
37
  await tx.mutate.note.delete({ id: args.id });
33
38
  }),
34
39
  },
@@ -11,6 +11,10 @@ const config: LinguiConfig = {
11
11
  },
12
12
  ],
13
13
  extractors: [extractor],
14
+ format: "po",
15
+ formatOptions: {
16
+ lineNumbers: false,
17
+ },
14
18
  };
15
19
 
16
20
  export default config;
@@ -5,6 +5,7 @@
5
5
 
6
6
  function switchLocale(locale: string) {
7
7
  setLocale(locale);
8
+ document.cookie = `locale=${locale};path=/;max-age=31536000;samesite=lax`;
8
9
  currentLocale = locale;
9
10
  }
10
11
  </script>
@@ -1,4 +1,4 @@
1
- import { setupI18n } from "@lingui/core";
1
+ import { setupI18n, type I18n } from "@lingui/core";
2
2
  import { setI18n } from "@mikstack/svelte-lingui";
3
3
  import { messages as enMessages } from "../locales/en.po";
4
4
  import { messages as fiMessages } from "../locales/fi.po";
@@ -27,3 +27,16 @@ export function getLocale(): string {
27
27
  }
28
28
 
29
29
  export const locales = Object.keys(allMessages);
30
+
31
+ /**
32
+ * Create a standalone i18n instance for server-side use (e.g. emails).
33
+ * Each call returns a fresh instance — safe for concurrent requests.
34
+ */
35
+ export function createServerI18n(locale = "en"): I18n {
36
+ const serverI18n = setupI18n();
37
+ serverI18n.loadAndActivate({
38
+ locale,
39
+ messages: allMessages[locale] ?? allMessages["en"],
40
+ });
41
+ return serverI18n;
42
+ }
@@ -0,0 +1,67 @@
1
+ import { html, body, section, text, button, render } from "@mikstack/email";
2
+ import { msg } from "@mikstack/svelte-lingui";
3
+ import { createServerI18n } from "$lib/i18n";
4
+
5
+ export function magicLinkEmail(url: string, locale = "en") {
6
+ const i18n = createServerI18n(locale);
7
+ const _ = (d: { id: string; message: string }) => i18n._(d) as string;
8
+
9
+ const email = html(
10
+ body(
11
+ [
12
+ section(
13
+ [
14
+ text(_(msg`Sign in to {{projectName}}`), {
15
+ fontSize: 24,
16
+ fontWeight: "bold",
17
+ color: "#111827",
18
+ marginBottom: 40,
19
+ marginTop: 40,
20
+ }),
21
+ button(_(msg`Sign in`), {
22
+ href: url,
23
+ backgroundColor: "#111827",
24
+ color: "#ffffff",
25
+ fontSize: 14,
26
+ fontWeight: "bold",
27
+ padding: [12, 34],
28
+ borderRadius: 6,
29
+ marginBottom: 16,
30
+ }),
31
+ text(_(msg`Or, copy and paste this temporary login URL:`), {
32
+ fontSize: 14,
33
+ lineHeight: 1.7,
34
+ color: "#111827",
35
+ marginTop: 24,
36
+ marginBottom: 14,
37
+ }),
38
+ text(url, {
39
+ fontSize: 13,
40
+ color: "#111827",
41
+ backgroundColor: "#f4f4f4",
42
+ borderRadius: 6,
43
+ padding: [16, 24],
44
+ border: "1px solid #eee",
45
+ }),
46
+ text(_(msg`If you didn't try to sign in, you can safely ignore this email.`), {
47
+ fontSize: 14,
48
+ lineHeight: 1.7,
49
+ color: "#ababab",
50
+ marginTop: 14,
51
+ marginBottom: 38,
52
+ }),
53
+ ],
54
+ { padding: [0, 24] },
55
+ ),
56
+ ],
57
+ { maxWidth: 480, backgroundColor: "#ffffff" },
58
+ ),
59
+ { lang: locale },
60
+ );
61
+
62
+ return {
63
+ subject: _(msg`Sign in to {{projectName}}`),
64
+ html: render(email),
65
+ text: render(email, { plainText: true }),
66
+ };
67
+ }
@@ -0,0 +1,12 @@
1
+ import { defineNotification } from "@mikstack/notifications";
2
+ import { magicLinkEmail } from "../emails/magic-link";
3
+
4
+ export const notifications = {
5
+ "magic-link": defineNotification({
6
+ key: "magic-link",
7
+ critical: true, // Auth emails bypass user preferences
8
+ channels: {
9
+ email: (data: { url: string; locale?: string }) => magicLinkEmail(data.url, data.locale),
10
+ },
11
+ }),
12
+ } as const;
@@ -1,98 +1,87 @@
1
1
  msgid ""
2
2
  msgstr ""
3
+ "Project-Id-Version: {{projectName}}\n"
3
4
  "Language: en\n"
5
+ "Language-Team: English, United States\n"
6
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
4
7
  "MIME-Version: 1.0\n"
5
8
  "Content-Type: text/plain; charset=UTF-8\n"
6
9
  "Content-Transfer-Encoding: 8bit\n"
7
10
 
8
- #: src/routes/(app)/+page.svelte
9
11
  msgid "Cancel"
10
12
  msgstr "Cancel"
11
13
 
12
- #: src/routes/(public)/sign-in/+page.svelte
13
14
  msgid "Check your email for a magic link to sign in."
14
15
  msgstr "Check your email for a magic link to sign in."
15
16
 
16
- #: src/routes/(app)/+page.svelte
17
17
  msgid "Content"
18
18
  msgstr "Content"
19
19
 
20
- #: src/routes/(app)/+page.svelte
21
20
  msgid "Create note"
22
21
  msgstr "Create note"
23
22
 
24
- #: src/routes/(app)/+page.svelte
25
23
  msgid "Creating..."
26
24
  msgstr "Creating..."
27
25
 
28
- #: src/routes/(app)/+page.svelte
29
26
  msgid "Delete"
30
27
  msgstr "Delete"
31
28
 
32
- #: src/routes/(app)/+page.svelte
33
29
  msgid "Edit"
34
30
  msgstr "Edit"
35
31
 
36
- #: src/routes/(public)/sign-in/+page.svelte
32
+ msgid "If you didn't try to sign in, you can safely ignore this email."
33
+ msgstr "If you didn't try to sign in, you can safely ignore this email."
34
+
37
35
  msgid "Email"
38
36
  msgstr "Email"
39
37
 
40
- #: src/routes/(app)/+page.svelte
41
38
  msgid "New note"
42
39
  msgstr "New note"
43
40
 
44
- #: src/routes/(app)/+page.svelte
45
41
  msgid "No notes yet. Create one above!"
46
42
  msgstr "No notes yet. Create one above!"
47
43
 
48
- #: src/routes/(app)/+page.svelte
49
44
  msgid "Note title"
50
45
  msgstr "Note title"
51
46
 
52
- #: src/routes/(app)/+page.svelte
53
47
  msgid "Notes"
54
48
  msgstr "Notes"
55
49
 
56
- #: src/routes/(public)/sign-in/+page.svelte
50
+ msgid "Or, copy and paste this temporary login URL:"
51
+ msgstr "Or, copy and paste this temporary login URL:"
52
+
57
53
  msgid "Please enter a valid email address"
58
54
  msgstr "Please enter a valid email address"
59
55
 
60
- #: src/routes/(app)/+page.svelte
61
56
  msgid "Save"
62
57
  msgstr "Save"
63
58
 
64
- #: src/routes/(app)/+page.svelte
65
59
  msgid "Saving..."
66
60
  msgstr "Saving..."
67
61
 
68
- #: src/routes/(public)/sign-in/+page.svelte
69
62
  msgid "Sending magic link..."
70
63
  msgstr "Sending magic link..."
71
64
 
72
- #: src/routes/(app)/+page.svelte
73
- msgid "Sign out"
74
- msgstr "Sign out"
65
+ msgid "Sign in"
66
+ msgstr "Sign in"
75
67
 
76
- #: src/routes/(public)/sign-in/+page.svelte
77
68
  msgid "Sign in to {{projectName}}"
78
69
  msgstr "Sign in to {{projectName}}"
79
70
 
80
- #: src/routes/(public)/sign-in/+page.svelte
81
71
  msgid "Sign in with magic link"
82
72
  msgstr "Sign in with magic link"
83
73
 
84
- #: src/routes/(app)/+page.svelte
74
+ msgid "Sign out"
75
+ msgstr "Sign out"
76
+
85
77
  msgid "Title"
86
78
  msgstr "Title"
87
79
 
88
- #: src/routes/(app)/+page.svelte
89
80
  msgid "Title is required"
90
81
  msgstr "Title is required"
91
82
 
92
- #: src/routes/(app)/+page.svelte
93
83
  msgid "Write something..."
94
84
  msgstr "Write something..."
95
85
 
96
- #: src/routes/(app)/+page.svelte
97
86
  msgid "Your notes"
98
87
  msgstr "Your notes"
@@ -1,98 +1,87 @@
1
1
  msgid ""
2
2
  msgstr ""
3
+ "Project-Id-Version: {{projectName}}\n"
3
4
  "Language: fi\n"
5
+ "Language-Team: Finnish\n"
6
+ "Plural-Forms: nplurals=2; plural=(n != 1);\n"
4
7
  "MIME-Version: 1.0\n"
5
8
  "Content-Type: text/plain; charset=UTF-8\n"
6
9
  "Content-Transfer-Encoding: 8bit\n"
7
10
 
8
- #: src/routes/(app)/+page.svelte
9
11
  msgid "Cancel"
10
12
  msgstr "Peruuta"
11
13
 
12
- #: src/routes/(public)/sign-in/+page.svelte
13
14
  msgid "Check your email for a magic link to sign in."
14
15
  msgstr "Tarkista sähköpostisi kirjautumislinkin varalta."
15
16
 
16
- #: src/routes/(app)/+page.svelte
17
17
  msgid "Content"
18
18
  msgstr "Sisältö"
19
19
 
20
- #: src/routes/(app)/+page.svelte
21
20
  msgid "Create note"
22
21
  msgstr "Luo muistiinpano"
23
22
 
24
- #: src/routes/(app)/+page.svelte
25
23
  msgid "Creating..."
26
24
  msgstr "Luodaan..."
27
25
 
28
- #: src/routes/(app)/+page.svelte
29
26
  msgid "Delete"
30
27
  msgstr "Poista"
31
28
 
32
- #: src/routes/(app)/+page.svelte
33
29
  msgid "Edit"
34
30
  msgstr "Muokkaa"
35
31
 
36
- #: src/routes/(public)/sign-in/+page.svelte
32
+ msgid "If you didn't try to sign in, you can safely ignore this email."
33
+ msgstr "Jos et yrittänyt kirjautua, voit jättää tämän sähköpostin huomiotta."
34
+
37
35
  msgid "Email"
38
36
  msgstr "Sähköposti"
39
37
 
40
- #: src/routes/(app)/+page.svelte
41
38
  msgid "New note"
42
39
  msgstr "Uusi muistiinpano"
43
40
 
44
- #: src/routes/(app)/+page.svelte
45
41
  msgid "No notes yet. Create one above!"
46
42
  msgstr "Ei vielä muistiinpanoja. Luo ensimmäinen yllä!"
47
43
 
48
- #: src/routes/(app)/+page.svelte
49
44
  msgid "Note title"
50
45
  msgstr "Muistiinpanon otsikko"
51
46
 
52
- #: src/routes/(app)/+page.svelte
53
47
  msgid "Notes"
54
48
  msgstr "Muistiinpanot"
55
49
 
56
- #: src/routes/(public)/sign-in/+page.svelte
50
+ msgid "Or, copy and paste this temporary login URL:"
51
+ msgstr "Tai kopioi ja liitä tämä väliaikainen kirjautumislinkki:"
52
+
57
53
  msgid "Please enter a valid email address"
58
54
  msgstr "Syötä kelvollinen sähköpostiosoite"
59
55
 
60
- #: src/routes/(app)/+page.svelte
61
56
  msgid "Save"
62
57
  msgstr "Tallenna"
63
58
 
64
- #: src/routes/(app)/+page.svelte
65
59
  msgid "Saving..."
66
60
  msgstr "Tallennetaan..."
67
61
 
68
- #: src/routes/(public)/sign-in/+page.svelte
69
62
  msgid "Sending magic link..."
70
63
  msgstr "Lähetetään kirjautumislinkkiä..."
71
64
 
72
- #: src/routes/(app)/+page.svelte
73
- msgid "Sign out"
74
- msgstr "Kirjaudu ulos"
65
+ msgid "Sign in"
66
+ msgstr "Kirjaudu"
75
67
 
76
- #: src/routes/(public)/sign-in/+page.svelte
77
68
  msgid "Sign in to {{projectName}}"
78
69
  msgstr "Kirjaudu palveluun {{projectName}}"
79
70
 
80
- #: src/routes/(public)/sign-in/+page.svelte
81
71
  msgid "Sign in with magic link"
82
72
  msgstr "Kirjaudu kirjautumislinkillä"
83
73
 
84
- #: src/routes/(app)/+page.svelte
74
+ msgid "Sign out"
75
+ msgstr "Kirjaudu ulos"
76
+
85
77
  msgid "Title"
86
78
  msgstr "Otsikko"
87
79
 
88
- #: src/routes/(app)/+page.svelte
89
80
  msgid "Title is required"
90
81
  msgstr "Otsikko on pakollinen"
91
82
 
92
- #: src/routes/(app)/+page.svelte
93
83
  msgid "Write something..."
94
84
  msgstr "Kirjoita jotain..."
95
85
 
96
- #: src/routes/(app)/+page.svelte
97
86
  msgid "Your notes"
98
87
  msgstr "Muistiinpanosi"