create-lightning-scaffold 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 +21 -0
- package/README.md +65 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +335 -0
- package/package.json +55 -0
- package/templates/backend/firebase/index.ts +25 -0
- package/templates/backend/firebase/package.json.ejs +8 -0
- package/templates/backend/supabase/index.ts +16 -0
- package/templates/backend/supabase/package.json.ejs +8 -0
- package/templates/base/.env.example.ejs +18 -0
- package/templates/components/nativewind-ui/button.tsx +25 -0
- package/templates/components/nativewind-ui/card.tsx +17 -0
- package/templates/components/nativewind-ui/index.ts +2 -0
- package/templates/components/shadcn/button.tsx +15 -0
- package/templates/components/shadcn/card.tsx +19 -0
- package/templates/components/shadcn/index.ts +2 -0
- package/templates/examples/mobile/biometric-onboard.tsx +104 -0
- package/templates/examples/mobile/gasless-transfer.tsx +72 -0
- package/templates/examples/mobile/index.tsx +30 -0
- package/templates/examples/mobile/passkey-login.tsx +55 -0
- package/templates/examples/web/biometric-onboard/page.tsx +70 -0
- package/templates/examples/web/gasless-transfer/page.tsx +85 -0
- package/templates/examples/web/page.tsx +27 -0
- package/templates/examples/web/passkey-login/page.tsx +50 -0
- package/templates/lib/lazorkit/mobile/index.ts +53 -0
- package/templates/lib/lazorkit/web/index.ts +67 -0
- package/templates/mobile/.vscode/extensions.json +1 -0
- package/templates/mobile/.vscode/settings.json +7 -0
- package/templates/mobile/app/(tabs)/_layout.tsx +59 -0
- package/templates/mobile/app/(tabs)/index.tsx +31 -0
- package/templates/mobile/app/(tabs)/two.tsx +31 -0
- package/templates/mobile/app/+html.tsx +38 -0
- package/templates/mobile/app/+not-found.tsx +40 -0
- package/templates/mobile/app/_layout.tsx +59 -0
- package/templates/mobile/app/modal.tsx +35 -0
- package/templates/mobile/app.json.ejs +38 -0
- package/templates/mobile/assets/fonts/SpaceMono-Regular.ttf +0 -0
- package/templates/mobile/assets/images/adaptive-icon.png +0 -0
- package/templates/mobile/assets/images/favicon.png +0 -0
- package/templates/mobile/assets/images/icon.png +0 -0
- package/templates/mobile/assets/images/splash-icon.png +0 -0
- package/templates/mobile/components/EditScreenInfo.tsx +77 -0
- package/templates/mobile/components/ExternalLink.tsx +25 -0
- package/templates/mobile/components/StyledText.tsx +5 -0
- package/templates/mobile/components/Themed.tsx +45 -0
- package/templates/mobile/components/__tests__/StyledText-test.js +10 -0
- package/templates/mobile/components/useClientOnlyValue.ts +4 -0
- package/templates/mobile/components/useClientOnlyValue.web.ts +12 -0
- package/templates/mobile/components/useColorScheme.ts +1 -0
- package/templates/mobile/components/useColorScheme.web.ts +8 -0
- package/templates/mobile/constants/Colors.ts +19 -0
- package/templates/mobile/lib/lazorkit/index.ts +53 -0
- package/templates/mobile/package.json.ejs +40 -0
- package/templates/mobile/tsconfig.json +17 -0
- package/templates/state/redux/index.ts +30 -0
- package/templates/state/zustand/index.ts +16 -0
- package/templates/styling/nativewind/global.css +3 -0
- package/templates/styling/nativewind/tailwind.config.js +7 -0
- package/templates/web/README.md +36 -0
- package/templates/web/app/favicon.ico +0 -0
- package/templates/web/app/globals.css +26 -0
- package/templates/web/app/layout.tsx.ejs +32 -0
- package/templates/web/app/page.tsx +65 -0
- package/templates/web/eslint.config.mjs +18 -0
- package/templates/web/lib/lazorkit/index.ts +67 -0
- package/templates/web/next.config.ts +7 -0
- package/templates/web/package.json.ejs +28 -0
- package/templates/web/postcss.config.mjs +7 -0
- package/templates/web/public/file.svg +1 -0
- package/templates/web/public/globe.svg +1 -0
- package/templates/web/public/next.svg +1 -0
- package/templates/web/public/vercel.svg +1 -0
- package/templates/web/public/window.svg +1 -0
- package/templates/web/tailwind.config.ts +14 -0
- package/templates/web/tsconfig.json +34 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 create-lightning-scaffold
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# create-lightning-scaffold
|
|
2
|
+
|
|
3
|
+
CLI to scaffold projects with LazorKit SDK integration. Generate React Native (Expo) or Next.js projects with passkey authentication, gasless transactions, and biometric onboarding built-in.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx create-lightning-scaffold
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- 🚀 **5 Presets**: Mobile App, Web App, Full-Stack Mobile, Full-Stack Web, Monorepo
|
|
14
|
+
- 🔐 **LazorKit SDK**: Passkey auth, gasless transactions, smart wallets
|
|
15
|
+
- 📱 **React Native + Expo**: Official `create-expo-app` under the hood
|
|
16
|
+
- 🌐 **Next.js**: Official `create-next-app` under the hood
|
|
17
|
+
- 🎨 **Styling**: TailwindCSS (web) / NativeWind (mobile)
|
|
18
|
+
- 📦 **State**: Zustand or Redux Toolkit
|
|
19
|
+
- 🗄️ **Backend**: Supabase or Firebase integration
|
|
20
|
+
- 📦 **Package Managers**: npm, pnpm, yarn, bun
|
|
21
|
+
|
|
22
|
+
## Included Examples
|
|
23
|
+
|
|
24
|
+
Every generated project includes 3 working LazorKit examples:
|
|
25
|
+
|
|
26
|
+
1. **Passkey Login** - WebAuthn-based authentication with smart wallet
|
|
27
|
+
2. **Gasless Transfer** - Send SOL without paying gas fees
|
|
28
|
+
3. **Biometric Onboarding** - Mobile-first onboarding with FaceID/TouchID
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Interactive mode
|
|
34
|
+
npx create-lightning-scaffold
|
|
35
|
+
|
|
36
|
+
# You'll be prompted for:
|
|
37
|
+
# - Project name
|
|
38
|
+
# - Preset (Mobile, Web, Full-Stack, Monorepo)
|
|
39
|
+
# - Customization options (styling, state, components, backend)
|
|
40
|
+
# - Package manager
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Presets
|
|
44
|
+
|
|
45
|
+
| Preset | Description | Structure |
|
|
46
|
+
|--------|-------------|-----------|
|
|
47
|
+
| Mobile App | React Native + Expo | Flat |
|
|
48
|
+
| Web App | Next.js | Flat |
|
|
49
|
+
| Full-Stack Mobile | React Native + Backend | Monorepo |
|
|
50
|
+
| Full-Stack Web | Next.js + Backend | Monorepo |
|
|
51
|
+
| Monorepo | Mobile + Web + Backend | Monorepo |
|
|
52
|
+
|
|
53
|
+
## Configuration
|
|
54
|
+
|
|
55
|
+
After scaffolding, copy `.env.example` to `.env` and add your LazorKit API key:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
cp .env.example .env
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Get your API key from [portal.lazor.sh](https://portal.lazor.sh).
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
MIT
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { program } from "commander";
|
|
5
|
+
import * as p2 from "@clack/prompts";
|
|
6
|
+
import pc2 from "picocolors";
|
|
7
|
+
import gradient from "gradient-string";
|
|
8
|
+
import figlet from "figlet";
|
|
9
|
+
|
|
10
|
+
// src/cli.ts
|
|
11
|
+
import * as p from "@clack/prompts";
|
|
12
|
+
import pc from "picocolors";
|
|
13
|
+
|
|
14
|
+
// src/utils/types.ts
|
|
15
|
+
var PRESET_INFO = {
|
|
16
|
+
mobile: { label: "Mobile App", hint: "React Native + Expo", platforms: ["mobile"] },
|
|
17
|
+
web: { label: "Web App", hint: "Next.js", platforms: ["web"] },
|
|
18
|
+
"fullstack-mobile": { label: "Full-Stack Mobile", hint: "React Native + Backend", platforms: ["mobile"] },
|
|
19
|
+
"fullstack-web": { label: "Full-Stack Web", hint: "Next.js + Backend", platforms: ["web"] },
|
|
20
|
+
monorepo: { label: "Monorepo", hint: "Mobile + Web + Backend", platforms: ["mobile", "web"] }
|
|
21
|
+
};
|
|
22
|
+
function getCompatibleOptions(preset) {
|
|
23
|
+
const info = PRESET_INFO[preset];
|
|
24
|
+
const hasMobile = info.platforms.includes("mobile");
|
|
25
|
+
const hasWeb = info.platforms.includes("web");
|
|
26
|
+
const hasBackend = preset.includes("fullstack") || preset === "monorepo";
|
|
27
|
+
return {
|
|
28
|
+
styling: hasMobile ? ["nativewind", "none"] : ["tailwind", "none"],
|
|
29
|
+
components: hasMobile ? ["nativewind-ui", "none"] : ["shadcn", "none"],
|
|
30
|
+
backend: hasBackend ? ["supabase", "firebase"] : ["none"],
|
|
31
|
+
state: ["zustand", "redux"]
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function isMonorepoPreset(preset) {
|
|
35
|
+
return preset === "fullstack-mobile" || preset === "fullstack-web" || preset === "monorepo";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/utils/helpers.ts
|
|
39
|
+
import fs from "fs-extra";
|
|
40
|
+
import path from "path";
|
|
41
|
+
import { fileURLToPath } from "url";
|
|
42
|
+
function validateProjectName(name) {
|
|
43
|
+
if (!name) return "Project name is required";
|
|
44
|
+
if (!/^[a-z0-9-_]+$/i.test(name)) return "Only letters, numbers, hyphens, and underscores allowed";
|
|
45
|
+
if (fs.existsSync(name)) return `Directory "${name}" already exists`;
|
|
46
|
+
return void 0;
|
|
47
|
+
}
|
|
48
|
+
function getTemplatesDir() {
|
|
49
|
+
const __dirname2 = path.dirname(fileURLToPath(import.meta.url));
|
|
50
|
+
return path.join(__dirname2, "..", "templates");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// src/cli.ts
|
|
54
|
+
async function runPrompts() {
|
|
55
|
+
p.intro(pc.bgCyan(pc.black(" create-lightning-scaffold ")));
|
|
56
|
+
const name = await p.text({
|
|
57
|
+
message: "Project name:",
|
|
58
|
+
placeholder: "my-app",
|
|
59
|
+
validate: validateProjectName
|
|
60
|
+
});
|
|
61
|
+
if (p.isCancel(name)) return null;
|
|
62
|
+
const preset = await p.select({
|
|
63
|
+
message: "Select a preset:",
|
|
64
|
+
options: Object.entries(PRESET_INFO).map(([value, { label, hint }]) => ({
|
|
65
|
+
value,
|
|
66
|
+
label,
|
|
67
|
+
hint
|
|
68
|
+
}))
|
|
69
|
+
});
|
|
70
|
+
if (p.isCancel(preset)) return null;
|
|
71
|
+
const options = getCompatibleOptions(preset);
|
|
72
|
+
const customize = await p.confirm({
|
|
73
|
+
message: "Customize options?",
|
|
74
|
+
initialValue: false
|
|
75
|
+
});
|
|
76
|
+
if (p.isCancel(customize)) return null;
|
|
77
|
+
let styling = options.styling[0];
|
|
78
|
+
let components = options.components[0];
|
|
79
|
+
let state = "zustand";
|
|
80
|
+
let backend = options.backend[0];
|
|
81
|
+
if (customize) {
|
|
82
|
+
const stylingChoice = await p.select({
|
|
83
|
+
message: "Styling:",
|
|
84
|
+
options: options.styling.map((v) => ({ value: v, label: v === "none" ? "None" : v }))
|
|
85
|
+
});
|
|
86
|
+
if (p.isCancel(stylingChoice)) return null;
|
|
87
|
+
styling = stylingChoice;
|
|
88
|
+
const stateChoice = await p.select({
|
|
89
|
+
message: "State management:",
|
|
90
|
+
options: [
|
|
91
|
+
{ value: "zustand", label: "Zustand" },
|
|
92
|
+
{ value: "redux", label: "Redux Toolkit" }
|
|
93
|
+
]
|
|
94
|
+
});
|
|
95
|
+
if (p.isCancel(stateChoice)) return null;
|
|
96
|
+
state = stateChoice;
|
|
97
|
+
const componentsChoice = await p.select({
|
|
98
|
+
message: "Component library:",
|
|
99
|
+
options: options.components.map((v) => ({ value: v, label: v === "none" ? "None" : v }))
|
|
100
|
+
});
|
|
101
|
+
if (p.isCancel(componentsChoice)) return null;
|
|
102
|
+
components = componentsChoice;
|
|
103
|
+
if (options.backend[0] !== "none") {
|
|
104
|
+
const backendChoice = await p.select({
|
|
105
|
+
message: "Backend:",
|
|
106
|
+
options: [
|
|
107
|
+
{ value: "supabase", label: "Supabase" },
|
|
108
|
+
{ value: "firebase", label: "Firebase" }
|
|
109
|
+
]
|
|
110
|
+
});
|
|
111
|
+
if (p.isCancel(backendChoice)) return null;
|
|
112
|
+
backend = backendChoice;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const packageManager = await p.select({
|
|
116
|
+
message: "Package manager:",
|
|
117
|
+
options: [
|
|
118
|
+
{ value: "npm", label: "npm" },
|
|
119
|
+
{ value: "pnpm", label: "pnpm" },
|
|
120
|
+
{ value: "yarn", label: "yarn" },
|
|
121
|
+
{ value: "bun", label: "bun" }
|
|
122
|
+
]
|
|
123
|
+
});
|
|
124
|
+
if (p.isCancel(packageManager)) return null;
|
|
125
|
+
const gitInit = await p.confirm({
|
|
126
|
+
message: "Initialize git repository?",
|
|
127
|
+
initialValue: true
|
|
128
|
+
});
|
|
129
|
+
if (p.isCancel(gitInit)) return null;
|
|
130
|
+
return {
|
|
131
|
+
name,
|
|
132
|
+
preset,
|
|
133
|
+
backend,
|
|
134
|
+
styling,
|
|
135
|
+
state,
|
|
136
|
+
components,
|
|
137
|
+
packageManager,
|
|
138
|
+
gitInit
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// src/scaffolder.ts
|
|
143
|
+
import fs2 from "fs-extra";
|
|
144
|
+
import path2 from "path";
|
|
145
|
+
import ejs from "ejs";
|
|
146
|
+
async function scaffold(config) {
|
|
147
|
+
const targetDir = path2.resolve(process.cwd(), config.name);
|
|
148
|
+
const templatesDir = getTemplatesDir();
|
|
149
|
+
const isMonorepo = isMonorepoPreset(config.preset);
|
|
150
|
+
const platforms = PRESET_INFO[config.preset].platforms;
|
|
151
|
+
await fs2.ensureDir(targetDir);
|
|
152
|
+
if (isMonorepo) {
|
|
153
|
+
await setupMonorepo(targetDir, config);
|
|
154
|
+
if (platforms.includes("mobile")) {
|
|
155
|
+
await copyTemplate(path2.join(templatesDir, "mobile"), path2.join(targetDir, "apps/mobile"), config);
|
|
156
|
+
}
|
|
157
|
+
if (platforms.includes("web")) {
|
|
158
|
+
await copyTemplate(path2.join(templatesDir, "web"), path2.join(targetDir, "apps/web"), config);
|
|
159
|
+
}
|
|
160
|
+
if (config.backend !== "none") {
|
|
161
|
+
await fs2.ensureDir(path2.join(targetDir, "packages/backend"));
|
|
162
|
+
await copyTemplate(path2.join(templatesDir, "backend", config.backend), path2.join(targetDir, "packages/backend"), config);
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
const platformTemplate = platforms[0] === "mobile" ? "mobile" : "web";
|
|
166
|
+
await copyTemplate(path2.join(templatesDir, platformTemplate), targetDir, config);
|
|
167
|
+
}
|
|
168
|
+
const appDir = isMonorepo ? path2.join(targetDir, platforms[0] === "mobile" ? "apps/mobile" : "apps/web") : targetDir;
|
|
169
|
+
await addStyling(appDir, config);
|
|
170
|
+
await addStateManagement(appDir, config);
|
|
171
|
+
await addComponents(appDir, config);
|
|
172
|
+
await addExamples(appDir, platforms[0], config);
|
|
173
|
+
await copyTemplate(path2.join(templatesDir, "base"), targetDir, config);
|
|
174
|
+
return targetDir;
|
|
175
|
+
}
|
|
176
|
+
async function setupMonorepo(targetDir, config) {
|
|
177
|
+
const pkg = {
|
|
178
|
+
name: config.name,
|
|
179
|
+
private: true,
|
|
180
|
+
scripts: {
|
|
181
|
+
dev: "echo 'Run dev in apps/*'",
|
|
182
|
+
build: "echo 'Run build in apps/*'"
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
if (config.packageManager === "pnpm") {
|
|
186
|
+
await fs2.writeFile(path2.join(targetDir, "pnpm-workspace.yaml"), "packages:\n - 'apps/*'\n - 'packages/*'\n");
|
|
187
|
+
} else {
|
|
188
|
+
pkg.workspaces = ["apps/*", "packages/*"];
|
|
189
|
+
}
|
|
190
|
+
await fs2.writeJson(path2.join(targetDir, "package.json"), pkg, { spaces: 2 });
|
|
191
|
+
await fs2.ensureDir(path2.join(targetDir, "apps"));
|
|
192
|
+
await fs2.ensureDir(path2.join(targetDir, "packages"));
|
|
193
|
+
}
|
|
194
|
+
async function addStyling(appDir, config) {
|
|
195
|
+
if (config.styling === "none") return;
|
|
196
|
+
const pkgPath = path2.join(appDir, "package.json");
|
|
197
|
+
if (!await fs2.pathExists(pkgPath)) return;
|
|
198
|
+
const pkg = await fs2.readJson(pkgPath);
|
|
199
|
+
pkg.dependencies = pkg.dependencies || {};
|
|
200
|
+
pkg.devDependencies = pkg.devDependencies || {};
|
|
201
|
+
if (config.styling === "nativewind") {
|
|
202
|
+
pkg.dependencies["nativewind"] = "^4.0.0";
|
|
203
|
+
pkg.devDependencies["tailwindcss"] = "^3.4.0";
|
|
204
|
+
const templatesDir = getTemplatesDir();
|
|
205
|
+
await copyTemplate(path2.join(templatesDir, "styling", "nativewind"), appDir, config);
|
|
206
|
+
}
|
|
207
|
+
await fs2.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
208
|
+
}
|
|
209
|
+
async function addStateManagement(appDir, config) {
|
|
210
|
+
const pkgPath = path2.join(appDir, "package.json");
|
|
211
|
+
if (!await fs2.pathExists(pkgPath)) return;
|
|
212
|
+
const pkg = await fs2.readJson(pkgPath);
|
|
213
|
+
pkg.dependencies = pkg.dependencies || {};
|
|
214
|
+
if (config.state === "zustand") {
|
|
215
|
+
pkg.dependencies["zustand"] = "^4.5.0";
|
|
216
|
+
} else {
|
|
217
|
+
pkg.dependencies["@reduxjs/toolkit"] = "^2.0.0";
|
|
218
|
+
pkg.dependencies["react-redux"] = "^9.0.0";
|
|
219
|
+
}
|
|
220
|
+
await fs2.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
221
|
+
const templatesDir = getTemplatesDir();
|
|
222
|
+
await copyTemplate(path2.join(templatesDir, "state", config.state), path2.join(appDir, "lib/store"), config);
|
|
223
|
+
}
|
|
224
|
+
async function addComponents(appDir, config) {
|
|
225
|
+
if (config.components === "none") return;
|
|
226
|
+
const templatesDir = getTemplatesDir();
|
|
227
|
+
await copyTemplate(path2.join(templatesDir, "components", config.components), path2.join(appDir, "components/ui"), config);
|
|
228
|
+
}
|
|
229
|
+
async function addExamples(appDir, platform, config) {
|
|
230
|
+
const templatesDir = getTemplatesDir();
|
|
231
|
+
const examplesDir = path2.join(templatesDir, "examples", platform);
|
|
232
|
+
const targetDir = platform === "mobile" ? path2.join(appDir, "app") : path2.join(appDir, "app/examples");
|
|
233
|
+
await copyTemplate(examplesDir, targetDir, config);
|
|
234
|
+
}
|
|
235
|
+
async function copyTemplate(src, dest, config) {
|
|
236
|
+
if (!await fs2.pathExists(src)) return;
|
|
237
|
+
await fs2.ensureDir(dest);
|
|
238
|
+
const files = await fs2.readdir(src, { withFileTypes: true });
|
|
239
|
+
for (const file of files) {
|
|
240
|
+
const srcPath = path2.join(src, file.name);
|
|
241
|
+
const destName = file.name.replace(/\.ejs$/, "");
|
|
242
|
+
const destPath = path2.join(dest, destName);
|
|
243
|
+
if (file.isDirectory()) {
|
|
244
|
+
await copyTemplate(srcPath, destPath, config);
|
|
245
|
+
} else if (file.name.endsWith(".ejs")) {
|
|
246
|
+
const content = await fs2.readFile(srcPath, "utf-8");
|
|
247
|
+
const rendered = ejs.render(content, config);
|
|
248
|
+
await fs2.writeFile(destPath, rendered);
|
|
249
|
+
} else {
|
|
250
|
+
await fs2.copy(srcPath, destPath);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/installer.ts
|
|
256
|
+
import { spawn } from "child_process";
|
|
257
|
+
function installDependencies(cwd, pm) {
|
|
258
|
+
return new Promise((resolve, reject) => {
|
|
259
|
+
const cmd = pm === "npm" ? "npm" : pm;
|
|
260
|
+
const args = pm === "yarn" ? [] : ["install"];
|
|
261
|
+
const child = spawn(cmd, args, { cwd, stdio: "inherit", shell: true });
|
|
262
|
+
child.on("close", (code) => code === 0 ? resolve() : reject(new Error(`Install failed with code ${code}`)));
|
|
263
|
+
child.on("error", reject);
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
function initGit(cwd) {
|
|
267
|
+
return new Promise((resolve, reject) => {
|
|
268
|
+
const child = spawn("git", ["init"], { cwd, stdio: "ignore", shell: true });
|
|
269
|
+
child.on("close", (code) => code === 0 ? resolve() : reject(new Error("Git init failed")));
|
|
270
|
+
child.on("error", reject);
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// src/index.ts
|
|
275
|
+
var showBanner = () => {
|
|
276
|
+
const art = figlet.textSync("Lightning Scaffold", { font: "Slant" });
|
|
277
|
+
console.log(gradient.cristal.multiline(art));
|
|
278
|
+
console.log();
|
|
279
|
+
};
|
|
280
|
+
program.name("create-lightning-scaffold").description("Scaffold projects with LazorKit SDK integration").version("1.0.0").option("-y, --yes", "Use defaults").option("-n, --name <name>", "Project name").option("-p, --preset <preset>", "Preset: mobile, web, fullstack-mobile, fullstack-web, monorepo").option("--skip-install", "Skip dependency installation").action(async (opts) => {
|
|
281
|
+
showBanner();
|
|
282
|
+
let config;
|
|
283
|
+
if (opts.yes || opts.preset) {
|
|
284
|
+
const name = opts.name || "my-lightning-app";
|
|
285
|
+
const preset = opts.preset || "mobile";
|
|
286
|
+
const isMobile = preset === "mobile" || preset === "fullstack-mobile";
|
|
287
|
+
const hasBackend = preset.includes("fullstack") || preset === "monorepo";
|
|
288
|
+
config = {
|
|
289
|
+
name,
|
|
290
|
+
preset,
|
|
291
|
+
backend: hasBackend ? "supabase" : "none",
|
|
292
|
+
styling: isMobile ? "nativewind" : "tailwind",
|
|
293
|
+
state: "zustand",
|
|
294
|
+
components: isMobile ? "nativewind-ui" : "shadcn",
|
|
295
|
+
packageManager: "npm",
|
|
296
|
+
gitInit: true
|
|
297
|
+
};
|
|
298
|
+
p2.intro(pc2.bgCyan(pc2.black(" create-lightning-scaffold ")));
|
|
299
|
+
p2.log.info(`Creating: ${name} (${preset})`);
|
|
300
|
+
} else {
|
|
301
|
+
config = await runPrompts();
|
|
302
|
+
}
|
|
303
|
+
if (!config) {
|
|
304
|
+
p2.cancel("Operation cancelled");
|
|
305
|
+
process.exit(0);
|
|
306
|
+
}
|
|
307
|
+
const s = p2.spinner();
|
|
308
|
+
s.start("Scaffolding project...");
|
|
309
|
+
try {
|
|
310
|
+
const targetDir = await scaffold(config);
|
|
311
|
+
s.stop("Project scaffolded!");
|
|
312
|
+
if (!opts.skipInstall) {
|
|
313
|
+
s.start("Installing dependencies...");
|
|
314
|
+
await installDependencies(targetDir, config.packageManager);
|
|
315
|
+
s.stop("Dependencies installed!");
|
|
316
|
+
}
|
|
317
|
+
if (config.gitInit) {
|
|
318
|
+
s.start("Initializing git...");
|
|
319
|
+
await initGit(targetDir);
|
|
320
|
+
s.stop("Git initialized!");
|
|
321
|
+
}
|
|
322
|
+
p2.outro(pc2.green("\u2713 Project created successfully!"));
|
|
323
|
+
console.log(`
|
|
324
|
+
${pc2.bold("Next steps:")}`);
|
|
325
|
+
console.log(` cd ${config.name}`);
|
|
326
|
+
console.log(` ${config.packageManager === "npm" ? "npm run" : config.packageManager} dev
|
|
327
|
+
`);
|
|
328
|
+
console.log(pc2.dim("LazorKit examples included: passkey-login, gasless-transfer, biometric-onboard"));
|
|
329
|
+
} catch (err) {
|
|
330
|
+
s.stop("Failed");
|
|
331
|
+
p2.cancel(`Error: ${err instanceof Error ? err.message : err}`);
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-lightning-scaffold",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI to scaffold projects with LazorKit SDK integration",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-lightning-scaffold": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"templates"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsup",
|
|
15
|
+
"dev": "tsup --watch",
|
|
16
|
+
"start": "node dist/index.js",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@clack/prompts": "^0.7.0",
|
|
21
|
+
"commander": "^12.0.0",
|
|
22
|
+
"ejs": "^3.1.10",
|
|
23
|
+
"figlet": "^1.9.4",
|
|
24
|
+
"fs-extra": "^11.2.0",
|
|
25
|
+
"gradient-string": "^3.0.0",
|
|
26
|
+
"picocolors": "^1.0.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/ejs": "^3.1.5",
|
|
30
|
+
"@types/figlet": "^1.7.0",
|
|
31
|
+
"@types/fs-extra": "^11.0.4",
|
|
32
|
+
"@types/gradient-string": "^1.1.6",
|
|
33
|
+
"@types/node": "^20.0.0",
|
|
34
|
+
"tsup": "^8.0.0",
|
|
35
|
+
"typescript": "^5.0.0"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18.0.0"
|
|
39
|
+
},
|
|
40
|
+
"keywords": [
|
|
41
|
+
"cli",
|
|
42
|
+
"scaffold",
|
|
43
|
+
"lazorkit",
|
|
44
|
+
"react-native",
|
|
45
|
+
"nextjs",
|
|
46
|
+
"expo"
|
|
47
|
+
],
|
|
48
|
+
"license": "MIT",
|
|
49
|
+
"author": "tobi-techy",
|
|
50
|
+
"repository": {
|
|
51
|
+
"type": "git",
|
|
52
|
+
"url": "https://github.com/tobi-techy/LIGHTING-SCAFFOLD.git"
|
|
53
|
+
},
|
|
54
|
+
"homepage": "https://github.com/tobi-techy/LIGHTING-SCAFFOLD#readme"
|
|
55
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { initializeApp } from "firebase/app";
|
|
2
|
+
import { getAuth } from "firebase/auth";
|
|
3
|
+
import { getFirestore, doc, setDoc, collection, query, where, getDocs, orderBy } from "firebase/firestore";
|
|
4
|
+
|
|
5
|
+
const firebaseConfig = {
|
|
6
|
+
apiKey: process.env.EXPO_PUBLIC_FIREBASE_API_KEY || process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
|
|
7
|
+
authDomain: process.env.EXPO_PUBLIC_FIREBASE_AUTH_DOMAIN || process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
|
|
8
|
+
projectId: process.env.EXPO_PUBLIC_FIREBASE_PROJECT_ID || process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const app = initializeApp(firebaseConfig);
|
|
12
|
+
export const auth = getAuth(app);
|
|
13
|
+
export const db = getFirestore(app);
|
|
14
|
+
|
|
15
|
+
// Example: Save wallet to user profile
|
|
16
|
+
export async function saveWallet(userId: string, walletAddress: string) {
|
|
17
|
+
return setDoc(doc(db, "wallets", userId), { address: walletAddress, updatedAt: new Date() });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Example: Get user's transactions
|
|
21
|
+
export async function getTransactions(walletAddress: string) {
|
|
22
|
+
const q = query(collection(db, "transactions"), where("walletAddress", "==", walletAddress), orderBy("createdAt", "desc"));
|
|
23
|
+
const snapshot = await getDocs(q);
|
|
24
|
+
return snapshot.docs.map((d) => ({ id: d.id, ...d.data() }));
|
|
25
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createClient } from "@supabase/supabase-js";
|
|
2
|
+
|
|
3
|
+
const supabaseUrl = process.env.EXPO_PUBLIC_SUPABASE_URL || process.env.NEXT_PUBLIC_SUPABASE_URL || "";
|
|
4
|
+
const supabaseKey = process.env.EXPO_PUBLIC_SUPABASE_ANON_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || "";
|
|
5
|
+
|
|
6
|
+
export const supabase = createClient(supabaseUrl, supabaseKey);
|
|
7
|
+
|
|
8
|
+
// Example: Save wallet to user profile
|
|
9
|
+
export async function saveWallet(userId: string, walletAddress: string) {
|
|
10
|
+
return supabase.from("wallets").upsert({ user_id: userId, address: walletAddress });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Example: Get user's transactions
|
|
14
|
+
export async function getTransactions(walletAddress: string) {
|
|
15
|
+
return supabase.from("transactions").select("*").eq("wallet_address", walletAddress).order("created_at", { ascending: false });
|
|
16
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# LazorKit Configuration
|
|
2
|
+
LAZORKIT_API_KEY=your_api_key_here
|
|
3
|
+
LAZORKIT_PORTAL_URL=https://portal.lazor.sh
|
|
4
|
+
LAZORKIT_PAYMASTER_URL=https://kora.devnet.lazorkit.com
|
|
5
|
+
|
|
6
|
+
# Solana
|
|
7
|
+
SOLANA_RPC=https://api.devnet.solana.com
|
|
8
|
+
SOLANA_NETWORK=devnet
|
|
9
|
+
<% if (backend === 'supabase') { %>
|
|
10
|
+
# Supabase
|
|
11
|
+
SUPABASE_URL=your_supabase_url
|
|
12
|
+
SUPABASE_ANON_KEY=your_supabase_anon_key
|
|
13
|
+
<% } %><% if (backend === 'firebase') { %>
|
|
14
|
+
# Firebase
|
|
15
|
+
FIREBASE_API_KEY=your_firebase_api_key
|
|
16
|
+
FIREBASE_AUTH_DOMAIN=your_project.firebaseapp.com
|
|
17
|
+
FIREBASE_PROJECT_ID=your_project_id
|
|
18
|
+
<% } %>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Pressable, Text, PressableProps, TextStyle, ViewStyle } from "react-native";
|
|
2
|
+
|
|
3
|
+
interface ButtonProps extends PressableProps {
|
|
4
|
+
title: string;
|
|
5
|
+
variant?: "default" | "outline" | "ghost";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function Button({ title, variant = "default", style, ...props }: ButtonProps) {
|
|
9
|
+
const baseStyle: ViewStyle = { paddingHorizontal: 16, paddingVertical: 12, borderRadius: 8, alignItems: "center" };
|
|
10
|
+
const variants: Record<string, ViewStyle> = {
|
|
11
|
+
default: { backgroundColor: "#6366f1" },
|
|
12
|
+
outline: { borderWidth: 1, borderColor: "#6366f1" },
|
|
13
|
+
ghost: {},
|
|
14
|
+
};
|
|
15
|
+
const textVariants: Record<string, TextStyle> = {
|
|
16
|
+
default: { color: "#fff" },
|
|
17
|
+
outline: { color: "#6366f1" },
|
|
18
|
+
ghost: { color: "#6366f1" },
|
|
19
|
+
};
|
|
20
|
+
return (
|
|
21
|
+
<Pressable style={[baseStyle, variants[variant], style as ViewStyle]} {...props}>
|
|
22
|
+
<Text style={[{ fontWeight: "600" }, textVariants[variant]]}>{title}</Text>
|
|
23
|
+
</Pressable>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { View, Text, ViewProps, TextProps } from "react-native";
|
|
2
|
+
|
|
3
|
+
export function Card({ style, ...props }: ViewProps) {
|
|
4
|
+
return <View style={[{ borderRadius: 12, backgroundColor: "#fff", shadowColor: "#000", shadowOpacity: 0.1, shadowRadius: 4, padding: 16 }, style]} {...props} />;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function CardHeader({ style, ...props }: ViewProps) {
|
|
8
|
+
return <View style={[{ marginBottom: 12 }, style]} {...props} />;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function CardTitle({ style, ...props }: TextProps) {
|
|
12
|
+
return <Text style={[{ fontSize: 20, fontWeight: "600" }, style]} {...props} />;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function CardContent(props: ViewProps) {
|
|
16
|
+
return <View {...props} />;
|
|
17
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|
4
|
+
variant?: "default" | "outline" | "ghost";
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function Button({ className = "", variant = "default", ...props }: ButtonProps) {
|
|
8
|
+
const base = "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none px-4 py-2";
|
|
9
|
+
const variants = {
|
|
10
|
+
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
|
11
|
+
outline: "border border-input hover:bg-accent hover:text-accent-foreground",
|
|
12
|
+
ghost: "hover:bg-accent hover:text-accent-foreground",
|
|
13
|
+
};
|
|
14
|
+
return <button className={`${base} ${variants[variant]} ${className}`} {...props} />;
|
|
15
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
interface CardProps extends React.HTMLAttributes<HTMLDivElement> {}
|
|
4
|
+
|
|
5
|
+
export function Card({ className = "", ...props }: CardProps) {
|
|
6
|
+
return <div className={`rounded-lg border bg-card text-card-foreground shadow-sm ${className}`} {...props} />;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function CardHeader({ className = "", ...props }: CardProps) {
|
|
10
|
+
return <div className={`flex flex-col space-y-1.5 p-6 ${className}`} {...props} />;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function CardTitle({ className = "", ...props }: React.HTMLAttributes<HTMLHeadingElement>) {
|
|
14
|
+
return <h3 className={`text-2xl font-semibold leading-none tracking-tight ${className}`} {...props} />;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function CardContent({ className = "", ...props }: CardProps) {
|
|
18
|
+
return <div className={`p-6 pt-0 ${className}`} {...props} />;
|
|
19
|
+
}
|