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 +7 -1
- package/package.json +1 -1
- package/templates/base/AGENTS.md +10 -6
- package/templates/base/README.md +5 -0
- package/templates/base/src/lib/zero/mutators.ts +10 -6
- package/templates/base/src/routes/(app)/+page.svelte +4 -2
- package/templates/testing/src/lib/server/db/schema.test.ts +39 -0
- package/templates/ui/.vscode/settings.json +3 -0
- package/templates/ui/package.json.partial +1 -0
- package/templates/ui/src/app.css +18 -18
- package/templates/ui/stylelint.config.js +1 -1
- package/templates/ui-vendor/src/lib/components/ui/Alert/Alert.svelte +4 -4
- package/templates/ui-vendor/src/lib/components/ui/Badge/Badge.svelte +1 -1
- package/templates/testing/src/example.test.ts +0 -7
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))
|
|
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
package/templates/base/AGENTS.md
CHANGED
|
@@ -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
|
|
35
|
+
Mutators verify ownership before update/delete by including `userId` in the query:
|
|
36
36
|
|
|
37
37
|
```typescript
|
|
38
|
-
const entity = await tx.run(
|
|
39
|
-
|
|
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
|
|
51
|
+
### Forms
|
|
50
52
|
|
|
51
|
-
Use `createForm` from `@mikstack/form` for client-side forms with Valibot validation.
|
|
52
|
-
|
|
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">
|
package/templates/base/README.md
CHANGED
|
@@ -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(
|
|
25
|
-
|
|
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(
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
});
|
package/templates/ui/src/app.css
CHANGED
|
@@ -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
|
|
69
|
-
--surface-2: oklch(97% 0
|
|
70
|
-
--surface-3: oklch(93% 0
|
|
71
|
-
--text-1: oklch(20% 0
|
|
72
|
-
--text-2: oklch(40% 0
|
|
73
|
-
--accent: oklch(55% 0.2
|
|
74
|
-
--danger: oklch(55% 0.2
|
|
75
|
-
--border: oklch(85% 0
|
|
76
|
-
--focus: oklch(55% 0.2
|
|
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
|
|
85
|
-
--surface-2: oklch(20% 0
|
|
86
|
-
--surface-3: oklch(25% 0
|
|
87
|
-
--text-1: oklch(93% 0
|
|
88
|
-
--text-2: oklch(70% 0
|
|
89
|
-
--accent: oklch(70% 0.18
|
|
90
|
-
--danger: oklch(70% 0.18
|
|
91
|
-
--border: oklch(30% 0
|
|
92
|
-
--focus: oklch(70% 0.18
|
|
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
|
|
40
|
-
border-color: oklch(55% 0.15
|
|
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
|
|
46
|
-
border-color: oklch(75% 0.15
|
|
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
|
|