create-lacspace-app 1.0.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/LICENSE +51 -0
- package/README.md +67 -0
- package/dist/index.js +456 -0
- package/package.json +17 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
Lacspace Free Licence
|
|
2
|
+
Version 1.0, August 2026
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2026 Lacspace
|
|
5
|
+
|
|
6
|
+
PREAMBLE
|
|
7
|
+
|
|
8
|
+
This software is published by Lacspace under the Lacspace Free Licence — a free,
|
|
9
|
+
permissive licence that lets you use this software for any purpose, including in
|
|
10
|
+
commercial products and services, at no cost. It grants the same freedoms as
|
|
11
|
+
common permissive open-source licences; the only condition is that this notice
|
|
12
|
+
travels with the software. The canonical, always-current text of this licence is
|
|
13
|
+
maintained at https://lacspace.com/licenses/lacspace-free-1.0
|
|
14
|
+
|
|
15
|
+
GRANT OF RIGHTS
|
|
16
|
+
|
|
17
|
+
Permission is hereby granted, free of charge, to any person or organisation
|
|
18
|
+
obtaining a copy of this software and its associated documentation and data files
|
|
19
|
+
(the "Software"), to deal in the Software without restriction, including without
|
|
20
|
+
limitation the rights to use, copy, modify, merge, publish, distribute,
|
|
21
|
+
sublicense, and/or sell copies of the Software, and to permit persons to whom the
|
|
22
|
+
Software is furnished to do so, subject to the conditions below. These rights are
|
|
23
|
+
granted for any purpose, personal or commercial, and are perpetual, worldwide,
|
|
24
|
+
non-exclusive, and royalty-free.
|
|
25
|
+
|
|
26
|
+
CONDITIONS
|
|
27
|
+
|
|
28
|
+
The above copyright notice, this permission notice, and the name of this licence
|
|
29
|
+
("Lacspace Free Licence") shall be included in all copies or substantial portions
|
|
30
|
+
of the Software.
|
|
31
|
+
|
|
32
|
+
TRADEMARKS
|
|
33
|
+
|
|
34
|
+
This licence does not grant permission to use the trade names, trademarks, service
|
|
35
|
+
marks, logos, or product names of Lacspace, except as required to reproduce the
|
|
36
|
+
notice above or to describe the origin of the Software in a truthful manner.
|
|
37
|
+
|
|
38
|
+
DISCLAIMER OF WARRANTY AND LIMITATION OF LIABILITY
|
|
39
|
+
|
|
40
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
41
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
42
|
+
FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
43
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN
|
|
44
|
+
AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION
|
|
45
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
The Lacspace Free Licence is a source-available, permissive licence and is not (as
|
|
50
|
+
of this version) an OSI-approved licence. In substance it grants the same freedoms
|
|
51
|
+
as the MIT Licence. Learn more at https://lacspace.com/licenses
|
package/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# create-lacspace-app
|
|
4
|
+
|
|
5
|
+
**Scaffold a beautiful, production-ready Next.js app in one command — pick a template, start gorgeous.**
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/create-lacspace-app)
|
|
8
|
+
[](https://github.com/lacspace/npm-packages/blob/main/LICENSE)
|
|
9
|
+
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
> `create-next-app` gives you a blank page. This gives you a **finished-looking app** — a polished template, styled with Tailwind, already wired with the Lacspace libraries (SEO metadata + JSON-LD, security headers, sitemap & robots). Choose your kind of site and you're running in seconds.
|
|
13
|
+
|
|
14
|
+
## Use it
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm create lacspace-app@latest my-app
|
|
18
|
+
# or
|
|
19
|
+
npx create-lacspace-app my-app --template business
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
It asks for a project name and a template, scaffolds the project, installs dependencies and initialises git.
|
|
23
|
+
|
|
24
|
+
## Templates
|
|
25
|
+
|
|
26
|
+
| Key | What you get |
|
|
27
|
+
| --- | --- |
|
|
28
|
+
| **personal** | A sleek personal / developer portfolio with a projects grid |
|
|
29
|
+
| **business** | A professional company / agency site with services + CTA |
|
|
30
|
+
| **ecommerce** | A modern storefront home with a featured product grid |
|
|
31
|
+
| **saas** | A high-converting SaaS landing page with features + pricing |
|
|
32
|
+
|
|
33
|
+
Every template is a real Next.js 15 App Router + Tailwind v4 project — dark, modern, responsive, with a gradient accent you can theme.
|
|
34
|
+
|
|
35
|
+
## Batteries already wired
|
|
36
|
+
|
|
37
|
+
- **`lib/site.ts`** — one [`@lacspace/seo`](https://www.npmjs.com/package/@lacspace/seo) `defineSite()` config drives your `<title>`, canonical, Open Graph, Twitter card and JSON-LD
|
|
38
|
+
- **`next.config.mjs`** — hardened security headers via [`@lacspace/headers`](https://www.npmjs.com/package/@lacspace/headers)
|
|
39
|
+
- **`app/robots.txt` + `app/sitemap.xml`** — generated from your site config with [`@lacspace/robots`](https://www.npmjs.com/package/@lacspace/robots) & [`@lacspace/sitemap`](https://www.npmjs.com/package/@lacspace/sitemap)
|
|
40
|
+
|
|
41
|
+
Edit `lib/site.ts` and `app/page.tsx` and you're off.
|
|
42
|
+
|
|
43
|
+
## Options
|
|
44
|
+
|
|
45
|
+
| Flag | Meaning |
|
|
46
|
+
| --- | --- |
|
|
47
|
+
| `-t, --template <key>` | `personal` \| `business` \| `ecommerce` \| `saas` |
|
|
48
|
+
| `--pm <npm\|pnpm\|yarn\|bun>` | package manager (default npm) |
|
|
49
|
+
| `--no-install` | skip installing dependencies |
|
|
50
|
+
| `--no-git` | skip git init |
|
|
51
|
+
| `-y, --yes` | accept defaults (needs a project name) |
|
|
52
|
+
|
|
53
|
+
## Licensing
|
|
54
|
+
|
|
55
|
+
Free under the **[Lacspace Free Licence](https://lacspace.com/licenses/lacspace-free-1.0)** — MIT-equivalent freedoms. Use it in personal and commercial projects at no cost; just keep the notice. The apps you generate are yours. See the **[Lacspace Licence Centre](https://lacspace.com/licenses)**.
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
<div align="center">
|
|
60
|
+
|
|
61
|
+
**Part of the Lacspace ecosystem — zero-dependency, isomorphic TypeScript packages.**
|
|
62
|
+
|
|
63
|
+
[All packages ↗](https://lacspace.com/packages) · [npm org ↗](https://www.npmjs.com/org/lacspace) · [Licence Centre ↗](https://lacspace.com/licenses) · [GitHub ↗](https://github.com/lacspace/npm-packages)
|
|
64
|
+
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div align="center"><sub>Built with care by <a href="https://lacspace.com">Lacspace</a> · Lacspace Free Licence · <a href="https://github.com/lacspace/npm-packages">source</a></sub></div>
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readdirSync, mkdirSync, writeFileSync } from 'fs';
|
|
3
|
+
import { resolve, basename, join, dirname } from 'path';
|
|
4
|
+
import { createInterface } from 'readline/promises';
|
|
5
|
+
import { stdout, exit, argv, stdin, cwd } from 'process';
|
|
6
|
+
import { spawnSync } from 'child_process';
|
|
7
|
+
|
|
8
|
+
var C = {
|
|
9
|
+
reset: "\x1B[0m",
|
|
10
|
+
bold: "\x1B[1m",
|
|
11
|
+
dim: "\x1B[2m",
|
|
12
|
+
green: "\x1B[32m",
|
|
13
|
+
cyan: "\x1B[36m",
|
|
14
|
+
yellow: "\x1B[33m",
|
|
15
|
+
red: "\x1B[31m",
|
|
16
|
+
magenta: "\x1B[35m"
|
|
17
|
+
};
|
|
18
|
+
var c = (k, s) => `${C[k]}${s}${C.reset}`;
|
|
19
|
+
var TEMPLATES = [
|
|
20
|
+
{ key: "personal", label: "Personal portfolio", description: "A sleek personal / developer portfolio with projects and contact.", accent: ["#6366f1", "#a855f7"], siteName: "Your Name", siteDescription: "Developer, designer & maker. Selected work and writing." },
|
|
21
|
+
{ key: "business", label: "Business site", description: "A professional company / agency site with services and a CTA.", accent: ["#2563eb", "#06b6d4"], siteName: "Acme Studio", siteDescription: "We design and build digital products that grow businesses." },
|
|
22
|
+
{ key: "ecommerce", label: "E-commerce storefront", description: "A modern product storefront home with a featured grid.", accent: ["#0d9488", "#84cc16"], siteName: "Acme Store", siteDescription: "Beautiful things, thoughtfully made. Free shipping worldwide." },
|
|
23
|
+
{ key: "saas", label: "SaaS landing", description: "A high-converting SaaS landing page with features and pricing.", accent: ["#7c3aed", "#ec4899"], siteName: "Acme Cloud", siteDescription: "The all-in-one platform your team will love. Ship faster." }
|
|
24
|
+
];
|
|
25
|
+
var pkgJson = (ctx) => JSON.stringify({
|
|
26
|
+
name: ctx.name,
|
|
27
|
+
version: "0.1.0",
|
|
28
|
+
private: true,
|
|
29
|
+
scripts: { dev: "next dev", build: "next build", start: "next start", lint: "next lint" },
|
|
30
|
+
dependencies: {
|
|
31
|
+
next: "^15.1.0",
|
|
32
|
+
react: "^19.0.0",
|
|
33
|
+
"react-dom": "^19.0.0",
|
|
34
|
+
"@lacspace/seo": "^1.5.0",
|
|
35
|
+
"@lacspace/headers": "^1.1.1",
|
|
36
|
+
"@lacspace/robots": "^1.2.0",
|
|
37
|
+
"@lacspace/sitemap": "^1.1.0"
|
|
38
|
+
},
|
|
39
|
+
devDependencies: {
|
|
40
|
+
typescript: "^5.7.0",
|
|
41
|
+
"@types/node": "^22.10.0",
|
|
42
|
+
"@types/react": "^19.0.0",
|
|
43
|
+
"@types/react-dom": "^19.0.0",
|
|
44
|
+
tailwindcss: "^4.0.0",
|
|
45
|
+
"@tailwindcss/postcss": "^4.0.0",
|
|
46
|
+
postcss: "^8.4.49"
|
|
47
|
+
}
|
|
48
|
+
}, null, 2) + "\n";
|
|
49
|
+
var tsconfig = () => JSON.stringify({
|
|
50
|
+
compilerOptions: {
|
|
51
|
+
target: "ES2022",
|
|
52
|
+
lib: ["dom", "dom.iterable", "esnext"],
|
|
53
|
+
allowJs: true,
|
|
54
|
+
skipLibCheck: true,
|
|
55
|
+
strict: true,
|
|
56
|
+
noEmit: true,
|
|
57
|
+
esModuleInterop: true,
|
|
58
|
+
module: "esnext",
|
|
59
|
+
moduleResolution: "bundler",
|
|
60
|
+
resolveJsonModule: true,
|
|
61
|
+
isolatedModules: true,
|
|
62
|
+
jsx: "preserve",
|
|
63
|
+
incremental: true,
|
|
64
|
+
plugins: [{ name: "next" }],
|
|
65
|
+
paths: { "@/*": ["./*"] }
|
|
66
|
+
},
|
|
67
|
+
include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
68
|
+
exclude: ["node_modules"]
|
|
69
|
+
}, null, 2) + "\n";
|
|
70
|
+
var nextConfig = () => `import { toNextHeaders } from "@lacspace/headers";
|
|
71
|
+
|
|
72
|
+
/** @type {import('next').NextConfig} */
|
|
73
|
+
const nextConfig = {
|
|
74
|
+
async headers() {
|
|
75
|
+
// Hardened security headers (HSTS, CSP, X-Frame-Options, \u2026) from @lacspace/headers
|
|
76
|
+
return toNextHeaders();
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export default nextConfig;
|
|
81
|
+
`;
|
|
82
|
+
var postcss = () => `export default {
|
|
83
|
+
plugins: { "@tailwindcss/postcss": {} },
|
|
84
|
+
};
|
|
85
|
+
`;
|
|
86
|
+
var gitignore = () => `node_modules
|
|
87
|
+
.next
|
|
88
|
+
out
|
|
89
|
+
.env*
|
|
90
|
+
!.env.example
|
|
91
|
+
*.log
|
|
92
|
+
.DS_Store
|
|
93
|
+
`;
|
|
94
|
+
var globalsCss = (ctx) => `@import "tailwindcss";
|
|
95
|
+
|
|
96
|
+
:root {
|
|
97
|
+
--accent-from: ${ctx.template.accent[0]};
|
|
98
|
+
--accent-to: ${ctx.template.accent[1]};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
html { scroll-behavior: smooth; }
|
|
102
|
+
body { background: #0a0a0f; color: #e5e7eb; -webkit-font-smoothing: antialiased; }
|
|
103
|
+
|
|
104
|
+
.gradient-text {
|
|
105
|
+
background: linear-gradient(120deg, var(--accent-from), var(--accent-to));
|
|
106
|
+
-webkit-background-clip: text; background-clip: text; color: transparent;
|
|
107
|
+
}
|
|
108
|
+
.gradient-bg { background: linear-gradient(120deg, var(--accent-from), var(--accent-to)); }
|
|
109
|
+
`;
|
|
110
|
+
var siteTs = (ctx) => `import { defineSite } from "@lacspace/seo";
|
|
111
|
+
|
|
112
|
+
/** Your site's SEO configuration \u2014 set once, used everywhere. */
|
|
113
|
+
export const site = defineSite({
|
|
114
|
+
name: ${JSON.stringify(ctx.template.siteName)},
|
|
115
|
+
url: "https://example.com",
|
|
116
|
+
description: ${JSON.stringify(ctx.template.siteDescription)},
|
|
117
|
+
// twitter: "yourhandle",
|
|
118
|
+
// ogImage: "/og",
|
|
119
|
+
});
|
|
120
|
+
`;
|
|
121
|
+
var layout = (ctx) => `import type { Metadata } from "next";
|
|
122
|
+
import { site } from "@/lib/site";
|
|
123
|
+
import "./globals.css";
|
|
124
|
+
|
|
125
|
+
export const metadata: Metadata = site.meta({ title: ${JSON.stringify(ctx.template.siteName)} });
|
|
126
|
+
|
|
127
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
128
|
+
return (
|
|
129
|
+
<html lang="en">
|
|
130
|
+
<body>
|
|
131
|
+
{children}
|
|
132
|
+
<script
|
|
133
|
+
type="application/ld+json"
|
|
134
|
+
dangerouslySetInnerHTML={{ __html: JSON.stringify(site.rootJsonLd()) }}
|
|
135
|
+
/>
|
|
136
|
+
</body>
|
|
137
|
+
</html>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
`;
|
|
141
|
+
var robotsTs = () => `import { robotsForSite } from "@lacspace/robots";
|
|
142
|
+
import { site } from "@/lib/site";
|
|
143
|
+
|
|
144
|
+
export function GET() {
|
|
145
|
+
return new Response(robotsForSite(site.config), { headers: { "content-type": "text/plain" } });
|
|
146
|
+
}
|
|
147
|
+
`;
|
|
148
|
+
var sitemapTs = () => `import { sitemapForSite } from "@lacspace/sitemap";
|
|
149
|
+
import { site } from "@/lib/site";
|
|
150
|
+
|
|
151
|
+
export function GET() {
|
|
152
|
+
const xml = sitemapForSite(site.config, ["/", "/about", "/contact"]);
|
|
153
|
+
return new Response(xml, { headers: { "content-type": "application/xml" } });
|
|
154
|
+
}
|
|
155
|
+
`;
|
|
156
|
+
var readme = (ctx) => `# ${ctx.name}
|
|
157
|
+
|
|
158
|
+
A Next.js app scaffolded with [create-lacspace-app](https://www.npmjs.com/package/create-lacspace-app) \u2014 template: **${ctx.template.label}**.
|
|
159
|
+
|
|
160
|
+
Pre-wired with the Lacspace libraries:
|
|
161
|
+
- [\`@lacspace/seo\`](https://www.npmjs.com/package/@lacspace/seo) \u2014 metadata + JSON-LD via \`lib/site.ts\` (edit it once, it flows everywhere)
|
|
162
|
+
- [\`@lacspace/headers\`](https://www.npmjs.com/package/@lacspace/headers) \u2014 security headers in \`next.config.mjs\`
|
|
163
|
+
- [\`@lacspace/robots\`](https://www.npmjs.com/package/@lacspace/robots) + [\`@lacspace/sitemap\`](https://www.npmjs.com/package/@lacspace/sitemap) \u2014 \`app/robots.txt\` & \`app/sitemap.xml\`
|
|
164
|
+
|
|
165
|
+
## Getting started
|
|
166
|
+
|
|
167
|
+
\`\`\`bash
|
|
168
|
+
npm install
|
|
169
|
+
npm run dev
|
|
170
|
+
\`\`\`
|
|
171
|
+
|
|
172
|
+
Open [http://localhost:3000](http://localhost:3000). Edit \`app/page.tsx\` and \`lib/site.ts\`.
|
|
173
|
+
|
|
174
|
+
Built with [Lacspace](https://lacspace.com/packages).
|
|
175
|
+
`;
|
|
176
|
+
var NAV = (name, links) => `<header className="sticky top-0 z-40 backdrop-blur border-b border-white/10">
|
|
177
|
+
<nav className="mx-auto flex max-w-6xl items-center justify-between px-6 py-4">
|
|
178
|
+
<span className="text-lg font-black gradient-text">${name}</span>
|
|
179
|
+
<div className="hidden gap-8 text-sm text-white/60 sm:flex">
|
|
180
|
+
${links.map((l) => `<a href="#${l.toLowerCase()}" className="hover:text-white">${l}</a>`).join("\n ")}
|
|
181
|
+
</div>
|
|
182
|
+
</nav>
|
|
183
|
+
</header>`;
|
|
184
|
+
var FOOTER = (name) => `<footer className="border-t border-white/10 py-10 text-center text-sm text-white/40">
|
|
185
|
+
\xA9 {new Date().getFullYear()} ${name}. Built with{" "}
|
|
186
|
+
<a href="https://lacspace.com/packages" className="text-white/70 hover:text-white">Lacspace</a>.
|
|
187
|
+
</footer>`;
|
|
188
|
+
function homePage(ctx) {
|
|
189
|
+
const n = ctx.template.siteName;
|
|
190
|
+
const shell = (inner) => `export default function Home() {
|
|
191
|
+
return (
|
|
192
|
+
<main className="min-h-screen">
|
|
193
|
+
${inner}
|
|
194
|
+
</main>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
`;
|
|
198
|
+
if (ctx.template.key === "personal") {
|
|
199
|
+
return shell(`${NAV(n, ["Work", "About", "Contact"])}
|
|
200
|
+
<section className="mx-auto max-w-3xl px-6 py-28 text-center">
|
|
201
|
+
<p className="mb-4 text-sm font-semibold uppercase tracking-widest text-white/40">Portfolio</p>
|
|
202
|
+
<h1 className="text-5xl font-black leading-tight sm:text-7xl">Hi, I'm <span className="gradient-text">${n}</span>.</h1>
|
|
203
|
+
<p className="mx-auto mt-6 max-w-xl text-lg text-white/60">${ctx.template.siteDescription}</p>
|
|
204
|
+
<div className="mt-10 flex justify-center gap-4">
|
|
205
|
+
<a href="#work" className="gradient-bg rounded-full px-6 py-3 font-semibold text-black">View my work</a>
|
|
206
|
+
<a href="#contact" className="rounded-full border border-white/20 px-6 py-3 font-semibold hover:bg-white/5">Get in touch</a>
|
|
207
|
+
</div>
|
|
208
|
+
</section>
|
|
209
|
+
<section id="work" className="mx-auto max-w-6xl px-6 py-16">
|
|
210
|
+
<h2 className="mb-10 text-3xl font-bold">Selected work</h2>
|
|
211
|
+
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
|
212
|
+
{[1, 2, 3, 4, 5, 6].map((i) => (
|
|
213
|
+
<div key={i} className="group rounded-2xl border border-white/10 bg-white/[0.02] p-6 transition hover:-translate-y-1 hover:border-white/20">
|
|
214
|
+
<div className="mb-4 h-32 rounded-xl gradient-bg opacity-80" />
|
|
215
|
+
<h3 className="font-semibold">Project {i}</h3>
|
|
216
|
+
<p className="mt-1 text-sm text-white/50">A short description of what you built and the impact it had.</p>
|
|
217
|
+
</div>
|
|
218
|
+
))}
|
|
219
|
+
</div>
|
|
220
|
+
</section>
|
|
221
|
+
${FOOTER(n)}`);
|
|
222
|
+
}
|
|
223
|
+
if (ctx.template.key === "business") {
|
|
224
|
+
return shell(`${NAV(n, ["Services", "Work", "Contact"])}
|
|
225
|
+
<section className="mx-auto max-w-4xl px-6 py-28 text-center">
|
|
226
|
+
<h1 className="text-5xl font-black leading-tight sm:text-6xl">We build products <span className="gradient-text">that grow businesses</span>.</h1>
|
|
227
|
+
<p className="mx-auto mt-6 max-w-2xl text-lg text-white/60">${ctx.template.siteDescription}</p>
|
|
228
|
+
<a href="#contact" className="mt-10 inline-block gradient-bg rounded-full px-8 py-3 font-semibold text-black">Start a project</a>
|
|
229
|
+
</section>
|
|
230
|
+
<section id="services" className="mx-auto max-w-6xl px-6 py-16">
|
|
231
|
+
<h2 className="mb-10 text-center text-3xl font-bold">What we do</h2>
|
|
232
|
+
<div className="grid gap-6 md:grid-cols-3">
|
|
233
|
+
{[
|
|
234
|
+
{ t: "Strategy", d: "Positioning, research and a roadmap that ships." },
|
|
235
|
+
{ t: "Design", d: "Brand and product design that people remember." },
|
|
236
|
+
{ t: "Engineering", d: "Fast, reliable software built to last." },
|
|
237
|
+
].map((s) => (
|
|
238
|
+
<div key={s.t} className="rounded-2xl border border-white/10 bg-white/[0.02] p-8">
|
|
239
|
+
<h3 className="text-xl font-bold gradient-text">{s.t}</h3>
|
|
240
|
+
<p className="mt-2 text-white/60">{s.d}</p>
|
|
241
|
+
</div>
|
|
242
|
+
))}
|
|
243
|
+
</div>
|
|
244
|
+
</section>
|
|
245
|
+
<section id="contact" className="mx-auto max-w-3xl px-6 py-24 text-center">
|
|
246
|
+
<h2 className="text-3xl font-bold">Let's work together</h2>
|
|
247
|
+
<p className="mt-3 text-white/60">Tell us about your project and we'll get back within a day.</p>
|
|
248
|
+
<a href="mailto:hello@example.com" className="mt-8 inline-block rounded-full border border-white/20 px-8 py-3 font-semibold hover:bg-white/5">hello@example.com</a>
|
|
249
|
+
</section>
|
|
250
|
+
${FOOTER(n)}`);
|
|
251
|
+
}
|
|
252
|
+
if (ctx.template.key === "ecommerce") {
|
|
253
|
+
return shell(`${NAV(n, ["Shop", "About", "Cart"])}
|
|
254
|
+
<section className="mx-auto max-w-5xl px-6 py-24 text-center">
|
|
255
|
+
<h1 className="text-5xl font-black leading-tight sm:text-6xl"><span className="gradient-text">${n}</span></h1>
|
|
256
|
+
<p className="mx-auto mt-6 max-w-xl text-lg text-white/60">${ctx.template.siteDescription}</p>
|
|
257
|
+
<a href="#shop" className="mt-10 inline-block gradient-bg rounded-full px-8 py-3 font-semibold text-black">Shop the collection</a>
|
|
258
|
+
</section>
|
|
259
|
+
<section id="shop" className="mx-auto max-w-6xl px-6 py-16">
|
|
260
|
+
<h2 className="mb-10 text-3xl font-bold">Featured</h2>
|
|
261
|
+
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
|
|
262
|
+
{[
|
|
263
|
+
{ n: "Aurora Lamp", p: "$89" }, { n: "Terra Mug", p: "$24" },
|
|
264
|
+
{ n: "Linen Throw", p: "$65" }, { n: "Oak Stand", p: "$120" },
|
|
265
|
+
].map((prod) => (
|
|
266
|
+
<div key={prod.n} className="group rounded-2xl border border-white/10 bg-white/[0.02] p-4 transition hover:-translate-y-1">
|
|
267
|
+
<div className="mb-4 aspect-square rounded-xl gradient-bg opacity-80" />
|
|
268
|
+
<div className="flex items-center justify-between">
|
|
269
|
+
<h3 className="font-semibold">{prod.n}</h3>
|
|
270
|
+
<span className="text-white/60">{prod.p}</span>
|
|
271
|
+
</div>
|
|
272
|
+
<button className="mt-3 w-full rounded-lg border border-white/15 py-2 text-sm font-medium hover:bg-white/5">Add to cart</button>
|
|
273
|
+
</div>
|
|
274
|
+
))}
|
|
275
|
+
</div>
|
|
276
|
+
</section>
|
|
277
|
+
${FOOTER(n)}`);
|
|
278
|
+
}
|
|
279
|
+
return shell(`${NAV(n, ["Features", "Pricing", "Sign in"])}
|
|
280
|
+
<section className="mx-auto max-w-4xl px-6 py-28 text-center">
|
|
281
|
+
<p className="mb-4 inline-block rounded-full border border-white/15 px-4 py-1 text-xs font-semibold text-white/60">New \xB7 v1.0</p>
|
|
282
|
+
<h1 className="text-5xl font-black leading-tight sm:text-6xl">Ship faster with <span className="gradient-text">${n}</span></h1>
|
|
283
|
+
<p className="mx-auto mt-6 max-w-2xl text-lg text-white/60">${ctx.template.siteDescription}</p>
|
|
284
|
+
<div className="mt-10 flex justify-center gap-4">
|
|
285
|
+
<a href="#pricing" className="gradient-bg rounded-full px-8 py-3 font-semibold text-black">Start free</a>
|
|
286
|
+
<a href="#features" className="rounded-full border border-white/20 px-8 py-3 font-semibold hover:bg-white/5">See features</a>
|
|
287
|
+
</div>
|
|
288
|
+
</section>
|
|
289
|
+
<section id="features" className="mx-auto max-w-6xl px-6 py-16">
|
|
290
|
+
<div className="grid gap-6 md:grid-cols-3">
|
|
291
|
+
{[
|
|
292
|
+
{ t: "Fast", d: "Built on the edge \u2014 global by default." },
|
|
293
|
+
{ t: "Secure", d: "Hardened headers and auth out of the box." },
|
|
294
|
+
{ t: "Scalable", d: "From your first user to your millionth." },
|
|
295
|
+
].map((f) => (
|
|
296
|
+
<div key={f.t} className="rounded-2xl border border-white/10 bg-white/[0.02] p-8">
|
|
297
|
+
<h3 className="text-xl font-bold gradient-text">{f.t}</h3>
|
|
298
|
+
<p className="mt-2 text-white/60">{f.d}</p>
|
|
299
|
+
</div>
|
|
300
|
+
))}
|
|
301
|
+
</div>
|
|
302
|
+
</section>
|
|
303
|
+
<section id="pricing" className="mx-auto max-w-md px-6 py-24 text-center">
|
|
304
|
+
<div className="rounded-3xl border border-white/10 bg-white/[0.02] p-10">
|
|
305
|
+
<h2 className="text-3xl font-bold">Pro</h2>
|
|
306
|
+
<p className="mt-2 text-5xl font-black gradient-text">$29<span className="text-lg text-white/40">/mo</span></p>
|
|
307
|
+
<a href="#" className="mt-8 inline-block w-full gradient-bg rounded-full px-8 py-3 font-semibold text-black">Get started</a>
|
|
308
|
+
</div>
|
|
309
|
+
</section>
|
|
310
|
+
${FOOTER(n)}`);
|
|
311
|
+
}
|
|
312
|
+
function buildFiles(ctx) {
|
|
313
|
+
return {
|
|
314
|
+
"package.json": pkgJson(ctx),
|
|
315
|
+
"tsconfig.json": tsconfig(),
|
|
316
|
+
"next.config.mjs": nextConfig(),
|
|
317
|
+
"postcss.config.mjs": postcss(),
|
|
318
|
+
".gitignore": gitignore(),
|
|
319
|
+
"README.md": readme(ctx),
|
|
320
|
+
"lib/site.ts": siteTs(ctx),
|
|
321
|
+
"app/layout.tsx": layout(ctx),
|
|
322
|
+
"app/globals.css": globalsCss(ctx),
|
|
323
|
+
"app/page.tsx": homePage(ctx),
|
|
324
|
+
"app/robots.txt/route.ts": robotsTs(),
|
|
325
|
+
"app/sitemap.xml/route.ts": sitemapTs()
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
function parseArgs(list) {
|
|
329
|
+
const a = { yes: false, install: true, git: true, pm: "npm", help: false };
|
|
330
|
+
for (let i = 0; i < list.length; i++) {
|
|
331
|
+
const arg = list[i];
|
|
332
|
+
const next = () => list[++i] ?? "";
|
|
333
|
+
if (arg === "-t" || arg === "--template") a.template = next();
|
|
334
|
+
else if (arg === "-y" || arg === "--yes") a.yes = true;
|
|
335
|
+
else if (arg === "--no-install") a.install = false;
|
|
336
|
+
else if (arg === "--no-git") a.git = false;
|
|
337
|
+
else if (arg === "--pm") a.pm = next();
|
|
338
|
+
else if (arg === "-h" || arg === "--help") a.help = true;
|
|
339
|
+
else if (arg.startsWith("--template=")) a.template = arg.slice(11);
|
|
340
|
+
else if (!arg.startsWith("-") && !a.name) a.name = arg;
|
|
341
|
+
}
|
|
342
|
+
return a;
|
|
343
|
+
}
|
|
344
|
+
var HELP = `
|
|
345
|
+
${c("bold", "create-lacspace-app")} \u2014 scaffold a beautiful Next.js app, Lacspace-wired
|
|
346
|
+
|
|
347
|
+
${c("bold", "Usage")}
|
|
348
|
+
npm create lacspace-app@latest <name> [options]
|
|
349
|
+
npx create-lacspace-app <name> --template <key>
|
|
350
|
+
|
|
351
|
+
${c("bold", "Templates")}
|
|
352
|
+
${TEMPLATES.map((t) => ` ${t.key.padEnd(10)} ${t.description}`).join("\n")}
|
|
353
|
+
|
|
354
|
+
${c("bold", "Options")}
|
|
355
|
+
-t, --template <key> Template (${TEMPLATES.map((t) => t.key).join(" | ")})
|
|
356
|
+
--pm <npm|pnpm|yarn|bun> Package manager (default npm)
|
|
357
|
+
--no-install Skip installing dependencies
|
|
358
|
+
--no-git Skip git init
|
|
359
|
+
-y, --yes Accept defaults (needs <name>)
|
|
360
|
+
-h, --help Show this help
|
|
361
|
+
`;
|
|
362
|
+
async function main() {
|
|
363
|
+
const args = parseArgs(argv.slice(2));
|
|
364
|
+
if (args.help) {
|
|
365
|
+
stdout.write(HELP + "\n");
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
stdout.write(`
|
|
369
|
+
${c("bold", c("magenta", "\u25C6 create-lacspace-app"))} ${c("dim", "\u2014 a gorgeous Next.js starter, batteries wired")}
|
|
370
|
+
|
|
371
|
+
`);
|
|
372
|
+
let name = args.name;
|
|
373
|
+
let templateKey = args.template;
|
|
374
|
+
if (!args.yes) {
|
|
375
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
376
|
+
try {
|
|
377
|
+
if (!name) name = (await rl.question(`${c("green", "?")} Project name ${c("dim", "(my-app)")}: `)).trim() || "my-app";
|
|
378
|
+
if (!templateKey) {
|
|
379
|
+
stdout.write(`
|
|
380
|
+
Choose a template:
|
|
381
|
+
`);
|
|
382
|
+
TEMPLATES.forEach((t, i) => stdout.write(` ${c("cyan", String(i + 1))}. ${c("bold", t.label)} ${c("dim", "\u2014 " + t.description)}
|
|
383
|
+
`));
|
|
384
|
+
const ans = (await rl.question(`
|
|
385
|
+
${c("green", "?")} Template ${c("dim", "(1)")}: `)).trim() || "1";
|
|
386
|
+
const idx = /^\d+$/.test(ans) ? parseInt(ans, 10) - 1 : TEMPLATES.findIndex((t) => t.key === ans);
|
|
387
|
+
templateKey = TEMPLATES[idx]?.key ?? "personal";
|
|
388
|
+
}
|
|
389
|
+
} finally {
|
|
390
|
+
rl.close();
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
name = name ?? "my-app";
|
|
394
|
+
const template = TEMPLATES.find((t) => t.key === templateKey) ?? TEMPLATES[0];
|
|
395
|
+
const dir = resolve(cwd(), name);
|
|
396
|
+
const projectName = basename(dir).toLowerCase().replace(/[^a-z0-9-_]/g, "-");
|
|
397
|
+
if (existsSync(dir) && readdirSync(dir).length > 0) {
|
|
398
|
+
stdout.write(c("red", `
|
|
399
|
+
\u2717 Directory "${name}" already exists and is not empty.
|
|
400
|
+
|
|
401
|
+
`));
|
|
402
|
+
exit(1);
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const files = buildFiles({ name: projectName, template });
|
|
406
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
407
|
+
const full = join(dir, rel);
|
|
408
|
+
mkdirSync(dirname(full), { recursive: true });
|
|
409
|
+
writeFileSync(full, content);
|
|
410
|
+
}
|
|
411
|
+
stdout.write(`
|
|
412
|
+
${c("green", "\u2714")} Created ${c("bold", name)} ${c("dim", `(${template.label})`)}
|
|
413
|
+
`);
|
|
414
|
+
for (const rel of Object.keys(files)) stdout.write(` ${c("dim", "+ " + rel)}
|
|
415
|
+
`);
|
|
416
|
+
if (args.git) {
|
|
417
|
+
const r = spawnSync("git", ["init", "-q"], { cwd: dir, stdio: "ignore" });
|
|
418
|
+
if (r.status === 0) stdout.write(`
|
|
419
|
+
${c("green", "\u2714")} Initialised git
|
|
420
|
+
`);
|
|
421
|
+
}
|
|
422
|
+
if (args.install) {
|
|
423
|
+
stdout.write(`
|
|
424
|
+
${c("cyan", "\u25F7")} Installing dependencies with ${args.pm}\u2026
|
|
425
|
+
`);
|
|
426
|
+
const install = args.pm === "yarn" ? [] : ["install"];
|
|
427
|
+
const r = spawnSync(args.pm, install, { cwd: dir, stdio: "inherit" });
|
|
428
|
+
if (r.status !== 0) stdout.write(c("yellow", `
|
|
429
|
+
! Install failed \u2014 run "${args.pm} install" yourself.
|
|
430
|
+
`));
|
|
431
|
+
}
|
|
432
|
+
const run = args.pm === "npm" ? "npm run dev" : `${args.pm} dev`;
|
|
433
|
+
stdout.write(`
|
|
434
|
+
${c("bold", "Done! Next steps")}
|
|
435
|
+
`);
|
|
436
|
+
stdout.write(` ${c("cyan", `cd ${name}`)}
|
|
437
|
+
`);
|
|
438
|
+
if (!args.install) stdout.write(` ${c("cyan", `${args.pm} install`)}
|
|
439
|
+
`);
|
|
440
|
+
stdout.write(` ${c("cyan", run)}
|
|
441
|
+
`);
|
|
442
|
+
stdout.write(`
|
|
443
|
+
Edit ${c("cyan", "lib/site.ts")} (your SEO config) and ${c("cyan", "app/page.tsx")}.
|
|
444
|
+
`);
|
|
445
|
+
stdout.write(`
|
|
446
|
+
${c("green", "Happy building!")} ${c("dim", "https://lacspace.com/packages")}
|
|
447
|
+
|
|
448
|
+
`);
|
|
449
|
+
}
|
|
450
|
+
main().catch((err) => {
|
|
451
|
+
stdout.write(c("red", `
|
|
452
|
+
\u2717 ${err instanceof Error ? err.message : String(err)}
|
|
453
|
+
|
|
454
|
+
`));
|
|
455
|
+
exit(1);
|
|
456
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-lacspace-app",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Scaffold a beautiful, production-ready Next.js app from a Lacspace template — personal portfolio, business site, e-commerce or SaaS landing — pre-wired with SEO metadata + JSON-LD, security headers, sitemap and robots. Like create-next-app, but you start gorgeous.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": { "create-lacspace-app": "./dist/index.js" },
|
|
7
|
+
"files": ["dist"],
|
|
8
|
+
"scripts": { "build": "tsup", "prepublishOnly": "npm run build" },
|
|
9
|
+
"keywords": ["create-lacspace-app", "nextjs", "next-app", "starter", "template", "scaffold", "boilerplate", "portfolio-template", "ecommerce-template", "saas-template", "cli", "create-next-app-alternative", "typescript"],
|
|
10
|
+
"author": "Lacspace <contact@lacspace.com>",
|
|
11
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
12
|
+
"homepage": "https://lacspace.com/packages",
|
|
13
|
+
"repository": { "type": "git", "url": "git+https://github.com/lacspace/npm-packages.git", "directory": "create-lacspace-app" },
|
|
14
|
+
"bugs": { "url": "https://github.com/lacspace/npm-packages/issues" },
|
|
15
|
+
"engines": { "node": ">=18" },
|
|
16
|
+
"publishConfig": { "access": "public" }
|
|
17
|
+
}
|