create-mikstack 0.1.0
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/README.md +54 -0
- package/dist/index.js +410 -0
- package/package.json +43 -0
- package/templates/adapters/cloudflare/package.json.partial +5 -0
- package/templates/adapters/cloudflare/svelte.config.js +19 -0
- package/templates/adapters/node/Dockerfile +30 -0
- package/templates/adapters/node/docker-compose.prod.yml +27 -0
- package/templates/adapters/node/package.json.partial +5 -0
- package/templates/adapters/node/svelte.config.js +19 -0
- package/templates/adapters/vercel/package.json.partial +5 -0
- package/templates/adapters/vercel/svelte.config.js +19 -0
- package/templates/base/.env.example +23 -0
- package/templates/base/.gitignore.append +2 -0
- package/templates/base/.mcp.json +9 -0
- package/templates/base/.prettierignore +10 -0
- package/templates/base/.vscode/extensions.json +3 -0
- package/templates/base/AGENTS.md +123 -0
- package/templates/base/README.md +27 -0
- package/templates/base/agents.md +28 -0
- package/templates/base/docker-compose.yml +15 -0
- package/templates/base/drizzle-zero.config.ts +17 -0
- package/templates/base/drizzle.config.ts +17 -0
- package/templates/base/eslint.config.ts +65 -0
- package/templates/base/package.json.partial +43 -0
- package/templates/base/prettier.config.js +6 -0
- package/templates/base/src/app.d.ts +12 -0
- package/templates/base/src/app.html +11 -0
- package/templates/base/src/hooks.server.ts +15 -0
- package/templates/base/src/lib/auth-client.ts +6 -0
- package/templates/base/src/lib/server/auth.ts +52 -0
- package/templates/base/src/lib/server/db/index.ts +19 -0
- package/templates/base/src/lib/server/db/schema.ts +117 -0
- package/templates/base/src/lib/server/db/seed.ts +21 -0
- package/templates/base/src/lib/server/emails/magic-link.ts +77 -0
- package/templates/base/src/lib/server/emails/send.ts +55 -0
- package/templates/base/src/lib/server/notifications/definitions.ts +12 -0
- package/templates/base/src/lib/server/notifications.ts +38 -0
- package/templates/base/src/lib/z.svelte.ts +14 -0
- package/templates/base/src/lib/zero/context.ts +9 -0
- package/templates/base/src/lib/zero/db-provider.server.ts +11 -0
- package/templates/base/src/lib/zero/mutators.ts +35 -0
- package/templates/base/src/lib/zero/queries.ts +21 -0
- package/templates/base/src/lib/zero/schema.ts +1 -0
- package/templates/base/src/routes/+layout.server.ts +5 -0
- package/templates/base/src/routes/+layout.svelte +7 -0
- package/templates/base/src/routes/+page.server.ts +7 -0
- package/templates/base/src/routes/+page.svelte +319 -0
- package/templates/base/src/routes/api/dev/emails/+server.ts +89 -0
- package/templates/base/src/routes/api/dev/emails/[id]/+server.ts +24 -0
- package/templates/base/src/routes/api/notifications/[...path]/+server.ts +10 -0
- package/templates/base/src/routes/api/zero/get-queries/+server.ts +29 -0
- package/templates/base/src/routes/api/zero/mutate/+server.ts +31 -0
- package/templates/base/src/routes/sign-in/+page.svelte +97 -0
- package/templates/base/tsconfig.json +40 -0
- package/templates/github-actions-bun/.github/workflows/ci.yml +22 -0
- package/templates/github-actions-npm/.github/workflows/ci.yml +25 -0
- package/templates/github-actions-pnpm/.github/workflows/ci.yml +27 -0
- package/templates/i18n/lingui.config.ts +16 -0
- package/templates/i18n/package.json.partial +14 -0
- package/templates/i18n/src/lib/i18n.ts +10 -0
- package/templates/i18n/src/locales/en.po +6 -0
- package/templates/i18n/src/po.d.ts +3 -0
- package/templates/i18n/vite.config.ts +7 -0
- package/templates/supply-chain-bun/bunfig.toml +3 -0
- package/templates/testing/package.json.partial +11 -0
- package/templates/testing/src/example.test.ts +7 -0
- package/templates/testing/src/lib/server/db/test-utils.ts +25 -0
- package/templates/testing/vitest.config.ts +9 -0
- package/templates/ui/.vscode/extensions.json +8 -0
- package/templates/ui/package.json.partial +13 -0
- package/templates/ui/src/app.css +94 -0
- package/templates/ui/src/routes/+layout.svelte +12 -0
- package/templates/ui/stylelint.config.js +7 -0
- package/templates/ui/vite.config.ts +6 -0
- package/templates/ui-dependency/package.json.partial +5 -0
- package/templates/ui-vendor/src/lib/components/ui/Accordion/Accordion.svelte +71 -0
- package/templates/ui-vendor/src/lib/components/ui/Accordion/index.ts +1 -0
- package/templates/ui-vendor/src/lib/components/ui/Alert/Alert.svelte +60 -0
- package/templates/ui-vendor/src/lib/components/ui/Alert/index.ts +1 -0
- package/templates/ui-vendor/src/lib/components/ui/Badge/Badge.svelte +48 -0
- package/templates/ui-vendor/src/lib/components/ui/Badge/index.ts +1 -0
- package/templates/ui-vendor/src/lib/components/ui/Button/Button.svelte +77 -0
- package/templates/ui-vendor/src/lib/components/ui/Button/index.ts +1 -0
- package/templates/ui-vendor/src/lib/components/ui/Card/Card.svelte +49 -0
- package/templates/ui-vendor/src/lib/components/ui/Card/index.ts +1 -0
- package/templates/ui-vendor/src/lib/components/ui/Dialog/Dialog.svelte +70 -0
- package/templates/ui-vendor/src/lib/components/ui/Dialog/index.ts +1 -0
- package/templates/ui-vendor/src/lib/components/ui/FormField/FormField.svelte +53 -0
- package/templates/ui-vendor/src/lib/components/ui/FormField/index.ts +1 -0
- package/templates/ui-vendor/src/lib/components/ui/Input/Input.svelte +27 -0
- package/templates/ui-vendor/src/lib/components/ui/Input/index.ts +1 -0
- package/templates/ui-vendor/src/lib/components/ui/Separator/Separator.svelte +26 -0
- package/templates/ui-vendor/src/lib/components/ui/Separator/index.ts +1 -0
- package/templates/ui-vendor/src/lib/components/ui/Skeleton/Skeleton.svelte +40 -0
- package/templates/ui-vendor/src/lib/components/ui/Skeleton/index.ts +1 -0
- package/templates/ui-vendor/src/lib/components/ui/Switch/Switch.svelte +86 -0
- package/templates/ui-vendor/src/lib/components/ui/Switch/index.ts +1 -0
- package/templates/ui-vendor/src/lib/components/ui/Textarea/Textarea.svelte +29 -0
- package/templates/ui-vendor/src/lib/components/ui/Textarea/index.ts +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# create-mikstack
|
|
2
|
+
|
|
3
|
+
Scaffold an opinionated SvelteKit project.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
bun create mikstack@latest
|
|
7
|
+
pnpm create mikstack@latest
|
|
8
|
+
npm create mikstack@latest
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## What you get
|
|
12
|
+
|
|
13
|
+
**Always included:**
|
|
14
|
+
|
|
15
|
+
- SvelteKit with TypeScript
|
|
16
|
+
- PostgreSQL + Drizzle ORM (with Docker Compose)
|
|
17
|
+
- better-auth with magic link (dev mode logs links to console)
|
|
18
|
+
- ESLint (type-aware, flat config) + Prettier
|
|
19
|
+
- `.env.example` with all keys stubbed
|
|
20
|
+
|
|
21
|
+
**Configurable:**
|
|
22
|
+
|
|
23
|
+
| Feature | Default | Options |
|
|
24
|
+
| ----------------- | ------------ | ------------------------------------ |
|
|
25
|
+
| UI | Tailwind CSS | Tailwind / vanilla CSS (+ Stylelint) |
|
|
26
|
+
| Testing | on | Vitest + testcontainers |
|
|
27
|
+
| GitHub Actions CI | on | lint, format, typecheck, build |
|
|
28
|
+
| SvelteKit adapter | node | node / vercel / cloudflare |
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
### Interactive
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
bun create mikstack@latest my-app
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Prompts you to choose between recommended defaults or customize each option.
|
|
39
|
+
|
|
40
|
+
### Non-interactive
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
bun create mikstack@latest my-app --yes
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Scaffolds with all recommended defaults. Also activates when `CI=true`.
|
|
47
|
+
|
|
48
|
+
## Package manager detection
|
|
49
|
+
|
|
50
|
+
Automatically detected from `npm_config_user_agent`. Affects:
|
|
51
|
+
|
|
52
|
+
- `package.json` scripts
|
|
53
|
+
- `README.md` commands
|
|
54
|
+
- GitHub Actions CI workflow (separate template per PM)
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import * as p from "@clack/prompts";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
|
|
8
|
+
//#region src/config.ts
|
|
9
|
+
const defaults = {
|
|
10
|
+
mode: "recommended",
|
|
11
|
+
uiMode: "dependency",
|
|
12
|
+
i18n: true,
|
|
13
|
+
testing: true,
|
|
14
|
+
githubActions: true,
|
|
15
|
+
adapter: "node"
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/pm.ts
|
|
20
|
+
function detectPackageManager() {
|
|
21
|
+
const ua = process.env.npm_config_user_agent;
|
|
22
|
+
if (!ua) return "npm";
|
|
23
|
+
if (ua.startsWith("pnpm/")) return "pnpm";
|
|
24
|
+
if (ua.startsWith("bun/")) return "bun";
|
|
25
|
+
return "npm";
|
|
26
|
+
}
|
|
27
|
+
function pmInstall(pm) {
|
|
28
|
+
return pm === "npm" ? "npm install" : `${pm} install`;
|
|
29
|
+
}
|
|
30
|
+
function pmRun(pm) {
|
|
31
|
+
return pm === "npm" ? "npm run" : `${pm} run`;
|
|
32
|
+
}
|
|
33
|
+
function pmExec(pm) {
|
|
34
|
+
if (pm === "npm") return "npx";
|
|
35
|
+
if (pm === "pnpm") return "pnpm exec";
|
|
36
|
+
return "bunx";
|
|
37
|
+
}
|
|
38
|
+
function pmLockfile(pm) {
|
|
39
|
+
if (pm === "npm") return "package-lock.json";
|
|
40
|
+
if (pm === "pnpm") return "pnpm-lock.yaml";
|
|
41
|
+
return "bun.lock";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region src/post-scaffold.ts
|
|
46
|
+
function postScaffold(targetDir, packageManager) {
|
|
47
|
+
const cwd = path.resolve(process.cwd(), targetDir);
|
|
48
|
+
const run = (cmd) => execSync(cmd, {
|
|
49
|
+
cwd,
|
|
50
|
+
stdio: "pipe"
|
|
51
|
+
});
|
|
52
|
+
const s = p.spinner();
|
|
53
|
+
s.start("Installing dependencies...");
|
|
54
|
+
try {
|
|
55
|
+
run(`${packageManager} install`);
|
|
56
|
+
s.stop("Dependencies installed.");
|
|
57
|
+
} catch {
|
|
58
|
+
s.stop("Failed to install dependencies. You can run install manually.");
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
run(`${packageManager} run format`);
|
|
62
|
+
} catch {}
|
|
63
|
+
s.start("Initializing git repository...");
|
|
64
|
+
try {
|
|
65
|
+
run("git init");
|
|
66
|
+
run("git add .");
|
|
67
|
+
run("git commit -m \"the future is now\"");
|
|
68
|
+
s.stop("Git repository initialized.");
|
|
69
|
+
} catch {
|
|
70
|
+
s.stop("Failed to initialize git. You can run git init manually.");
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
//#endregion
|
|
75
|
+
//#region src/prompts.ts
|
|
76
|
+
function isCancel(value) {
|
|
77
|
+
return p.isCancel(value);
|
|
78
|
+
}
|
|
79
|
+
function onCancel() {
|
|
80
|
+
p.cancel("Operation cancelled.");
|
|
81
|
+
process.exit(0);
|
|
82
|
+
}
|
|
83
|
+
async function runPrompts(projectName, packageManager) {
|
|
84
|
+
p.intro("create-mikstack");
|
|
85
|
+
if (!projectName) {
|
|
86
|
+
const name = await p.text({
|
|
87
|
+
message: "Project name:",
|
|
88
|
+
placeholder: "my-app",
|
|
89
|
+
validate(value) {
|
|
90
|
+
if (!value.trim()) return "Project name is required.";
|
|
91
|
+
if (!/^[a-z0-9][a-z0-9._-]*$/.test(value)) return "Invalid package name. Use lowercase, numbers, hyphens, dots, and underscores.";
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
if (isCancel(name)) onCancel();
|
|
95
|
+
projectName = name;
|
|
96
|
+
}
|
|
97
|
+
const targetDir = projectName;
|
|
98
|
+
const mode = await p.select({
|
|
99
|
+
message: "Setup mode:",
|
|
100
|
+
options: [{
|
|
101
|
+
value: "recommended",
|
|
102
|
+
label: "Recommended (full stack)"
|
|
103
|
+
}, {
|
|
104
|
+
value: "customize",
|
|
105
|
+
label: "Customize"
|
|
106
|
+
}]
|
|
107
|
+
});
|
|
108
|
+
if (isCancel(mode)) onCancel();
|
|
109
|
+
if (mode === "recommended") {
|
|
110
|
+
p.outro(`Scaffolding ${projectName} with recommended defaults...`);
|
|
111
|
+
return {
|
|
112
|
+
projectName,
|
|
113
|
+
targetDir,
|
|
114
|
+
packageManager,
|
|
115
|
+
...defaults
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const uiMode = await p.select({
|
|
119
|
+
message: "UI components (@mikstack/ui):",
|
|
120
|
+
options: [{
|
|
121
|
+
value: "dependency",
|
|
122
|
+
label: "Dependency (import from package)"
|
|
123
|
+
}, {
|
|
124
|
+
value: "vendor",
|
|
125
|
+
label: "Vendor (copy into project)"
|
|
126
|
+
}]
|
|
127
|
+
});
|
|
128
|
+
if (isCancel(uiMode)) onCancel();
|
|
129
|
+
const i18n = await p.confirm({
|
|
130
|
+
message: "Include i18n? (@mikstack/svelte-lingui)",
|
|
131
|
+
initialValue: true
|
|
132
|
+
});
|
|
133
|
+
if (isCancel(i18n)) onCancel();
|
|
134
|
+
const testing = await p.confirm({
|
|
135
|
+
message: "Include testing setup? (Vitest + testcontainers)",
|
|
136
|
+
initialValue: true
|
|
137
|
+
});
|
|
138
|
+
if (isCancel(testing)) onCancel();
|
|
139
|
+
const githubActions = await p.confirm({
|
|
140
|
+
message: "Include GitHub Actions CI?",
|
|
141
|
+
initialValue: true
|
|
142
|
+
});
|
|
143
|
+
if (isCancel(githubActions)) onCancel();
|
|
144
|
+
const adapter = await p.select({
|
|
145
|
+
message: "SvelteKit adapter:",
|
|
146
|
+
options: [
|
|
147
|
+
{
|
|
148
|
+
value: "node",
|
|
149
|
+
label: "Node"
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
value: "vercel",
|
|
153
|
+
label: "Vercel"
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
value: "cloudflare",
|
|
157
|
+
label: "Cloudflare"
|
|
158
|
+
}
|
|
159
|
+
]
|
|
160
|
+
});
|
|
161
|
+
if (isCancel(adapter)) onCancel();
|
|
162
|
+
p.outro(`Scaffolding ${projectName}...`);
|
|
163
|
+
return {
|
|
164
|
+
projectName,
|
|
165
|
+
targetDir,
|
|
166
|
+
packageManager,
|
|
167
|
+
mode,
|
|
168
|
+
uiMode,
|
|
169
|
+
i18n,
|
|
170
|
+
testing,
|
|
171
|
+
githubActions,
|
|
172
|
+
adapter
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
//#endregion
|
|
177
|
+
//#region src/template-engine.ts
|
|
178
|
+
const CONDITIONAL_BLOCK_RE = /^[^\S\n]*(?:\/\/|#|<!--)[^\S\n]*\{\{#if:(\w+)\}\}[^\S\n]*(?:-->)?[^\S\n]*\n([\s\S]*?)^[^\S\n]*(?:\/\/|#|<!--)[^\S\n]*\{\{\/if:\1\}\}[^\S\n]*(?:-->)?[^\S\n]*\n?/gm;
|
|
179
|
+
const VARIABLE_RE = /\{\{(\w+)\}\}/g;
|
|
180
|
+
function renderTemplate(content, context) {
|
|
181
|
+
let result = content.replace(CONDITIONAL_BLOCK_RE, (_match, name, body) => {
|
|
182
|
+
if (context[name]) return body;
|
|
183
|
+
return "";
|
|
184
|
+
});
|
|
185
|
+
result = result.replace(VARIABLE_RE, (_match, name) => {
|
|
186
|
+
const value = context[name];
|
|
187
|
+
if (value === void 0) throw new Error(`Undefined template variable: {{${name}}}`);
|
|
188
|
+
return String(value);
|
|
189
|
+
});
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
//#endregion
|
|
194
|
+
//#region src/scaffold.ts
|
|
195
|
+
const TEMPLATES_DIR = path.resolve(import.meta.dirname, "..", "templates");
|
|
196
|
+
function scaffold(config) {
|
|
197
|
+
const target = path.resolve(process.cwd(), config.targetDir);
|
|
198
|
+
if (fs.existsSync(target)) {
|
|
199
|
+
if (fs.readdirSync(target).length > 0) throw new Error(`Target directory "${config.targetDir}" is not empty. Please choose a different name or remove the directory.`);
|
|
200
|
+
}
|
|
201
|
+
execSync(`${path.resolve(import.meta.dirname, "..", "node_modules", ".bin", "sv")} create ${target} --template minimal --types ts --add devtools-json --no-install`, { stdio: "pipe" });
|
|
202
|
+
const overlay = (dir) => {
|
|
203
|
+
copyDir(dir, target);
|
|
204
|
+
mergePartials(target);
|
|
205
|
+
};
|
|
206
|
+
overlay(path.join(TEMPLATES_DIR, "base"));
|
|
207
|
+
if (config.testing) overlay(path.join(TEMPLATES_DIR, "testing"));
|
|
208
|
+
overlay(path.join(TEMPLATES_DIR, "ui"));
|
|
209
|
+
if (config.i18n) overlay(path.join(TEMPLATES_DIR, "i18n"));
|
|
210
|
+
if (config.uiMode === "vendor") overlay(path.join(TEMPLATES_DIR, "ui-vendor"));
|
|
211
|
+
else overlay(path.join(TEMPLATES_DIR, "ui-dependency"));
|
|
212
|
+
if (config.githubActions) overlay(path.join(TEMPLATES_DIR, `github-actions-${config.packageManager}`));
|
|
213
|
+
overlay(path.join(TEMPLATES_DIR, `supply-chain-${config.packageManager}`));
|
|
214
|
+
overlay(path.join(TEMPLATES_DIR, "adapters", config.adapter));
|
|
215
|
+
renderDir(target, buildContext(config));
|
|
216
|
+
appendGitignoreFiles(target);
|
|
217
|
+
appendLockfileIgnores(target, config.packageManager);
|
|
218
|
+
fs.symlinkSync("AGENTS.md", path.join(target, "CLAUDE.md"));
|
|
219
|
+
const envExample = path.join(target, ".env.example");
|
|
220
|
+
const envFile = path.join(target, ".env");
|
|
221
|
+
if (fs.existsSync(envExample) && !fs.existsSync(envFile)) fs.copyFileSync(envExample, envFile);
|
|
222
|
+
}
|
|
223
|
+
function copyDir(src, dest) {
|
|
224
|
+
if (!fs.existsSync(src)) return;
|
|
225
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
226
|
+
const srcPath = path.join(src, entry.name);
|
|
227
|
+
const destPath = path.join(dest, entry.name);
|
|
228
|
+
if (entry.isDirectory()) {
|
|
229
|
+
fs.mkdirSync(destPath, { recursive: true });
|
|
230
|
+
copyDir(srcPath, destPath);
|
|
231
|
+
} else {
|
|
232
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
233
|
+
fs.copyFileSync(srcPath, destPath);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function mergePartials(dir) {
|
|
238
|
+
const pkgPath = path.join(dir, "package.json");
|
|
239
|
+
if (!fs.existsSync(pkgPath)) return;
|
|
240
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
241
|
+
const partials = findFiles(dir, "package.json.partial");
|
|
242
|
+
for (const partialPath of partials) {
|
|
243
|
+
deepMerge(pkg, JSON.parse(fs.readFileSync(partialPath, "utf-8")));
|
|
244
|
+
fs.unlinkSync(partialPath);
|
|
245
|
+
}
|
|
246
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
247
|
+
}
|
|
248
|
+
function deepMerge(target, source) {
|
|
249
|
+
for (const key of Object.keys(source)) {
|
|
250
|
+
const targetVal = target[key];
|
|
251
|
+
const sourceVal = source[key];
|
|
252
|
+
if (targetVal && sourceVal && typeof targetVal === "object" && typeof sourceVal === "object" && !Array.isArray(targetVal) && !Array.isArray(sourceVal)) deepMerge(targetVal, sourceVal);
|
|
253
|
+
else target[key] = sourceVal;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
function findFiles(dir, filename) {
|
|
257
|
+
const results = [];
|
|
258
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
259
|
+
const fullPath = path.join(dir, entry.name);
|
|
260
|
+
if (entry.isDirectory()) results.push(...findFiles(fullPath, filename));
|
|
261
|
+
else if (entry.name === filename) results.push(fullPath);
|
|
262
|
+
}
|
|
263
|
+
return results;
|
|
264
|
+
}
|
|
265
|
+
function buildContext(config) {
|
|
266
|
+
return {
|
|
267
|
+
projectName: config.projectName,
|
|
268
|
+
pmInstall: pmInstall(config.packageManager),
|
|
269
|
+
pmRun: pmRun(config.packageManager),
|
|
270
|
+
pmExec: pmExec(config.packageManager),
|
|
271
|
+
pm: config.packageManager,
|
|
272
|
+
lockfile: pmLockfile(config.packageManager),
|
|
273
|
+
adapter: config.adapter,
|
|
274
|
+
seedRunner: config.packageManager === "bun" ? "bun" : "node --experimental-strip-types --env-file=.env",
|
|
275
|
+
i18n: config.i18n,
|
|
276
|
+
testing: config.testing,
|
|
277
|
+
githubActions: config.githubActions,
|
|
278
|
+
uiVendor: config.uiMode === "vendor",
|
|
279
|
+
uiPrefix: config.uiMode === "vendor" ? "$lib/components/ui" : "@mikstack/ui",
|
|
280
|
+
pmIsNpm: config.packageManager === "npm",
|
|
281
|
+
pmIsPnpm: config.packageManager === "pnpm",
|
|
282
|
+
pmIsBun: config.packageManager === "bun"
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
function appendGitignoreFiles(dir) {
|
|
286
|
+
const gitignorePath = path.join(dir, ".gitignore");
|
|
287
|
+
if (!fs.existsSync(gitignorePath)) return;
|
|
288
|
+
const appendFiles = findFiles(dir, ".gitignore.append");
|
|
289
|
+
if (appendFiles.length === 0) return;
|
|
290
|
+
let content = fs.readFileSync(gitignorePath, "utf-8");
|
|
291
|
+
for (const appendFile of appendFiles) {
|
|
292
|
+
const appendContent = fs.readFileSync(appendFile, "utf-8").trim();
|
|
293
|
+
if (appendContent) content = content.trimEnd() + "\n\n" + appendContent + "\n";
|
|
294
|
+
fs.unlinkSync(appendFile);
|
|
295
|
+
}
|
|
296
|
+
fs.writeFileSync(gitignorePath, content);
|
|
297
|
+
}
|
|
298
|
+
const ALL_LOCKFILES = {
|
|
299
|
+
"package-lock.json": "npm",
|
|
300
|
+
"pnpm-lock.yaml": "pnpm",
|
|
301
|
+
"bun.lock": "bun"
|
|
302
|
+
};
|
|
303
|
+
function appendLockfileIgnores(dir, pm) {
|
|
304
|
+
const gitignorePath = path.join(dir, ".gitignore");
|
|
305
|
+
const lines = Object.entries(ALL_LOCKFILES).filter(([, lockPm]) => lockPm !== pm).map(([file]) => file);
|
|
306
|
+
if (lines.length > 0) {
|
|
307
|
+
const content = fs.readFileSync(gitignorePath, "utf-8");
|
|
308
|
+
fs.writeFileSync(gitignorePath, content.trimEnd() + "\n\n# Other package manager lockfiles\n" + lines.join("\n") + "\n");
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
const BINARY_EXTENSIONS = new Set([
|
|
312
|
+
".png",
|
|
313
|
+
".jpg",
|
|
314
|
+
".jpeg",
|
|
315
|
+
".gif",
|
|
316
|
+
".ico",
|
|
317
|
+
".webp",
|
|
318
|
+
".avif",
|
|
319
|
+
".woff",
|
|
320
|
+
".woff2",
|
|
321
|
+
".ttf",
|
|
322
|
+
".eot",
|
|
323
|
+
".zip",
|
|
324
|
+
".tar",
|
|
325
|
+
".gz"
|
|
326
|
+
]);
|
|
327
|
+
function renderDir(dir, context) {
|
|
328
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
329
|
+
const fullPath = path.join(dir, entry.name);
|
|
330
|
+
if (entry.isDirectory()) {
|
|
331
|
+
if (entry.name === ".git") continue;
|
|
332
|
+
renderDir(fullPath, context);
|
|
333
|
+
} else {
|
|
334
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
335
|
+
if (BINARY_EXTENSIONS.has(ext)) continue;
|
|
336
|
+
const content = fs.readFileSync(fullPath, "utf-8");
|
|
337
|
+
const rendered = renderTemplate(content, context);
|
|
338
|
+
if (rendered !== content) fs.writeFileSync(fullPath, rendered);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
//#endregion
|
|
344
|
+
//#region src/cli.ts
|
|
345
|
+
async function cli() {
|
|
346
|
+
const { values, positionals } = parseArgs({
|
|
347
|
+
allowPositionals: true,
|
|
348
|
+
options: {
|
|
349
|
+
yes: {
|
|
350
|
+
type: "boolean",
|
|
351
|
+
short: "y",
|
|
352
|
+
default: false
|
|
353
|
+
},
|
|
354
|
+
help: {
|
|
355
|
+
type: "boolean",
|
|
356
|
+
short: "h",
|
|
357
|
+
default: false
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
if (values.help) {
|
|
362
|
+
printHelp();
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const packageManager = detectPackageManager();
|
|
366
|
+
const projectName = positionals[0];
|
|
367
|
+
const nonInteractive = values.yes || process.env.CI === "true";
|
|
368
|
+
let config;
|
|
369
|
+
if (nonInteractive) {
|
|
370
|
+
const targetDir = projectName || "my-mikstack-app";
|
|
371
|
+
config = {
|
|
372
|
+
projectName: path.basename(targetDir),
|
|
373
|
+
targetDir,
|
|
374
|
+
packageManager,
|
|
375
|
+
...defaults
|
|
376
|
+
};
|
|
377
|
+
p.intro("create-mikstack");
|
|
378
|
+
p.log.info(`Scaffolding ${config.projectName} with recommended defaults (non-interactive)...`);
|
|
379
|
+
} else config = await runPrompts(projectName, packageManager);
|
|
380
|
+
try {
|
|
381
|
+
scaffold(config);
|
|
382
|
+
} catch (err) {
|
|
383
|
+
p.log.error(err instanceof Error ? err.message : String(err));
|
|
384
|
+
process.exit(1);
|
|
385
|
+
}
|
|
386
|
+
postScaffold(config.targetDir, config.packageManager);
|
|
387
|
+
p.note([`cd ${config.targetDir}`, `${config.packageManager} run dev`].join("\n"), "Next steps");
|
|
388
|
+
}
|
|
389
|
+
function printHelp() {
|
|
390
|
+
console.log(`
|
|
391
|
+
create-mikstack — scaffold an opinionated SvelteKit project
|
|
392
|
+
|
|
393
|
+
Usage:
|
|
394
|
+
create-mikstack [project-name] [options]
|
|
395
|
+
|
|
396
|
+
Options:
|
|
397
|
+
-y, --yes Use recommended defaults (non-interactive)
|
|
398
|
+
-h, --help Show this help message
|
|
399
|
+
`);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
//#endregion
|
|
403
|
+
//#region src/index.ts
|
|
404
|
+
cli().catch((err) => {
|
|
405
|
+
console.error(err);
|
|
406
|
+
process.exit(1);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
//#endregion
|
|
410
|
+
export { };
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-mikstack",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/MikaelSiidorow/mikstack.git",
|
|
8
|
+
"directory": "packages/create-mikstack"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"create-mikstack": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"templates"
|
|
16
|
+
],
|
|
17
|
+
"type": "module",
|
|
18
|
+
"scripts": {
|
|
19
|
+
"sync:ui": "bash scripts/sync-ui-vendor.sh",
|
|
20
|
+
"build": "bun run sync:ui && tsdown",
|
|
21
|
+
"dev": "tsdown --watch",
|
|
22
|
+
"check": "tsc --noEmit",
|
|
23
|
+
"lint": "oxlint --type-aware --type-check --ignore-pattern templates && oxfmt --check src tsdown.config.ts",
|
|
24
|
+
"format": "oxfmt src tsdown.config.ts",
|
|
25
|
+
"test": "bun test"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@clack/prompts": "^0.10.0",
|
|
29
|
+
"picocolors": "^1.1.1",
|
|
30
|
+
"sv": "0.12.1"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^22.15.0",
|
|
37
|
+
"oxfmt": "^0.28.0",
|
|
38
|
+
"oxlint": "^1.43.0",
|
|
39
|
+
"oxlint-tsgolint": "^0.11.4",
|
|
40
|
+
"tsdown": "^0.12.0",
|
|
41
|
+
"typescript": "^5.9.3"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// {{#if:i18n}}
|
|
2
|
+
import { linguiPreprocess } from "@mikstack/svelte-lingui/preprocessor";
|
|
3
|
+
// {{/if:i18n}}
|
|
4
|
+
import adapter from "@sveltejs/adapter-cloudflare";
|
|
5
|
+
|
|
6
|
+
/** @type {import('@sveltejs/kit').Config} */
|
|
7
|
+
const config = {
|
|
8
|
+
// {{#if:i18n}}
|
|
9
|
+
preprocess: [linguiPreprocess()],
|
|
10
|
+
// {{/if:i18n}}
|
|
11
|
+
kit: {
|
|
12
|
+
adapter: adapter(),
|
|
13
|
+
experimental: {
|
|
14
|
+
remoteFunctions: true,
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default config;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
FROM node:22-slim AS base
|
|
2
|
+
WORKDIR /app
|
|
3
|
+
|
|
4
|
+
FROM base AS deps
|
|
5
|
+
COPY package.json {{lockfile}} ./
|
|
6
|
+
# {{#if:pmIsNpm}}
|
|
7
|
+
RUN npm ci
|
|
8
|
+
# {{/if:pmIsNpm}}
|
|
9
|
+
# {{#if:pmIsPnpm}}
|
|
10
|
+
RUN corepack enable && pnpm install --frozen-lockfile
|
|
11
|
+
# {{/if:pmIsPnpm}}
|
|
12
|
+
# {{#if:pmIsBun}}
|
|
13
|
+
RUN npm i -g bun && bun install --frozen-lockfile
|
|
14
|
+
# {{/if:pmIsBun}}
|
|
15
|
+
|
|
16
|
+
FROM base AS build
|
|
17
|
+
WORKDIR /app
|
|
18
|
+
COPY --from=deps /app/node_modules ./node_modules
|
|
19
|
+
COPY . .
|
|
20
|
+
RUN npm run build
|
|
21
|
+
RUN npm prune --omit=dev
|
|
22
|
+
|
|
23
|
+
FROM base
|
|
24
|
+
WORKDIR /app
|
|
25
|
+
COPY --from=build /app/build ./build
|
|
26
|
+
COPY --from=build /app/node_modules ./node_modules
|
|
27
|
+
COPY package.json .
|
|
28
|
+
ENV NODE_ENV=production
|
|
29
|
+
EXPOSE 3000
|
|
30
|
+
CMD ["node", "build"]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
services:
|
|
2
|
+
db:
|
|
3
|
+
image: postgres
|
|
4
|
+
restart: always
|
|
5
|
+
environment:
|
|
6
|
+
POSTGRES_USER: root
|
|
7
|
+
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
|
8
|
+
POSTGRES_DB: { { projectName } }
|
|
9
|
+
command: ["postgres", "-c", "wal_level=logical"]
|
|
10
|
+
volumes:
|
|
11
|
+
- pgdata:/var/lib/postgresql/data
|
|
12
|
+
|
|
13
|
+
app:
|
|
14
|
+
build: .
|
|
15
|
+
restart: always
|
|
16
|
+
ports:
|
|
17
|
+
- 3000:3000
|
|
18
|
+
environment:
|
|
19
|
+
DATABASE_URL: postgres://root:${POSTGRES_PASSWORD}@db:5432/{{projectName}}
|
|
20
|
+
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET}
|
|
21
|
+
BETTER_AUTH_URL: ${BETTER_AUTH_URL}
|
|
22
|
+
ORIGIN: ${BETTER_AUTH_URL}
|
|
23
|
+
depends_on:
|
|
24
|
+
- db
|
|
25
|
+
|
|
26
|
+
volumes:
|
|
27
|
+
pgdata:
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// {{#if:i18n}}
|
|
2
|
+
import { linguiPreprocess } from "@mikstack/svelte-lingui/preprocessor";
|
|
3
|
+
// {{/if:i18n}}
|
|
4
|
+
import adapter from "@sveltejs/adapter-node";
|
|
5
|
+
|
|
6
|
+
/** @type {import('@sveltejs/kit').Config} */
|
|
7
|
+
const config = {
|
|
8
|
+
// {{#if:i18n}}
|
|
9
|
+
preprocess: [linguiPreprocess()],
|
|
10
|
+
// {{/if:i18n}}
|
|
11
|
+
kit: {
|
|
12
|
+
adapter: adapter(),
|
|
13
|
+
experimental: {
|
|
14
|
+
remoteFunctions: true,
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default config;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// {{#if:i18n}}
|
|
2
|
+
import { linguiPreprocess } from "@mikstack/svelte-lingui/preprocessor";
|
|
3
|
+
// {{/if:i18n}}
|
|
4
|
+
import adapter from "@sveltejs/adapter-vercel";
|
|
5
|
+
|
|
6
|
+
/** @type {import('@sveltejs/kit').Config} */
|
|
7
|
+
const config = {
|
|
8
|
+
// {{#if:i18n}}
|
|
9
|
+
preprocess: [linguiPreprocess()],
|
|
10
|
+
// {{/if:i18n}}
|
|
11
|
+
kit: {
|
|
12
|
+
adapter: adapter(),
|
|
13
|
+
experimental: {
|
|
14
|
+
remoteFunctions: true,
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default config;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Database (PostgreSQL with wal_level=logical required for Zero replication)
|
|
2
|
+
DATABASE_URL="postgres://root:mysecretpassword@localhost:5432/local"
|
|
3
|
+
ZERO_UPSTREAM_DB="postgres://root:mysecretpassword@localhost:5432/local"
|
|
4
|
+
|
|
5
|
+
# Zero Cache Configuration
|
|
6
|
+
ZERO_REPLICA_FILE="/tmp/{{projectName}}-sync-replica.db"
|
|
7
|
+
ZERO_MUTATE_URL="http://localhost:5173/api/zero/mutate"
|
|
8
|
+
ZERO_QUERY_URL="http://localhost:5173/api/zero/get-queries"
|
|
9
|
+
PUBLIC_SERVER="http://localhost:4848"
|
|
10
|
+
ZERO_QUERY_FORWARD_COOKIES=true
|
|
11
|
+
ZERO_MUTATE_FORWARD_COOKIES=true
|
|
12
|
+
|
|
13
|
+
# better-auth
|
|
14
|
+
# Generate with: openssl rand -base64 32
|
|
15
|
+
BETTER_AUTH_SECRET="change-me-generate-a-real-secret"
|
|
16
|
+
BETTER_AUTH_URL="http://localhost:5173"
|
|
17
|
+
|
|
18
|
+
# SMTP (for magic link emails in production)
|
|
19
|
+
# SMTP_HOST=smtp.example.com
|
|
20
|
+
# SMTP_PORT=587
|
|
21
|
+
# SMTP_USER=
|
|
22
|
+
# SMTP_PASS=
|
|
23
|
+
# SMTP_FROM=noreply@example.com
|