create-loumi-app 1.2.2 → 1.3.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/dist/index.js
CHANGED
|
@@ -1,28 +1,30 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {Command}from'commander';import*as
|
|
3
|
-
`);}var
|
|
2
|
+
import {Command}from'commander';import*as r from'@clack/prompts';import p from'chalk';import w from'path';import v from'fs-extra';import m from'ora';import {execa}from'execa';import {fileURLToPath}from'url';import ie from'sort-package-json';async function I(e){r.intro(p.bgBlue.white(" create-loumi-app "));let t=await r.text({message:"Projektname?",placeholder:"my-loumi-app",initialValue:e,validate:i=>{if(!i)return "Projektname ist erforderlich";if(!/^[a-z0-9-_]+$/i.test(i))return "Nur Buchstaben, Zahlen, Bindestriche und Unterstriche erlaubt"}});if(r.isCancel(t))return r.cancel("Abgebrochen."),null;let a=await r.multiselect({message:"Welche Add-ons m\xF6chtest du? (Space zum Ausw\xE4hlen)",options:[{value:"convex",label:"Convex",hint:"Backend, DB, Real-time + TanStack Query"},{value:"better-auth",label:"Better Auth",hint:"Authentication (Email/Password, OAuth) via Convex"},{value:"sanity",label:"Sanity",hint:"Headless CMS (Portable Text) + Blog-Routes"},{value:"resend",label:"Resend",hint:"E-Mail Versand (Transactional Emails)"},{value:"seo",label:"Advanced SEO",hint:"JSON-LD, Schema.org, robots.txt, llms.txt"},{value:"cloudflare",label:"Cloudflare Workers",hint:"Deploy auf Cloudflare Workers statt Netlify"}],required:false});if(r.isCancel(a))return r.cancel("Abgebrochen."),null;let o=await r.confirm({message:"Dependencies installieren?",initialValue:true});if(r.isCancel(o))return r.cancel("Abgebrochen."),null;let n=await r.confirm({message:"Git Repository initialisieren?",initialValue:true});return r.isCancel(n)?(r.cancel("Abgebrochen."),null):{projectName:t,addons:a||[],installDeps:o,initGit:n}}var te=fileURLToPath(import.meta.url),ne=w.dirname(te);function A(e){return w.join(ne,"..","template",e)}async function c(e,t){let a=A(e);await v.copy(a,t,{overwrite:true});}async function D(e,t,a){let o=w.join(e,t),n=w.join(e,a);await v.pathExists(o)&&await v.move(o,n,{overwrite:true});}async function y(e,t,a){if(!await v.pathExists(e))return;let n=(await v.readFile(e,"utf-8")).replaceAll(t,a);await v.writeFile(e,n);}async function l(e,t){await v.pathExists(e)?await v.appendFile(e,t):await v.writeFile(e,t);}var O={react:"^19.2.0","react-dom":"^19.2.0","@tanstack/react-router":"^1.158.0","@tanstack/react-start":"^1.158.0",tailwindcss:"^4.1.0","@tailwindcss/vite":"^4.1.0",typescript:"^5.9.0",vite:"^7.3.0","@vitejs/plugin-react":"^5.1.0","@netlify/vite-plugin-tanstack-start":"^1.0.0","@cloudflare/vite-plugin":"^1.23.0",wrangler:"^4.63.0","@cloudflare/workers-types":"^4.20260206.0","@cloudflare/unenv-preset":"^2.12.0",convex:"^1.31.0","@convex-dev/react-query":"^0.1.0","@tanstack/react-query":"^5.90.0","@tanstack/react-router-ssr-query":"^1.158.0","@sanity/client":"^7.14.0","@sanity/image-url":"^2.0.0","@portabletext/react":"^6.0.0","better-auth":"^1.4.18","@convex-dev/better-auth":"^0.10.10",resend:"^6.9.0","@types/react":"^19.2.0","@types/react-dom":"^19.2.0"};function se(e){return O[e]}function d(e){let t={};for(let a of e)t[a]=se(a);return t}async function u(e,{dependencies:t={},devDependencies:a={},scripts:o={}}){let n=w.join(e,"package.json"),i=await v.readJson(n);i.dependencies={...i.dependencies,...t},i.devDependencies={...i.devDependencies,...a},i.scripts={...i.scripts,...o};let g=ie(JSON.stringify(i,null,2));await v.writeFile(n,g+`
|
|
3
|
+
`);}var le=["react","react-dom","@tanstack/react-router","@tanstack/react-start","tailwindcss","@tailwindcss/vite"],ce=["typescript","vite","@vitejs/plugin-react","@netlify/vite-plugin-tanstack-start","@types/react","@types/react-dom"];async function L(e,t){await c("base",e),await D(e,"_gitignore",".gitignore");let a={name:t,version:"0.1.0",private:true,type:"module",scripts:{dev:"vite dev",build:"vite build",start:"vite preview",deploy:"netlify deploy --build --prod"},dependencies:{},devDependencies:{}};await v.writeJson(w.join(e,"package.json"),a,{spaces:2}),await u(e,{dependencies:d(le),devDependencies:d(ce)});}var de=["convex","@convex-dev/react-query","@tanstack/react-query","@tanstack/react-router-ssr-query"],pe=`
|
|
4
4
|
# Convex
|
|
5
5
|
# Run "npx convex dev" to generate these values
|
|
6
6
|
CONVEX_DEPLOYMENT=
|
|
7
7
|
VITE_CONVEX_URL=
|
|
8
|
-
`,
|
|
8
|
+
`,ue="\n\n## Convex (Backend / Database)\n\n- **Real-time database** with automatic WebSocket subscriptions\n- Convex functions live in `convex/` directory (schema, queries, mutations)\n- Schema defined in `convex/schema.ts` using `defineSchema`, `defineTable`, `v` validators\n- Queries/mutations exported from `convex/*.ts` using `query()` and `mutation()`\n- Client integration via `@convex-dev/react-query` \u2014 bridges Convex into TanStack Query cache\n- Router uses `setupRouterSsrQueryIntegration` from `@tanstack/react-router-ssr-query`\n- SSR data loading: `context.queryClient.ensureQueryData(convexQuery(api.module.fn, args))`\n- Client-side reading: `useSuspenseQuery(convexQuery(api.module.fn, args))`\n- Mutations: `useConvexMutation(api.module.fn)` or `useMutation` from react-query\n- `_generated/` folder is auto-generated \u2014 never edit manually\n- Run `npx convex dev` to start Convex dev server and generate types\n";async function B(e){await c("addons/convex",e),await u(e,{dependencies:d(de)}),await l(w.join(e,".env.local"),pe),await l(w.join(e,"CLAUDE.md"),ue);}var ge=["better-auth","@convex-dev/better-auth"],fe=`
|
|
9
9
|
# Better Auth
|
|
10
10
|
# Set BETTER_AUTH_SECRET on Convex: npx convex env set BETTER_AUTH_SECRET=$(openssl rand -base64 32)
|
|
11
11
|
# Set SITE_URL on Convex: npx convex env set SITE_URL=http://localhost:3000
|
|
12
12
|
VITE_CONVEX_SITE_URL=
|
|
13
13
|
VITE_SITE_URL=http://localhost:3000
|
|
14
|
-
`,
|
|
14
|
+
`,ve='\n\n## Better Auth (Authentication)\n\n- Uses `@convex-dev/better-auth` \u2014 Better Auth runs INSIDE Convex (single user table, no external DB)\n- Auth tables (user, session, account) managed by the Convex component \u2014 NOT in your schema\n- Server config: `convex/auth.ts` \u2014 `authComponent.adapter(ctx)` bridges Better Auth to Convex DB\n- Client: `src/lib/auth-client.ts` \u2014 `createAuthClient` with `convexClient()` plugin\n- Server utils: `src/lib/auth-server.ts` \u2014 `getToken`, `fetchAuthQuery`, etc.\n- API route: `src/routes/api.auth.$.ts` \u2014 catch-all handler for Better Auth endpoints\n- Root route uses `beforeLoad` to inject `isAuthenticated` + `token` into route context\n- Route protection: `if (!context.isAuthenticated) throw redirect({ to: "/login" })`\n- Client-side auth state: use `Authenticated`, `Unauthenticated`, `useConvexAuth()` from `convex/react`\n- Sign-in/up: `authClient.signIn.email({ email, password })` (client-side only)\n- Sign-out: use `location.reload()` in onSuccess callback (required with `expectAuth: true`)\n- Convex env vars (set via CLI, not .env.local): `BETTER_AUTH_SECRET`, `SITE_URL`\n- Do NOT create a separate user table in your Convex schema \u2014 use triggers for app-specific user data\n';async function J(e,t){t.includes("convex")||console.log(p.yellow(`
|
|
15
15
|
\u26A0 Better Auth ben\xF6tigt Convex. Convex wird automatisch mit installiert.
|
|
16
|
-
`)),await
|
|
16
|
+
`)),await c("addons/better-auth",e),await u(e,{dependencies:d(ge)}),await l(w.join(e,".env.local"),fe),await l(w.join(e,"CLAUDE.md"),ve);}var he=["@sanity/client","@sanity/image-url","@portabletext/react"],ye=`
|
|
17
17
|
# Sanity
|
|
18
18
|
VITE_SANITY_PROJECT_ID=
|
|
19
19
|
VITE_SANITY_DATASET=production
|
|
20
20
|
VITE_SANITY_API_VERSION=2025-01-01
|
|
21
|
-
`,
|
|
21
|
+
`,we='\n\n## Sanity (Headless CMS)\n\n- Client configured in `src/lib/sanity.ts` using `@sanity/client`\n- Content fetched via GROQ queries in route loaders: `sanityClient.fetch(QUERY, params)`\n- Blog routes: `/blog` (layout), `/blog/` (list), `/blog/$slug` (detail)\n- Rich text rendered with `<PortableText value={body} />` from `@portabletext/react`\n- Images: use `urlFor(source).width(x).url()` helper from `src/lib/sanity.ts`\n- Sanity schemas in `sanity/schemaTypes/` \u2014 documents and objects\n- SEO object type available on documents (`metaTitle`, `metaDescription`, `ogImage`)\n- GROQ cheat sheet: `*[_type == "post"]{title, slug}`, `| order(publishedAt desc)`\n- Set `VITE_SANITY_PROJECT_ID` in `.env.local` before running\n';async function W(e){await c("addons/sanity",e),await u(e,{dependencies:d(he)}),await l(w.join(e,".env.local"),ye),await l(w.join(e,"CLAUDE.md"),we);}var xe=["resend"],be=`
|
|
22
22
|
# Resend
|
|
23
23
|
RESEND_API_KEY=
|
|
24
|
-
`,
|
|
24
|
+
`,Ee="\n\n## Resend (Email)\n\n- Client configured in `src/lib/email.ts` using the `resend` SDK\n- `sendEmail({ to, subject, html })` helper \u2014 call from server-side code only (loaders, API routes)\n- `RESEND_API_KEY` must be set in `.env.local` (not `VITE_` prefixed \u2014 server-only)\n- Default `from` is `onboarding@resend.dev` \u2014 change to your verified domain\n- For React email templates, consider adding `@react-email/components`\n";async function q(e){await c("addons/resend",e),await u(e,{dependencies:d(xe)}),await l(w.join(e,".env.local"),be),await l(w.join(e,"CLAUDE.md"),Ee);}var ke='\n\n## Advanced SEO\n\n- `seo()` helper in `src/lib/seo.ts` \u2014 generates meta + links arrays for `head()`\n Usage: `head: () => ({ ...seo({ title: "Page", description: "..." }) })`\n- JSON-LD helpers in `src/lib/schema-org.ts`: `organizationSchema`, `webSiteSchema`, `articleSchema`, `breadcrumbSchema`\n- `public/robots.txt` \u2014 update Sitemap URL before deploying\n- `public/llms.txt` \u2014 LLM-readable site description, update with actual content\n- Always set `title` and `description` on every route via `head()`\n- Use `canonicalUrl` for pages accessible at multiple URLs\n- Use `noIndex: true` for pages that should not be indexed\n';async function G(e,t,a){await c("addons/seo",e),await y(w.join(e,"public","llms.txt"),"PROJECT_NAME",t);let o=a.includes("cloudflare")?`${t}.workers.dev`:`${t}.netlify.app`;await y(w.join(e,"public","robots.txt"),"PROJECT_DOMAIN",o),await l(w.join(e,"CLAUDE.md"),ke);}var Se=["@cloudflare/vite-plugin","wrangler","@cloudflare/workers-types","@cloudflare/unenv-preset"],Ce="\n\n## Cloudflare Workers\n\n- **Hosting:** Cloudflare Workers (Wrangler, `wrangler.jsonc`)\n- `wrangler dev` \u2014 lokaler Preview mit Cloudflare Workers Runtime\n- `wrangler deploy` \u2014 Deploy auf Cloudflare Workers\n- `vite.config.ts` nutzt `@cloudflare/vite-plugin` statt Netlify-Plugin\n- Environment Variables \xFCber Wrangler Dashboard oder `wrangler secret put`\n";async function M(e,t){await c("addons/cloudflare",e);let a=w.join(e,"netlify.toml");await v.pathExists(a)&&await v.remove(a),await u(e,{devDependencies:d(Se)});let o=w.join(e,"package.json"),n=await v.readJson(o);n.scripts.preview="wrangler dev",n.scripts.deploy="vite build && wrangler deploy",delete n.devDependencies["@netlify/vite-plugin-tanstack-start"],await v.writeJson(o,n,{spaces:2});let i=t.replace(/([a-z])([A-Z])/g,"$1-$2").replace(/[^a-z0-9-]/gi,"-").replace(/-+/g,"-").replace(/^-|-$/g,"").toLowerCase();await y(w.join(e,"wrangler.jsonc"),"PROJECT_NAME",i),await y(w.join(e,"CLAUDE.md"),"- **Hosting:** Netlify (`netlify.toml`)","- **Hosting:** Cloudflare Workers (`wrangler.jsonc`)"),await y(w.join(e,"CLAUDE.md"),`pnpm start # Preview production build
|
|
25
|
+
pnpm deploy # Build + deploy to Netlify`,`pnpm preview # Preview with Wrangler (local Cloudflare)
|
|
26
|
+
pnpm deploy # Build + deploy to Cloudflare Workers`),await l(w.join(e,"CLAUDE.md"),Ce);}async function Q(e){try{return await execa("git",["init"],{cwd:e}),await execa("git",["add","-A"],{cwd:e}),await execa("git",["commit","-m","Initial commit from create-loumi-app"],{cwd:e}),!0}catch{return false}}var Te=["convex","better-auth","sanity","resend","seo"];async function z(e,t){let a=w.join(e,".agents","skills"),o=w.join(e,".claude","skills");await v.ensureDir(a),await v.ensureDir(o);let n=0,i=0,g=["base",...t.filter(x=>Te.includes(x))];for(let x of g){let b=A(w.join("skills",x));if(!await v.pathExists(b))continue;let k=await v.readdir(b);for(let s of k)try{let f=w.join(b,s),K=w.join(a,s);await v.copy(f,K,{overwrite:!0});let T=w.join(o,s),X=w.join("..","..",".agents","skills",s);await v.pathExists(T)&&await v.remove(T),await v.symlink(X,T),n++;}catch{i++;}}return {installed:n,failed:i}}async function N(e){let{projectName:t,installDeps:a,initGit:o}=e,n=[...e.addons],i=w.resolve(process.cwd(),t);await v.pathExists(i)&&(await v.readdir(i)).length>0&&(console.log(p.red(`
|
|
25
27
|
Error: Verzeichnis "${t}" ist nicht leer.
|
|
26
|
-
`)),process.exit(1)),await
|
|
28
|
+
`)),process.exit(1)),await v.ensureDir(i),n.includes("better-auth")&&!n.includes("convex")&&n.push("convex");let g=m("Base Template kopieren...").start();if(await L(i,t),g.succeed("Base Template erstellt"),n.includes("convex")){let s=m("Convex einrichten...").start();await B(i),s.succeed("Convex eingerichtet");}if(n.includes("better-auth")){let s=m("Better Auth einrichten...").start();await J(i,n),s.succeed("Better Auth eingerichtet");}if(n.includes("sanity")){let s=m("Sanity einrichten...").start();await W(i),s.succeed("Sanity eingerichtet");}if(n.includes("resend")){let s=m("Resend einrichten...").start();await q(i),s.succeed("Resend eingerichtet");}if(n.includes("seo")){let s=m("Advanced SEO einrichten...").start();await G(i,t,n),s.succeed("Advanced SEO eingerichtet");}if(n.includes("cloudflare")){let s=m("Cloudflare Workers einrichten...").start();await M(i,t),s.succeed("Cloudflare Workers eingerichtet");}let x=m("Claude Skills installieren...").start(),{installed:b,failed:k}=await z(i,n);if(k===0?x.succeed(`${b} Claude Skills installiert`):x.warn(`${b} Skills installiert, ${k} fehlgeschlagen`),a){let s=m("Dependencies installieren...").start();try{let f=Pe();try{await execa(f,["install"],{cwd:i});}catch{await execa(f,["install","--ignore-scripts"],{cwd:i});}s.succeed(`Dependencies installiert (${f})`);}catch(f){s.fail("Dependencies konnten nicht installiert werden"),f instanceof Error&&console.log(p.yellow(` ${f.message}
|
|
27
29
|
`)),console.log(p.yellow(` F\xFChre manuell 'npm install' aus.
|
|
28
|
-
`));}}if(
|
|
30
|
+
`));}}if(o){let s=m("Git initialisieren...").start();await Q(i)?s.succeed("Git Repository initialisiert"):s.fail("Git konnte nicht initialisiert werden");}console.log(),console.log(p.green("\u2714 Projekt erstellt!")),console.log(),console.log(" N\xE4chste Schritte:"),console.log(p.cyan(` cd ${t}`)),a||console.log(p.cyan(" npm install")),n.includes("convex")&&console.log(p.cyan(" npx convex dev")),console.log(p.cyan(" npm run dev")),console.log(),n.includes("cloudflare")?console.log(p.dim(" Hosting: Cloudflare Workers (wrangler dev / wrangler deploy)")):console.log(p.dim(" Hosting: Netlify (netlify deploy --build --prod)")),n.includes("sanity")&&console.log(p.dim(" Vergiss nicht VITE_SANITY_PROJECT_ID in .env.local zu setzen!")),n.includes("better-auth")&&console.log(p.dim(" Better Auth Setup: npx convex env set BETTER_AUTH_SECRET=$(openssl rand -base64 32)")),n.includes("resend")&&console.log(p.dim(" Vergiss nicht RESEND_API_KEY in .env.local zu setzen!")),console.log();}function Pe(){let e=process.env.npm_config_user_agent;if(e){if(e.startsWith("pnpm"))return "pnpm";if(e.startsWith("yarn"))return "yarn";if(e.startsWith("bun"))return "bun"}return "npm"}var Y=new Command;Y.name("create-loumi-app").description("Scaffold a new Loumi web project").version("1.0.0").argument("[project-name]","Name of the project").option("--skip-install","Skip dependency installation").option("--add-ons <addons>","Comma-separated list of add-ons (convex,better-auth,sanity,resend,seo,cloudflare)").action(async(e,t)=>{if(e&&t.addOns!==void 0){let o=t.addOns.split(",").map(g=>g.trim().toLowerCase()).filter(Boolean),n=["convex","better-auth","sanity","resend","seo","cloudflare"],i=o.filter(g=>!n.includes(g));i.length>0&&(console.error(`Unknown add-ons: ${i.join(", ")}`),console.error(`Valid add-ons: ${n.join(", ")}`),process.exit(1)),await N({projectName:e,addons:o,installDeps:!t.skipInstall,initGit:true});return}let a=await I(e);a||process.exit(0),await N(a);});Y.parse();
|
package/package.json
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { defineConfig } from "vite";
|
|
2
|
+
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
|
|
3
|
+
import { cloudflare } from "@cloudflare/vite-plugin";
|
|
4
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
5
|
+
import viteReact from "@vitejs/plugin-react";
|
|
6
|
+
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
plugins: [
|
|
9
|
+
cloudflare({ inspectorPort: false, viteEnvironment: { name: "ssr" } }),
|
|
10
|
+
tanstackStart(),
|
|
11
|
+
tailwindcss(),
|
|
12
|
+
viteReact(),
|
|
13
|
+
],
|
|
14
|
+
});
|
package/template/base/CLAUDE.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
- **Framework:** TanStack Start (React, file-based routing)
|
|
6
6
|
- **Router:** TanStack Router (`src/routes/` directory, `createFileRoute`)
|
|
7
7
|
- **Styling:** Tailwind CSS v4 (Vite plugin, `@import "tailwindcss"`, `@theme` for tokens)
|
|
8
|
-
- **Hosting:**
|
|
8
|
+
- **Hosting:** Netlify (`netlify.toml`)
|
|
9
9
|
- **Build:** Vite (`vite.config.ts`)
|
|
10
10
|
- **Language:** TypeScript (strict mode)
|
|
11
11
|
|
|
@@ -36,6 +36,6 @@ src/
|
|
|
36
36
|
```bash
|
|
37
37
|
pnpm dev # Start dev server (Vite)
|
|
38
38
|
pnpm build # Production build
|
|
39
|
-
pnpm
|
|
40
|
-
pnpm deploy # Build + deploy to
|
|
39
|
+
pnpm start # Preview production build
|
|
40
|
+
pnpm deploy # Build + deploy to Netlify
|
|
41
41
|
```
|
|
@@ -1,14 +1,9 @@
|
|
|
1
1
|
import { defineConfig } from "vite";
|
|
2
2
|
import { tanstackStart } from "@tanstack/react-start/plugin/vite";
|
|
3
|
-
import { cloudflare } from "@cloudflare/vite-plugin";
|
|
4
3
|
import tailwindcss from "@tailwindcss/vite";
|
|
5
4
|
import viteReact from "@vitejs/plugin-react";
|
|
5
|
+
import netlify from "@netlify/vite-plugin-tanstack-start";
|
|
6
6
|
|
|
7
7
|
export default defineConfig({
|
|
8
|
-
plugins: [
|
|
9
|
-
cloudflare({ viteEnvironment: { name: "ssr" } }),
|
|
10
|
-
tanstackStart(),
|
|
11
|
-
tailwindcss(),
|
|
12
|
-
viteReact(),
|
|
13
|
-
],
|
|
8
|
+
plugins: [tanstackStart(), tailwindcss(), viteReact(), netlify()],
|
|
14
9
|
});
|