create-lacspace-app 2.7.0 → 2.8.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 +7 -0
- package/dist/index.js +35 -10
- package/dist/lib.cjs +43 -9
- package/dist/lib.d.cts +30 -1
- package/dist/lib.d.ts +30 -1
- package/dist/lib.js +42 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,6 +20,13 @@ npx create-lacspace-app my-app --template saas --fullstack
|
|
|
20
20
|
|
|
21
21
|
You choose the *kind* of site you're building. It writes a **real Next.js 15 + React 19 + Tailwind v4 app** — not a hello-world, but a genuinely **polished, modern site**: a fluid `clamp()` type scale, tight display headings, a refined light **and** dark palette, glass chrome, soft layered shadows, a smooth logo marquee, animated counters, scroll reveals and a shimmering primary CTA — every page filled in, an SEO stack wired end-to-end, and a **26-component UI kit** you can drop in anywhere.
|
|
22
22
|
|
|
23
|
+
> **New in v2.8 — recipes.** Scaffold a whole *kind of product* in one command with `--recipe <key>` (a curated template + full-stack mode + add-on stack):
|
|
24
|
+
> - **`ai-saas`** → SaaS + accounts + payments + AI chat + analytics
|
|
25
|
+
> - **`store`** → e-commerce + eSewa/Khalti checkout + email + analytics
|
|
26
|
+
> - **`blog`** → blog + content + search · **`docs-ai`** → docs + RAG + search · **`internal-tool`** → dashboard + auth + analytics + email
|
|
27
|
+
>
|
|
28
|
+
> `npx create-lacspace-app my-app --recipe ai-saas`. Explicit `--template`/`--with`/`--fullstack` still merge on top. Also on the lib API: `listRecipes()` / `getRecipe()`.
|
|
29
|
+
|
|
23
30
|
> **New in v2.7 — payments & email add-ons.**
|
|
24
31
|
> - **`payments`** — a checkout wired to **eSewa & Khalti**: orders, integer-safe money (`@lacspace/money`), and the signed eSewa flow that **works end-to-end in test mode with NO credentials**. Khalti activates when you add `KHALTI_SECRET`.
|
|
25
32
|
> - **`email`** — transactional email: a ready mail service (`@lacspace/mailer`) with beautiful templates (`@lacspace/email-templates`) + address validation. **Logs emails to the console until you add SMTP** — so it runs out-of-the-box, then delivers for real with one env change.
|
package/dist/index.js
CHANGED
|
@@ -29,14 +29,15 @@ var TEMPLATES = [
|
|
|
29
29
|
{ key: "marketplace", label: "Marketplace / commerce", description: "A real storefront wired to the Lacspace commerce packages \u2014 cart, checkout, tax, shipping, orders, invoices and Nepal payments.", accent: ["#0d9488", "#6366f1"], siteName: "LSBazaar", siteDescription: "A modern storefront \u2014 cart to checkout, wired end to end." }
|
|
30
30
|
];
|
|
31
31
|
function resolveContext(options = {}) {
|
|
32
|
-
const
|
|
32
|
+
const recipe = options.recipe ? RECIPES.find((r) => r.key === String(options.recipe).toLowerCase()) : void 0;
|
|
33
|
+
const base = TEMPLATES.find((t) => t.key === (options.template ?? recipe?.template)) ?? TEMPLATES[0];
|
|
33
34
|
const accent = resolveAccent(options.theme);
|
|
34
35
|
const template = accent ? { ...base, accent } : base;
|
|
35
36
|
const raw = options.name ?? "my-app";
|
|
36
37
|
const seg = raw.split(/[\\/]/).filter(Boolean).pop() ?? "my-app";
|
|
37
38
|
const name = seg.toLowerCase().replace(/[^a-z0-9-_]/g, "-").replace(/^-+|-+$/g, "") || "my-app";
|
|
38
|
-
const features = normalizeFeatures(options.features);
|
|
39
|
-
let mode = options.mode === "dynamic" ? "dynamic" : "static";
|
|
39
|
+
const features = normalizeFeatures([...recipe?.features ?? [], ...options.features ?? []]);
|
|
40
|
+
let mode = options.mode === "dynamic" ? "dynamic" : options.mode === "static" ? "static" : recipe?.mode ?? "static";
|
|
40
41
|
if (mode === "static" && features.some((f) => f.requiresBackend)) mode = "dynamic";
|
|
41
42
|
return { name, template, features, mode };
|
|
42
43
|
}
|
|
@@ -5252,6 +5253,13 @@ var faqSection = (ctx) => {
|
|
|
5252
5253
|
</div>
|
|
5253
5254
|
</section>`;
|
|
5254
5255
|
};
|
|
5256
|
+
var RECIPES = [
|
|
5257
|
+
{ key: "ai-saas", label: "AI SaaS", description: "SaaS landing + accounts + payments + a streaming AI chat + analytics.", template: "saas", mode: "dynamic", features: ["auth-pages", "payments", "ai-chat", "analytics"] },
|
|
5258
|
+
{ key: "store", label: "Online store", description: "E-commerce storefront + eSewa/Khalti checkout + transactional email + analytics.", template: "ecommerce", mode: "dynamic", features: ["payments", "email", "analytics"] },
|
|
5259
|
+
{ key: "blog", label: "Blog", description: "A blog with a Markdown content section, RSS/llms.txt and instant search.", template: "blog", features: ["content", "search"] },
|
|
5260
|
+
{ key: "internal-tool", label: "Internal tool", description: "An admin dashboard with accounts (2FA), analytics and email \u2014 full-stack.", template: "dashboard", mode: "dynamic", features: ["auth-pages", "analytics", "email"] },
|
|
5261
|
+
{ key: "docs-ai", label: "AI docs", description: "A documentation site with chat-with-your-docs RAG and instant search.", template: "docs", features: ["rag", "search"] }
|
|
5262
|
+
];
|
|
5255
5263
|
var contentLib = () => `import fs from "node:fs";
|
|
5256
5264
|
import path from "node:path";
|
|
5257
5265
|
import { parseFrontmatter, markdownToHtml, excerpt } from "@lacspace/markdown";
|
|
@@ -7228,7 +7236,9 @@ function parseArgs(list) {
|
|
|
7228
7236
|
} else if (arg.startsWith("--mode=")) {
|
|
7229
7237
|
const m = arg.slice(7).toLowerCase();
|
|
7230
7238
|
a.mode = m === "dynamic" ? "dynamic" : "static";
|
|
7231
|
-
} else if (arg === "
|
|
7239
|
+
} else if (arg === "--recipe") a.recipe = next();
|
|
7240
|
+
else if (arg.startsWith("--recipe=")) a.recipe = arg.slice(9);
|
|
7241
|
+
else if (arg === "-h" || arg === "--help") a.help = true;
|
|
7232
7242
|
else if (arg.startsWith("--template=")) a.template = arg.slice(11);
|
|
7233
7243
|
else if (arg.startsWith("--theme=")) a.theme = arg.slice(8);
|
|
7234
7244
|
else if (arg.startsWith("--accent=")) a.theme = arg.slice(9);
|
|
@@ -7332,6 +7342,7 @@ ${c("bold", "Options")}
|
|
|
7332
7342
|
MongoDB/Redis backend (JWT auth + CRUD) + shared types
|
|
7333
7343
|
${c("dim", "(alias --dynamic; default is --static, a single Next.js app)")}
|
|
7334
7344
|
--with <a,b> Feature add-ons, comma-separated (alias --features)
|
|
7345
|
+
--recipe <key> Start from a curated stack (${RECIPES.map((r) => r.key).join(" | ")})
|
|
7335
7346
|
--theme <name|hex> Accent: a preset, a "#hex", or "from,to" (e.g. --theme lacspace)
|
|
7336
7347
|
--pm <npm|pnpm|yarn|bun> Package manager (default npm)
|
|
7337
7348
|
--no-install Skip installing dependencies
|
|
@@ -7340,9 +7351,13 @@ ${c("bold", "Options")}
|
|
|
7340
7351
|
-h, --help Show this help
|
|
7341
7352
|
|
|
7342
7353
|
${c("bold", "Feature add-ons")} ${c("dim", "(--with) \u2014 free & keyless, local by default")}
|
|
7343
|
-
${FEATURES.map((f) => ` ${f.key.padEnd(
|
|
7354
|
+
${FEATURES.map((f) => ` ${f.key.padEnd(11)} ${f.description}`).join("\n")}
|
|
7344
7355
|
${c("dim", "e.g. npx create-lacspace-app my-app --template saas --with ai-chat,rag")}
|
|
7345
7356
|
|
|
7357
|
+
${c("bold", "Recipes")} ${c("dim", "(--recipe) \u2014 a whole product in one command")}
|
|
7358
|
+
${RECIPES.map((r) => ` ${r.key.padEnd(14)} ${r.description}`).join("\n")}
|
|
7359
|
+
${c("dim", "e.g. npx create-lacspace-app my-app --recipe ai-saas")}
|
|
7360
|
+
|
|
7346
7361
|
${c("bold", "Themes")} ${c("dim", "(--theme)")}
|
|
7347
7362
|
${Object.keys(THEMES).join(" \xB7 ")}
|
|
7348
7363
|
${c("dim", 'or a custom colour: --theme "#ff6a00" \xB7 --theme "#0bb9d9,#7c3aed"')}
|
|
@@ -7719,11 +7734,16 @@ async function main() {
|
|
|
7719
7734
|
stdout.write(`
|
|
7720
7735
|
${c("bold", c("magenta", "\u25C6 create-lacspace-app"))} ${c("dim", "\u2014 a gorgeous Next.js starter, batteries wired")}
|
|
7721
7736
|
|
|
7737
|
+
`);
|
|
7738
|
+
const recipe = args.recipe ? RECIPES.find((r) => r.key === args.recipe.toLowerCase()) : void 0;
|
|
7739
|
+
if (args.recipe && !recipe) stdout.write(c("yellow", ` ! Unknown recipe "${args.recipe}" \u2014 ignoring. Try: ${RECIPES.map((r) => r.key).join(", ")}
|
|
7740
|
+
`));
|
|
7741
|
+
if (recipe) stdout.write(` ${c("green", "\u2714")} Recipe ${c("cyan", recipe.key)} ${c("dim", "\u2014 " + recipe.description)}
|
|
7722
7742
|
`);
|
|
7723
7743
|
let name = args.name;
|
|
7724
|
-
let templateKey = args.template;
|
|
7725
|
-
let mode = args.mode ?? "static";
|
|
7726
|
-
const featureKeys = [...args.features];
|
|
7744
|
+
let templateKey = args.template ?? recipe?.template;
|
|
7745
|
+
let mode = args.mode ?? recipe?.mode ?? "static";
|
|
7746
|
+
const featureKeys = [...recipe?.features ?? [], ...args.features];
|
|
7727
7747
|
if (!args.yes && stdin.isTTY) {
|
|
7728
7748
|
const rl = createInterface({ input: stdin, output: stdout });
|
|
7729
7749
|
try {
|
|
@@ -7739,7 +7759,7 @@ ${c("green", "?")} Template ${c("dim", "(1)")}: `)).trim() || "1";
|
|
|
7739
7759
|
const idx = /^\d+$/.test(ans) ? parseInt(ans, 10) - 1 : TEMPLATES.findIndex((t) => t.key === ans);
|
|
7740
7760
|
templateKey = TEMPLATES[idx]?.key ?? "personal";
|
|
7741
7761
|
}
|
|
7742
|
-
if (args.mode === void 0) {
|
|
7762
|
+
if (args.mode === void 0 && !recipe) {
|
|
7743
7763
|
stdout.write(`
|
|
7744
7764
|
What kind of app?
|
|
7745
7765
|
`);
|
|
@@ -7792,6 +7812,11 @@ ${c("green", "?")} Add features? ${c("dim", "(comma-separated numbers, or Enter
|
|
|
7792
7812
|
return;
|
|
7793
7813
|
}
|
|
7794
7814
|
const features = normalizeFeatures(featureKeys);
|
|
7815
|
+
if (mode === "static" && features.some((f) => f.requiresBackend)) {
|
|
7816
|
+
mode = "dynamic";
|
|
7817
|
+
stdout.write(` ${c("dim", "\u2191 a selected add-on needs a backend \u2014 building full-stack")}
|
|
7818
|
+
`);
|
|
7819
|
+
}
|
|
7795
7820
|
const files = buildFiles({ name: projectName, template, features, mode });
|
|
7796
7821
|
for (const [rel, content] of Object.entries(files)) {
|
|
7797
7822
|
const full = join(dir, rel);
|
|
@@ -7880,4 +7905,4 @@ if (invokedAsCli) {
|
|
|
7880
7905
|
});
|
|
7881
7906
|
}
|
|
7882
7907
|
|
|
7883
|
-
export { FEATURES, SECTIONS, TEMPLATES, applyFeatures, buildFiles, normalizeFeatures, resolveContext };
|
|
7908
|
+
export { FEATURES, RECIPES, SECTIONS, TEMPLATES, applyFeatures, buildFiles, normalizeFeatures, resolveContext };
|
package/dist/lib.cjs
CHANGED
|
@@ -33,14 +33,15 @@ var TEMPLATES = [
|
|
|
33
33
|
{ key: "marketplace", label: "Marketplace / commerce", description: "A real storefront wired to the Lacspace commerce packages \u2014 cart, checkout, tax, shipping, orders, invoices and Nepal payments.", accent: ["#0d9488", "#6366f1"], siteName: "LSBazaar", siteDescription: "A modern storefront \u2014 cart to checkout, wired end to end." }
|
|
34
34
|
];
|
|
35
35
|
function resolveContext(options = {}) {
|
|
36
|
-
const
|
|
36
|
+
const recipe = options.recipe ? RECIPES.find((r) => r.key === String(options.recipe).toLowerCase()) : void 0;
|
|
37
|
+
const base = TEMPLATES.find((t) => t.key === (options.template ?? recipe?.template)) ?? TEMPLATES[0];
|
|
37
38
|
const accent = resolveAccent(options.theme);
|
|
38
39
|
const template = accent ? { ...base, accent } : base;
|
|
39
40
|
const raw = options.name ?? "my-app";
|
|
40
41
|
const seg = raw.split(/[\\/]/).filter(Boolean).pop() ?? "my-app";
|
|
41
42
|
const name = seg.toLowerCase().replace(/[^a-z0-9-_]/g, "-").replace(/^-+|-+$/g, "") || "my-app";
|
|
42
|
-
const features = normalizeFeatures(options.features);
|
|
43
|
-
let mode = options.mode === "dynamic" ? "dynamic" : "static";
|
|
43
|
+
const features = normalizeFeatures([...recipe?.features ?? [], ...options.features ?? []]);
|
|
44
|
+
let mode = options.mode === "dynamic" ? "dynamic" : options.mode === "static" ? "static" : recipe?.mode ?? "static";
|
|
44
45
|
if (mode === "static" && features.some((f) => f.requiresBackend)) mode = "dynamic";
|
|
45
46
|
return { name, template, features, mode };
|
|
46
47
|
}
|
|
@@ -5256,6 +5257,13 @@ var faqSection = (ctx) => {
|
|
|
5256
5257
|
</div>
|
|
5257
5258
|
</section>`;
|
|
5258
5259
|
};
|
|
5260
|
+
var RECIPES = [
|
|
5261
|
+
{ key: "ai-saas", label: "AI SaaS", description: "SaaS landing + accounts + payments + a streaming AI chat + analytics.", template: "saas", mode: "dynamic", features: ["auth-pages", "payments", "ai-chat", "analytics"] },
|
|
5262
|
+
{ key: "store", label: "Online store", description: "E-commerce storefront + eSewa/Khalti checkout + transactional email + analytics.", template: "ecommerce", mode: "dynamic", features: ["payments", "email", "analytics"] },
|
|
5263
|
+
{ key: "blog", label: "Blog", description: "A blog with a Markdown content section, RSS/llms.txt and instant search.", template: "blog", features: ["content", "search"] },
|
|
5264
|
+
{ key: "internal-tool", label: "Internal tool", description: "An admin dashboard with accounts (2FA), analytics and email \u2014 full-stack.", template: "dashboard", mode: "dynamic", features: ["auth-pages", "analytics", "email"] },
|
|
5265
|
+
{ key: "docs-ai", label: "AI docs", description: "A documentation site with chat-with-your-docs RAG and instant search.", template: "docs", features: ["rag", "search"] }
|
|
5266
|
+
];
|
|
5259
5267
|
var contentLib = () => `import fs from "node:fs";
|
|
5260
5268
|
import path from "node:path";
|
|
5261
5269
|
import { parseFrontmatter, markdownToHtml, excerpt } from "@lacspace/markdown";
|
|
@@ -7232,7 +7240,9 @@ function parseArgs(list) {
|
|
|
7232
7240
|
} else if (arg.startsWith("--mode=")) {
|
|
7233
7241
|
const m = arg.slice(7).toLowerCase();
|
|
7234
7242
|
a.mode = m === "dynamic" ? "dynamic" : "static";
|
|
7235
|
-
} else if (arg === "
|
|
7243
|
+
} else if (arg === "--recipe") a.recipe = next();
|
|
7244
|
+
else if (arg.startsWith("--recipe=")) a.recipe = arg.slice(9);
|
|
7245
|
+
else if (arg === "-h" || arg === "--help") a.help = true;
|
|
7236
7246
|
else if (arg.startsWith("--template=")) a.template = arg.slice(11);
|
|
7237
7247
|
else if (arg.startsWith("--theme=")) a.theme = arg.slice(8);
|
|
7238
7248
|
else if (arg.startsWith("--accent=")) a.theme = arg.slice(9);
|
|
@@ -7336,6 +7346,7 @@ ${c("bold", "Options")}
|
|
|
7336
7346
|
MongoDB/Redis backend (JWT auth + CRUD) + shared types
|
|
7337
7347
|
${c("dim", "(alias --dynamic; default is --static, a single Next.js app)")}
|
|
7338
7348
|
--with <a,b> Feature add-ons, comma-separated (alias --features)
|
|
7349
|
+
--recipe <key> Start from a curated stack (${RECIPES.map((r) => r.key).join(" | ")})
|
|
7339
7350
|
--theme <name|hex> Accent: a preset, a "#hex", or "from,to" (e.g. --theme lacspace)
|
|
7340
7351
|
--pm <npm|pnpm|yarn|bun> Package manager (default npm)
|
|
7341
7352
|
--no-install Skip installing dependencies
|
|
@@ -7344,9 +7355,13 @@ ${c("bold", "Options")}
|
|
|
7344
7355
|
-h, --help Show this help
|
|
7345
7356
|
|
|
7346
7357
|
${c("bold", "Feature add-ons")} ${c("dim", "(--with) \u2014 free & keyless, local by default")}
|
|
7347
|
-
${FEATURES.map((f) => ` ${f.key.padEnd(
|
|
7358
|
+
${FEATURES.map((f) => ` ${f.key.padEnd(11)} ${f.description}`).join("\n")}
|
|
7348
7359
|
${c("dim", "e.g. npx create-lacspace-app my-app --template saas --with ai-chat,rag")}
|
|
7349
7360
|
|
|
7361
|
+
${c("bold", "Recipes")} ${c("dim", "(--recipe) \u2014 a whole product in one command")}
|
|
7362
|
+
${RECIPES.map((r) => ` ${r.key.padEnd(14)} ${r.description}`).join("\n")}
|
|
7363
|
+
${c("dim", "e.g. npx create-lacspace-app my-app --recipe ai-saas")}
|
|
7364
|
+
|
|
7350
7365
|
${c("bold", "Themes")} ${c("dim", "(--theme)")}
|
|
7351
7366
|
${Object.keys(THEMES).join(" \xB7 ")}
|
|
7352
7367
|
${c("dim", 'or a custom colour: --theme "#ff6a00" \xB7 --theme "#0bb9d9,#7c3aed"')}
|
|
@@ -7723,11 +7738,16 @@ async function main() {
|
|
|
7723
7738
|
process$1.stdout.write(`
|
|
7724
7739
|
${c("bold", c("magenta", "\u25C6 create-lacspace-app"))} ${c("dim", "\u2014 a gorgeous Next.js starter, batteries wired")}
|
|
7725
7740
|
|
|
7741
|
+
`);
|
|
7742
|
+
const recipe = args.recipe ? RECIPES.find((r) => r.key === args.recipe.toLowerCase()) : void 0;
|
|
7743
|
+
if (args.recipe && !recipe) process$1.stdout.write(c("yellow", ` ! Unknown recipe "${args.recipe}" \u2014 ignoring. Try: ${RECIPES.map((r) => r.key).join(", ")}
|
|
7744
|
+
`));
|
|
7745
|
+
if (recipe) process$1.stdout.write(` ${c("green", "\u2714")} Recipe ${c("cyan", recipe.key)} ${c("dim", "\u2014 " + recipe.description)}
|
|
7726
7746
|
`);
|
|
7727
7747
|
let name = args.name;
|
|
7728
|
-
let templateKey = args.template;
|
|
7729
|
-
let mode = args.mode ?? "static";
|
|
7730
|
-
const featureKeys = [...args.features];
|
|
7748
|
+
let templateKey = args.template ?? recipe?.template;
|
|
7749
|
+
let mode = args.mode ?? recipe?.mode ?? "static";
|
|
7750
|
+
const featureKeys = [...recipe?.features ?? [], ...args.features];
|
|
7731
7751
|
if (!args.yes && process$1.stdin.isTTY) {
|
|
7732
7752
|
const rl = promises.createInterface({ input: process$1.stdin, output: process$1.stdout });
|
|
7733
7753
|
try {
|
|
@@ -7743,7 +7763,7 @@ ${c("green", "?")} Template ${c("dim", "(1)")}: `)).trim() || "1";
|
|
|
7743
7763
|
const idx = /^\d+$/.test(ans) ? parseInt(ans, 10) - 1 : TEMPLATES.findIndex((t) => t.key === ans);
|
|
7744
7764
|
templateKey = TEMPLATES[idx]?.key ?? "personal";
|
|
7745
7765
|
}
|
|
7746
|
-
if (args.mode === void 0) {
|
|
7766
|
+
if (args.mode === void 0 && !recipe) {
|
|
7747
7767
|
process$1.stdout.write(`
|
|
7748
7768
|
What kind of app?
|
|
7749
7769
|
`);
|
|
@@ -7796,6 +7816,11 @@ ${c("green", "?")} Add features? ${c("dim", "(comma-separated numbers, or Enter
|
|
|
7796
7816
|
return;
|
|
7797
7817
|
}
|
|
7798
7818
|
const features = normalizeFeatures(featureKeys);
|
|
7819
|
+
if (mode === "static" && features.some((f) => f.requiresBackend)) {
|
|
7820
|
+
mode = "dynamic";
|
|
7821
|
+
process$1.stdout.write(` ${c("dim", "\u2191 a selected add-on needs a backend \u2014 building full-stack")}
|
|
7822
|
+
`);
|
|
7823
|
+
}
|
|
7799
7824
|
const files = buildFiles({ name: projectName, template, features, mode });
|
|
7800
7825
|
for (const [rel, content] of Object.entries(files)) {
|
|
7801
7826
|
const full = path.join(dir, rel);
|
|
@@ -7907,6 +7932,13 @@ function getFeature(key) {
|
|
|
7907
7932
|
const f = FEATURES.find((x) => x.key === key);
|
|
7908
7933
|
return f ? { ...f } : void 0;
|
|
7909
7934
|
}
|
|
7935
|
+
function listRecipes() {
|
|
7936
|
+
return RECIPES.map((r) => ({ ...r, features: [...r.features] }));
|
|
7937
|
+
}
|
|
7938
|
+
function getRecipe(key) {
|
|
7939
|
+
const r = RECIPES.find((x) => x.key === key);
|
|
7940
|
+
return r ? { ...r, features: [...r.features] } : void 0;
|
|
7941
|
+
}
|
|
7910
7942
|
function generateProject(options = {}) {
|
|
7911
7943
|
return buildFiles(resolveContext(options));
|
|
7912
7944
|
}
|
|
@@ -7924,9 +7956,11 @@ async function scaffold(options = {}) {
|
|
|
7924
7956
|
|
|
7925
7957
|
exports.generateProject = generateProject;
|
|
7926
7958
|
exports.getFeature = getFeature;
|
|
7959
|
+
exports.getRecipe = getRecipe;
|
|
7927
7960
|
exports.getSection = getSection;
|
|
7928
7961
|
exports.getTemplate = getTemplate;
|
|
7929
7962
|
exports.listFeatures = listFeatures;
|
|
7963
|
+
exports.listRecipes = listRecipes;
|
|
7930
7964
|
exports.listSections = listSections;
|
|
7931
7965
|
exports.listTemplates = listTemplates;
|
|
7932
7966
|
exports.scaffold = scaffold;
|
package/dist/lib.d.cts
CHANGED
|
@@ -104,6 +104,28 @@ interface GenerateOptions {
|
|
|
104
104
|
* changes.
|
|
105
105
|
*/
|
|
106
106
|
mode?: "static" | "dynamic";
|
|
107
|
+
/**
|
|
108
|
+
* A one-shot **recipe** (see {@link RECIPES}) — a curated bundle of template +
|
|
109
|
+
* mode + feature add-ons for a whole kind of product, e.g. `"ai-saas"` or
|
|
110
|
+
* `"store"`. Explicit `template`/`features`/`mode` still win and are merged on
|
|
111
|
+
* top. Omit for no recipe.
|
|
112
|
+
*/
|
|
113
|
+
recipe?: string;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* A one-command **recipe**: a curated template + mode + add-on stack for a whole
|
|
117
|
+
* kind of product. `npx create-lacspace-app my-app --recipe ai-saas`.
|
|
118
|
+
*/
|
|
119
|
+
interface RecipeDef {
|
|
120
|
+
key: string;
|
|
121
|
+
label: string;
|
|
122
|
+
description: string;
|
|
123
|
+
/** The template this recipe starts from. */
|
|
124
|
+
template: string;
|
|
125
|
+
/** Force full-stack when the stack needs a backend (add-ons can also force it). */
|
|
126
|
+
mode?: "static" | "dynamic";
|
|
127
|
+
/** The add-ons this recipe layers on. */
|
|
128
|
+
features: string[];
|
|
107
129
|
}
|
|
108
130
|
|
|
109
131
|
/** A project file map: relative path → file contents. */
|
|
@@ -131,6 +153,13 @@ declare function getSection(name: string): string | undefined;
|
|
|
131
153
|
declare function listFeatures(): FeatureDef[];
|
|
132
154
|
/** Look up one feature add-on's definition by key, or `undefined`. */
|
|
133
155
|
declare function getFeature(key: string): FeatureDef | undefined;
|
|
156
|
+
/**
|
|
157
|
+
* The built-in one-shot recipes (see {@link RecipeDef}) — curated template + mode +
|
|
158
|
+
* add-on bundles you can pass as `options.recipe` or request with `--recipe <key>`.
|
|
159
|
+
*/
|
|
160
|
+
declare function listRecipes(): RecipeDef[];
|
|
161
|
+
/** Look up one recipe's definition by key, or `undefined`. */
|
|
162
|
+
declare function getRecipe(key: string): RecipeDef | undefined;
|
|
134
163
|
/**
|
|
135
164
|
* Generate a full project as an in-memory file map (relative path → contents).
|
|
136
165
|
*
|
|
@@ -162,4 +191,4 @@ interface ScaffoldResult {
|
|
|
162
191
|
*/
|
|
163
192
|
declare function scaffold(options?: ScaffoldOptions): Promise<ScaffoldResult>;
|
|
164
193
|
|
|
165
|
-
export { type FeatureBackend, type FeatureDef, type FeatureRoute, type GenerateOptions, type ProjectFiles, type ScaffoldOptions, type ScaffoldResult, type TemplateDef, generateProject, getFeature, getSection, getTemplate, listFeatures, listSections, listTemplates, scaffold, templateKeys, templates };
|
|
194
|
+
export { type FeatureBackend, type FeatureDef, type FeatureRoute, type GenerateOptions, type ProjectFiles, type RecipeDef, type ScaffoldOptions, type ScaffoldResult, type TemplateDef, generateProject, getFeature, getRecipe, getSection, getTemplate, listFeatures, listRecipes, listSections, listTemplates, scaffold, templateKeys, templates };
|
package/dist/lib.d.ts
CHANGED
|
@@ -104,6 +104,28 @@ interface GenerateOptions {
|
|
|
104
104
|
* changes.
|
|
105
105
|
*/
|
|
106
106
|
mode?: "static" | "dynamic";
|
|
107
|
+
/**
|
|
108
|
+
* A one-shot **recipe** (see {@link RECIPES}) — a curated bundle of template +
|
|
109
|
+
* mode + feature add-ons for a whole kind of product, e.g. `"ai-saas"` or
|
|
110
|
+
* `"store"`. Explicit `template`/`features`/`mode` still win and are merged on
|
|
111
|
+
* top. Omit for no recipe.
|
|
112
|
+
*/
|
|
113
|
+
recipe?: string;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* A one-command **recipe**: a curated template + mode + add-on stack for a whole
|
|
117
|
+
* kind of product. `npx create-lacspace-app my-app --recipe ai-saas`.
|
|
118
|
+
*/
|
|
119
|
+
interface RecipeDef {
|
|
120
|
+
key: string;
|
|
121
|
+
label: string;
|
|
122
|
+
description: string;
|
|
123
|
+
/** The template this recipe starts from. */
|
|
124
|
+
template: string;
|
|
125
|
+
/** Force full-stack when the stack needs a backend (add-ons can also force it). */
|
|
126
|
+
mode?: "static" | "dynamic";
|
|
127
|
+
/** The add-ons this recipe layers on. */
|
|
128
|
+
features: string[];
|
|
107
129
|
}
|
|
108
130
|
|
|
109
131
|
/** A project file map: relative path → file contents. */
|
|
@@ -131,6 +153,13 @@ declare function getSection(name: string): string | undefined;
|
|
|
131
153
|
declare function listFeatures(): FeatureDef[];
|
|
132
154
|
/** Look up one feature add-on's definition by key, or `undefined`. */
|
|
133
155
|
declare function getFeature(key: string): FeatureDef | undefined;
|
|
156
|
+
/**
|
|
157
|
+
* The built-in one-shot recipes (see {@link RecipeDef}) — curated template + mode +
|
|
158
|
+
* add-on bundles you can pass as `options.recipe` or request with `--recipe <key>`.
|
|
159
|
+
*/
|
|
160
|
+
declare function listRecipes(): RecipeDef[];
|
|
161
|
+
/** Look up one recipe's definition by key, or `undefined`. */
|
|
162
|
+
declare function getRecipe(key: string): RecipeDef | undefined;
|
|
134
163
|
/**
|
|
135
164
|
* Generate a full project as an in-memory file map (relative path → contents).
|
|
136
165
|
*
|
|
@@ -162,4 +191,4 @@ interface ScaffoldResult {
|
|
|
162
191
|
*/
|
|
163
192
|
declare function scaffold(options?: ScaffoldOptions): Promise<ScaffoldResult>;
|
|
164
193
|
|
|
165
|
-
export { type FeatureBackend, type FeatureDef, type FeatureRoute, type GenerateOptions, type ProjectFiles, type ScaffoldOptions, type ScaffoldResult, type TemplateDef, generateProject, getFeature, getSection, getTemplate, listFeatures, listSections, listTemplates, scaffold, templateKeys, templates };
|
|
194
|
+
export { type FeatureBackend, type FeatureDef, type FeatureRoute, type GenerateOptions, type ProjectFiles, type RecipeDef, type ScaffoldOptions, type ScaffoldResult, type TemplateDef, generateProject, getFeature, getRecipe, getSection, getTemplate, listFeatures, listRecipes, listSections, listTemplates, scaffold, templateKeys, templates };
|
package/dist/lib.js
CHANGED
|
@@ -30,14 +30,15 @@ var TEMPLATES = [
|
|
|
30
30
|
{ key: "marketplace", label: "Marketplace / commerce", description: "A real storefront wired to the Lacspace commerce packages \u2014 cart, checkout, tax, shipping, orders, invoices and Nepal payments.", accent: ["#0d9488", "#6366f1"], siteName: "LSBazaar", siteDescription: "A modern storefront \u2014 cart to checkout, wired end to end." }
|
|
31
31
|
];
|
|
32
32
|
function resolveContext(options = {}) {
|
|
33
|
-
const
|
|
33
|
+
const recipe = options.recipe ? RECIPES.find((r) => r.key === String(options.recipe).toLowerCase()) : void 0;
|
|
34
|
+
const base = TEMPLATES.find((t) => t.key === (options.template ?? recipe?.template)) ?? TEMPLATES[0];
|
|
34
35
|
const accent = resolveAccent(options.theme);
|
|
35
36
|
const template = accent ? { ...base, accent } : base;
|
|
36
37
|
const raw = options.name ?? "my-app";
|
|
37
38
|
const seg = raw.split(/[\\/]/).filter(Boolean).pop() ?? "my-app";
|
|
38
39
|
const name = seg.toLowerCase().replace(/[^a-z0-9-_]/g, "-").replace(/^-+|-+$/g, "") || "my-app";
|
|
39
|
-
const features = normalizeFeatures(options.features);
|
|
40
|
-
let mode = options.mode === "dynamic" ? "dynamic" : "static";
|
|
40
|
+
const features = normalizeFeatures([...recipe?.features ?? [], ...options.features ?? []]);
|
|
41
|
+
let mode = options.mode === "dynamic" ? "dynamic" : options.mode === "static" ? "static" : recipe?.mode ?? "static";
|
|
41
42
|
if (mode === "static" && features.some((f) => f.requiresBackend)) mode = "dynamic";
|
|
42
43
|
return { name, template, features, mode };
|
|
43
44
|
}
|
|
@@ -5253,6 +5254,13 @@ var faqSection = (ctx) => {
|
|
|
5253
5254
|
</div>
|
|
5254
5255
|
</section>`;
|
|
5255
5256
|
};
|
|
5257
|
+
var RECIPES = [
|
|
5258
|
+
{ key: "ai-saas", label: "AI SaaS", description: "SaaS landing + accounts + payments + a streaming AI chat + analytics.", template: "saas", mode: "dynamic", features: ["auth-pages", "payments", "ai-chat", "analytics"] },
|
|
5259
|
+
{ key: "store", label: "Online store", description: "E-commerce storefront + eSewa/Khalti checkout + transactional email + analytics.", template: "ecommerce", mode: "dynamic", features: ["payments", "email", "analytics"] },
|
|
5260
|
+
{ key: "blog", label: "Blog", description: "A blog with a Markdown content section, RSS/llms.txt and instant search.", template: "blog", features: ["content", "search"] },
|
|
5261
|
+
{ key: "internal-tool", label: "Internal tool", description: "An admin dashboard with accounts (2FA), analytics and email \u2014 full-stack.", template: "dashboard", mode: "dynamic", features: ["auth-pages", "analytics", "email"] },
|
|
5262
|
+
{ key: "docs-ai", label: "AI docs", description: "A documentation site with chat-with-your-docs RAG and instant search.", template: "docs", features: ["rag", "search"] }
|
|
5263
|
+
];
|
|
5256
5264
|
var contentLib = () => `import fs from "node:fs";
|
|
5257
5265
|
import path from "node:path";
|
|
5258
5266
|
import { parseFrontmatter, markdownToHtml, excerpt } from "@lacspace/markdown";
|
|
@@ -7229,7 +7237,9 @@ function parseArgs(list) {
|
|
|
7229
7237
|
} else if (arg.startsWith("--mode=")) {
|
|
7230
7238
|
const m = arg.slice(7).toLowerCase();
|
|
7231
7239
|
a.mode = m === "dynamic" ? "dynamic" : "static";
|
|
7232
|
-
} else if (arg === "
|
|
7240
|
+
} else if (arg === "--recipe") a.recipe = next();
|
|
7241
|
+
else if (arg.startsWith("--recipe=")) a.recipe = arg.slice(9);
|
|
7242
|
+
else if (arg === "-h" || arg === "--help") a.help = true;
|
|
7233
7243
|
else if (arg.startsWith("--template=")) a.template = arg.slice(11);
|
|
7234
7244
|
else if (arg.startsWith("--theme=")) a.theme = arg.slice(8);
|
|
7235
7245
|
else if (arg.startsWith("--accent=")) a.theme = arg.slice(9);
|
|
@@ -7333,6 +7343,7 @@ ${c("bold", "Options")}
|
|
|
7333
7343
|
MongoDB/Redis backend (JWT auth + CRUD) + shared types
|
|
7334
7344
|
${c("dim", "(alias --dynamic; default is --static, a single Next.js app)")}
|
|
7335
7345
|
--with <a,b> Feature add-ons, comma-separated (alias --features)
|
|
7346
|
+
--recipe <key> Start from a curated stack (${RECIPES.map((r) => r.key).join(" | ")})
|
|
7336
7347
|
--theme <name|hex> Accent: a preset, a "#hex", or "from,to" (e.g. --theme lacspace)
|
|
7337
7348
|
--pm <npm|pnpm|yarn|bun> Package manager (default npm)
|
|
7338
7349
|
--no-install Skip installing dependencies
|
|
@@ -7341,9 +7352,13 @@ ${c("bold", "Options")}
|
|
|
7341
7352
|
-h, --help Show this help
|
|
7342
7353
|
|
|
7343
7354
|
${c("bold", "Feature add-ons")} ${c("dim", "(--with) \u2014 free & keyless, local by default")}
|
|
7344
|
-
${FEATURES.map((f) => ` ${f.key.padEnd(
|
|
7355
|
+
${FEATURES.map((f) => ` ${f.key.padEnd(11)} ${f.description}`).join("\n")}
|
|
7345
7356
|
${c("dim", "e.g. npx create-lacspace-app my-app --template saas --with ai-chat,rag")}
|
|
7346
7357
|
|
|
7358
|
+
${c("bold", "Recipes")} ${c("dim", "(--recipe) \u2014 a whole product in one command")}
|
|
7359
|
+
${RECIPES.map((r) => ` ${r.key.padEnd(14)} ${r.description}`).join("\n")}
|
|
7360
|
+
${c("dim", "e.g. npx create-lacspace-app my-app --recipe ai-saas")}
|
|
7361
|
+
|
|
7347
7362
|
${c("bold", "Themes")} ${c("dim", "(--theme)")}
|
|
7348
7363
|
${Object.keys(THEMES).join(" \xB7 ")}
|
|
7349
7364
|
${c("dim", 'or a custom colour: --theme "#ff6a00" \xB7 --theme "#0bb9d9,#7c3aed"')}
|
|
@@ -7720,11 +7735,16 @@ async function main() {
|
|
|
7720
7735
|
stdout.write(`
|
|
7721
7736
|
${c("bold", c("magenta", "\u25C6 create-lacspace-app"))} ${c("dim", "\u2014 a gorgeous Next.js starter, batteries wired")}
|
|
7722
7737
|
|
|
7738
|
+
`);
|
|
7739
|
+
const recipe = args.recipe ? RECIPES.find((r) => r.key === args.recipe.toLowerCase()) : void 0;
|
|
7740
|
+
if (args.recipe && !recipe) stdout.write(c("yellow", ` ! Unknown recipe "${args.recipe}" \u2014 ignoring. Try: ${RECIPES.map((r) => r.key).join(", ")}
|
|
7741
|
+
`));
|
|
7742
|
+
if (recipe) stdout.write(` ${c("green", "\u2714")} Recipe ${c("cyan", recipe.key)} ${c("dim", "\u2014 " + recipe.description)}
|
|
7723
7743
|
`);
|
|
7724
7744
|
let name = args.name;
|
|
7725
|
-
let templateKey = args.template;
|
|
7726
|
-
let mode = args.mode ?? "static";
|
|
7727
|
-
const featureKeys = [...args.features];
|
|
7745
|
+
let templateKey = args.template ?? recipe?.template;
|
|
7746
|
+
let mode = args.mode ?? recipe?.mode ?? "static";
|
|
7747
|
+
const featureKeys = [...recipe?.features ?? [], ...args.features];
|
|
7728
7748
|
if (!args.yes && stdin.isTTY) {
|
|
7729
7749
|
const rl = createInterface({ input: stdin, output: stdout });
|
|
7730
7750
|
try {
|
|
@@ -7740,7 +7760,7 @@ ${c("green", "?")} Template ${c("dim", "(1)")}: `)).trim() || "1";
|
|
|
7740
7760
|
const idx = /^\d+$/.test(ans) ? parseInt(ans, 10) - 1 : TEMPLATES.findIndex((t) => t.key === ans);
|
|
7741
7761
|
templateKey = TEMPLATES[idx]?.key ?? "personal";
|
|
7742
7762
|
}
|
|
7743
|
-
if (args.mode === void 0) {
|
|
7763
|
+
if (args.mode === void 0 && !recipe) {
|
|
7744
7764
|
stdout.write(`
|
|
7745
7765
|
What kind of app?
|
|
7746
7766
|
`);
|
|
@@ -7793,6 +7813,11 @@ ${c("green", "?")} Add features? ${c("dim", "(comma-separated numbers, or Enter
|
|
|
7793
7813
|
return;
|
|
7794
7814
|
}
|
|
7795
7815
|
const features = normalizeFeatures(featureKeys);
|
|
7816
|
+
if (mode === "static" && features.some((f) => f.requiresBackend)) {
|
|
7817
|
+
mode = "dynamic";
|
|
7818
|
+
stdout.write(` ${c("dim", "\u2191 a selected add-on needs a backend \u2014 building full-stack")}
|
|
7819
|
+
`);
|
|
7820
|
+
}
|
|
7796
7821
|
const files = buildFiles({ name: projectName, template, features, mode });
|
|
7797
7822
|
for (const [rel, content] of Object.entries(files)) {
|
|
7798
7823
|
const full = join(dir, rel);
|
|
@@ -7904,6 +7929,13 @@ function getFeature(key) {
|
|
|
7904
7929
|
const f = FEATURES.find((x) => x.key === key);
|
|
7905
7930
|
return f ? { ...f } : void 0;
|
|
7906
7931
|
}
|
|
7932
|
+
function listRecipes() {
|
|
7933
|
+
return RECIPES.map((r) => ({ ...r, features: [...r.features] }));
|
|
7934
|
+
}
|
|
7935
|
+
function getRecipe(key) {
|
|
7936
|
+
const r = RECIPES.find((x) => x.key === key);
|
|
7937
|
+
return r ? { ...r, features: [...r.features] } : void 0;
|
|
7938
|
+
}
|
|
7907
7939
|
function generateProject(options = {}) {
|
|
7908
7940
|
return buildFiles(resolveContext(options));
|
|
7909
7941
|
}
|
|
@@ -7919,4 +7951,4 @@ async function scaffold(options = {}) {
|
|
|
7919
7951
|
return { dir: target, files: Object.keys(files) };
|
|
7920
7952
|
}
|
|
7921
7953
|
|
|
7922
|
-
export { generateProject, getFeature, getSection, getTemplate, listFeatures, listSections, listTemplates, scaffold, templateKeys, templates };
|
|
7954
|
+
export { generateProject, getFeature, getRecipe, getSection, getTemplate, listFeatures, listRecipes, listSections, listTemplates, scaffold, templateKeys, templates };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-lacspace-app",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"description": "Scaffold a beautiful, production-ready Next.js app from a Lacspace template — portfolio, business, e-commerce, SaaS, blog, docs, dashboard, restaurant or marketplace — pre-wired with SEO, security headers, sitemap and robots. Use it as a CLI (like create-next-app, but you start gorgeous) or import it as a library to generate projects programmatically.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|