create-mikstack 0.1.31 → 0.1.33

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/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import * as p from "@clack/prompts";
3
3
  import { execSync, spawn } from "node:child_process";
4
4
  import path from "node:path";
5
5
  import { parseArgs } from "node:util";
6
+ import crypto from "node:crypto";
6
7
  import fs from "node:fs";
7
8
 
8
9
  //#region src/config.ts
@@ -178,6 +179,7 @@ function scaffold(config, onStatus) {
178
179
  });
179
180
  fs.rmSync(path.join(target, "agents.md"), { force: true });
180
181
  fs.rmSync(path.join(target, ".npmrc"), { force: true });
182
+ fs.rmSync(path.join(target, "src", "lib", "index.ts"), { force: true });
181
183
  onStatus?.("Applying templates...");
182
184
  const overlay = (dir) => {
183
185
  copyDir(dir, target);
@@ -199,7 +201,11 @@ function scaffold(config, onStatus) {
199
201
  fs.symlinkSync("AGENTS.md", path.join(target, "CLAUDE.md"));
200
202
  const envExample = path.join(target, ".env.example");
201
203
  const envFile = path.join(target, ".env");
202
- if (fs.existsSync(envExample) && !fs.existsSync(envFile)) fs.copyFileSync(envExample, envFile);
204
+ if (fs.existsSync(envExample) && !fs.existsSync(envFile)) {
205
+ fs.copyFileSync(envExample, envFile);
206
+ const envContent = fs.readFileSync(envFile, "utf-8");
207
+ fs.writeFileSync(envFile, envContent.replace("BETTER_AUTH_SECRET=\"change-me-generate-a-real-secret\"", `BETTER_AUTH_SECRET="${crypto.randomBytes(32).toString("base64url")}"`));
208
+ }
203
209
  }
204
210
  function copyDir(src, dest) {
205
211
  if (!fs.existsSync(src)) return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-mikstack",
3
- "version": "0.1.31",
3
+ "version": "0.1.33",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -32,11 +32,13 @@ const data = $derived(query.data);
32
32
  await z.mutate(mutators.myMutation(args));
33
33
  ```
34
34
 
35
- Mutators verify ownership before update/delete via `tx.run()`:
35
+ Mutators verify ownership before update/delete by including `userId` in the query:
36
36
 
37
37
  ```typescript
38
- const entity = await tx.run(zql.note.where("id", args.id).one());
39
- if (!entity || entity.userId !== ctx.userID) return;
38
+ const entity = await tx.run(
39
+ zql.note.where("id", args.id).where("userId", ctx.userID).one(),
40
+ );
41
+ if (!entity) return;
40
42
  ```
41
43
 
42
44
  ### Auth
@@ -46,10 +48,12 @@ Client: `src/lib/auth-client.ts` (magic link + client helpers)
46
48
  Session is available in `locals.user` and `locals.session` via `src/hooks.server.ts`.
47
49
  Routes under `(app)/` require authentication — unauthenticated requests redirect to `/sign-in`.
48
50
 
49
- ### Forms (@mikstack/form)
51
+ ### Forms
50
52
 
51
- Use `createForm` from `@mikstack/form` for client-side forms with Valibot validation.
52
- Ideal for Zero mutators where forms don't use SvelteKit form actions.
53
+ **Zero mutations:** Use `createForm` from `@mikstack/form` for client-side forms with Valibot validation.
54
+
55
+ **Server submissions:** Use SvelteKit remote functions (`*.remote.ts` files) with the `form()` helper and Valibot validation.
56
+ For docs, call the Svelte MCP tool: `list-sections`, then `get-documentation` for the "Remote functions" section.
53
57
 
54
58
  ```svelte
55
59
  <script lang="ts">
@@ -2,6 +2,11 @@
2
2
 
3
3
  Built with [mikstack](https://github.com/mikaelsiidorow/mikstack).
4
4
 
5
+ ## Prerequisites
6
+
7
+ - [Docker](https://docs.docker.com/get-docker/) (for local Postgres)
8
+ - Node.js 22+
9
+
5
10
  ## Getting Started
6
11
 
7
12
  ```bash
@@ -5,7 +5,7 @@ import { zql } from "./schema";
5
5
  export const mutators = defineMutators({
6
6
  note: {
7
7
  create: defineMutator(
8
- v.object({ id: v.string(), title: v.string(), content: v.string() }),
8
+ v.object({ id: v.string(), title: v.pipe(v.string(), v.minLength(1)), content: v.string() }),
9
9
  async ({ tx, ctx, args }) => {
10
10
  const now = Date.now();
11
11
  await tx.mutate.note.insert({
@@ -19,10 +19,12 @@ export const mutators = defineMutators({
19
19
  },
20
20
  ),
21
21
  update: defineMutator(
22
- v.object({ id: v.string(), title: v.string(), content: v.string() }),
22
+ v.object({ id: v.string(), title: v.pipe(v.string(), v.minLength(1)), content: v.string() }),
23
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;
24
+ const note = await tx.run(
25
+ zql.note.where("id", args.id).where("userId", ctx.userID).one(),
26
+ );
27
+ if (!note) return;
26
28
  await tx.mutate.note.update({
27
29
  id: args.id,
28
30
  title: args.title,
@@ -32,8 +34,10 @@ export const mutators = defineMutators({
32
34
  },
33
35
  ),
34
36
  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;
37
+ const note = await tx.run(
38
+ zql.note.where("id", args.id).where("userId", ctx.userID).one(),
39
+ );
40
+ if (!note) return;
37
41
  await tx.mutate.note.delete({ id: args.id });
38
42
  }),
39
43
  },
@@ -146,7 +146,8 @@
146
146
  <!-- {{#if:!i18n}} -->
147
147
  <h2>New note</h2>
148
148
  <!-- {{/if:!i18n}} -->
149
- <form id={createNoteForm.id} onsubmit={createNoteForm.onsubmit} onkeydown={submitOnModEnter} class="note-form" role="form">
149
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
150
+ <form id={createNoteForm.id} onsubmit={createNoteForm.onsubmit} onkeydown={submitOnModEnter} class="note-form">
150
151
  <FormField for={createNoteForm.fields.title.as("text").id}>
151
152
  {#snippet label(attrs)}
152
153
  <!-- {{#if:i18n}} -->
@@ -238,7 +239,8 @@
238
239
  {#each notes as note (note.id)}
239
240
  <li class="note-card">
240
241
  {#if editingId === note.id}
241
- <form id={editForm.id} onsubmit={editForm.onsubmit} onkeydown={submitOnModEnter} class="note-form" role="form">
242
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
243
+ <form id={editForm.id} onsubmit={editForm.onsubmit} onkeydown={submitOnModEnter} class="note-form">
242
244
  <FormField for={editForm.fields.title.as("text").id}>
243
245
  {#snippet label(attrs)}
244
246
  <!-- {{#if:i18n}} -->
@@ -0,0 +1,39 @@
1
+ import { pushSchema } from "drizzle-kit/api";
2
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
3
+ import { createTestDatabase, stopTestDatabase, type TestDatabase } from "./test-utils";
4
+ import * as schema from "./schema";
5
+
6
+ describe("database schema", () => {
7
+ let testDb: TestDatabase;
8
+
9
+ beforeAll(async () => {
10
+ testDb = await createTestDatabase();
11
+ }, 60_000);
12
+
13
+ afterAll(async () => {
14
+ if (testDb) await stopTestDatabase(testDb);
15
+ });
16
+
17
+ it("pushes schema without data loss", async () => {
18
+ const result = await pushSchema(schema, testDb.db);
19
+ expect(result.warnings).toEqual([]);
20
+ expect(result.hasDataLoss).toBe(false);
21
+ await result.apply();
22
+
23
+ const tables = await testDb.client`
24
+ SELECT table_name FROM information_schema.tables
25
+ WHERE table_schema = 'public'
26
+ ORDER BY table_name
27
+ `;
28
+ const tableNames = tables.map((row) => row.table_name);
29
+
30
+ expect(tableNames).toContain("user");
31
+ expect(tableNames).toContain("session");
32
+ expect(tableNames).toContain("account");
33
+ expect(tableNames).toContain("verification");
34
+ expect(tableNames).toContain("note");
35
+ expect(tableNames).toContain("notification_delivery");
36
+ expect(tableNames).toContain("in_app_notification");
37
+ expect(tableNames).toContain("notification_preference");
38
+ });
39
+ });
@@ -0,0 +1,3 @@
1
+ {
2
+ "stylelint.validate": ["css", "svelte"]
3
+ }
@@ -8,6 +8,7 @@
8
8
  },
9
9
  "devDependencies": {
10
10
  "stylelint": "^17.1.1",
11
+ "stylelint-config-html": "^1.1.0",
11
12
  "stylelint-config-standard": "^40.0.0"
12
13
  }
13
14
  }
@@ -65,15 +65,15 @@ select {
65
65
  --radius-lg: 0.75rem;
66
66
 
67
67
  /* Light Mode Colors (oklch) */
68
- --surface-1: oklch(100% 0 0);
69
- --surface-2: oklch(97% 0 0);
70
- --surface-3: oklch(93% 0 0);
71
- --text-1: oklch(20% 0 0);
72
- --text-2: oklch(40% 0 0);
73
- --accent: oklch(55% 0.2 260);
74
- --danger: oklch(55% 0.2 25);
75
- --border: oklch(85% 0 0);
76
- --focus: oklch(55% 0.2 260 / 50%);
68
+ --surface-1: oklch(100% 0 0deg);
69
+ --surface-2: oklch(97% 0 0deg);
70
+ --surface-3: oklch(93% 0 0deg);
71
+ --text-1: oklch(20% 0 0deg);
72
+ --text-2: oklch(40% 0 0deg);
73
+ --accent: oklch(55% 0.2 260deg);
74
+ --danger: oklch(55% 0.2 25deg);
75
+ --border: oklch(85% 0 0deg);
76
+ --focus: oklch(55% 0.2 260deg / 50%);
77
77
 
78
78
  color: var(--text-1);
79
79
  background-color: var(--surface-1);
@@ -81,14 +81,14 @@ select {
81
81
 
82
82
  @media (prefers-color-scheme: dark) {
83
83
  :root {
84
- --surface-1: oklch(15% 0 0);
85
- --surface-2: oklch(20% 0 0);
86
- --surface-3: oklch(25% 0 0);
87
- --text-1: oklch(93% 0 0);
88
- --text-2: oklch(70% 0 0);
89
- --accent: oklch(70% 0.18 260);
90
- --danger: oklch(70% 0.18 25);
91
- --border: oklch(30% 0 0);
92
- --focus: oklch(70% 0.18 260 / 50%);
84
+ --surface-1: oklch(15% 0 0deg);
85
+ --surface-2: oklch(20% 0 0deg);
86
+ --surface-3: oklch(25% 0 0deg);
87
+ --text-1: oklch(93% 0 0deg);
88
+ --text-2: oklch(70% 0 0deg);
89
+ --accent: oklch(70% 0.18 260deg);
90
+ --danger: oklch(70% 0.18 25deg);
91
+ --border: oklch(30% 0 0deg);
92
+ --focus: oklch(70% 0.18 260deg / 50%);
93
93
  }
94
94
  }
@@ -1,6 +1,6 @@
1
1
  /** @type {import('stylelint').Config} */
2
2
  export default {
3
- extends: ["stylelint-config-standard"],
3
+ extends: ["stylelint-config-standard", "stylelint-config-html/svelte"],
4
4
  rules: {
5
5
  "selector-pseudo-class-no-unknown": [true, { ignorePseudoClasses: ["global"] }],
6
6
  },
@@ -36,14 +36,14 @@
36
36
  }
37
37
 
38
38
  &[data-variant='success'] {
39
- background-color: oklch(55% 0.15 145 / 10%);
40
- border-color: oklch(55% 0.15 145 / 30%);
39
+ background-color: oklch(55% 0.15 145deg / 10%);
40
+ border-color: oklch(55% 0.15 145deg / 30%);
41
41
  color: var(--text-1);
42
42
  }
43
43
 
44
44
  &[data-variant='warning'] {
45
- background-color: oklch(75% 0.15 85 / 10%);
46
- border-color: oklch(75% 0.15 85 / 30%);
45
+ background-color: oklch(75% 0.15 85deg / 10%);
46
+ border-color: oklch(75% 0.15 85deg / 30%);
47
47
  color: var(--text-1);
48
48
  }
49
49
 
@@ -36,7 +36,7 @@
36
36
  }
37
37
 
38
38
  &[data-variant='success'] {
39
- background-color: oklch(55% 0.15 145);
39
+ background-color: oklch(55% 0.15 145deg);
40
40
  color: white;
41
41
  }
42
42
 
@@ -1,7 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- describe("example", () => {
4
- it("should work", () => {
5
- expect(1 + 1).toBe(2);
6
- });
7
- });