create-lightning-scaffold 1.0.0 → 1.0.2
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 +107 -25
- package/dist/index.js +169 -64
- package/package.json +1 -1
- package/templates/backend/firebase/index.ts +86 -9
- package/templates/backend/supabase/index.ts +107 -6
- package/templates/base/.env.example.ejs +17 -10
- package/templates/lib/backend.ts.ejs +59 -0
- package/templates/mobile/app/_layout.tsx +22 -52
- package/templates/mobile/app/index.tsx.ejs +44 -0
- package/templates/mobile/components/History.tsx.ejs +114 -0
- package/templates/mobile/components/Onboarding.tsx.ejs +79 -0
- package/templates/mobile/components/Recovery.tsx.ejs +119 -0
- package/templates/mobile/components/Swap.tsx.ejs +315 -0
- package/templates/mobile/package.json.ejs +4 -0
- package/templates/vite/README.md +73 -0
- package/templates/vite/eslint.config.js +23 -0
- package/templates/vite/index.html +13 -0
- package/templates/vite/package.json.ejs +37 -0
- package/templates/vite/postcss.config.js.ejs +8 -0
- package/templates/vite/public/vite.svg +1 -0
- package/templates/vite/src/App.tsx.ejs +65 -0
- package/templates/vite/src/assets/react.svg +1 -0
- package/templates/vite/src/components/History.tsx.ejs +112 -0
- package/templates/vite/src/components/Onboarding.tsx.ejs +64 -0
- package/templates/vite/src/components/Recovery.tsx.ejs +107 -0
- package/templates/vite/src/components/Swap.tsx.ejs +241 -0
- package/templates/vite/src/index.css.ejs +55 -0
- package/templates/vite/src/main.tsx +25 -0
- package/templates/vite/tailwind.config.js.ejs +13 -0
- package/templates/vite/tsconfig.app.json +28 -0
- package/templates/vite/tsconfig.json +7 -0
- package/templates/vite/tsconfig.node.json +26 -0
- package/templates/vite/vite.config.ts +15 -0
- package/templates/web/app/globals.css.ejs +53 -0
- package/templates/web/app/layout.tsx.ejs +8 -15
- package/templates/web/app/page.tsx.ejs +45 -0
- package/templates/web/app/providers.tsx +27 -0
- package/templates/web/components/History.tsx.ejs +114 -0
- package/templates/web/components/Onboarding.tsx.ejs +66 -0
- package/templates/web/components/Recovery.tsx.ejs +108 -0
- package/templates/web/components/Swap.tsx.ejs +289 -0
- package/templates/web/package.json.ejs +3 -1
- package/templates/examples/mobile/biometric-onboard.tsx +0 -104
- package/templates/examples/mobile/gasless-transfer.tsx +0 -72
- package/templates/examples/mobile/index.tsx +0 -30
- package/templates/examples/mobile/passkey-login.tsx +0 -55
- package/templates/examples/web/biometric-onboard/page.tsx +0 -70
- package/templates/examples/web/gasless-transfer/page.tsx +0 -85
- package/templates/examples/web/page.tsx +0 -27
- package/templates/examples/web/passkey-login/page.tsx +0 -50
- package/templates/lib/lazorkit/mobile/index.ts +0 -53
- package/templates/lib/lazorkit/web/index.ts +0 -67
- package/templates/mobile/app/(tabs)/_layout.tsx +0 -59
- package/templates/mobile/app/(tabs)/index.tsx +0 -31
- package/templates/mobile/app/(tabs)/two.tsx +0 -31
- package/templates/mobile/app/+html.tsx +0 -38
- package/templates/mobile/app/+not-found.tsx +0 -40
- package/templates/mobile/app/modal.tsx +0 -35
- package/templates/mobile/components/EditScreenInfo.tsx +0 -77
- package/templates/mobile/components/StyledText.tsx +0 -5
- package/templates/mobile/components/__tests__/StyledText-test.js +0 -10
- package/templates/mobile/lib/lazorkit/index.ts +0 -53
- package/templates/web/app/globals.css +0 -26
- package/templates/web/app/page.tsx +0 -65
- package/templates/web/lib/lazorkit/index.ts +0 -67
package/README.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# create-lightning-scaffold
|
|
2
|
+
<img width="1826" height="1183" alt="Screenshot 2026-01-12 at 22 18 56" src="https://github.com/user-attachments/assets/c34c35d4-1d18-49a4-8024-2d55f774af2e" />
|
|
2
3
|
|
|
3
|
-
CLI to scaffold
|
|
4
|
+
CLI to scaffold Solana apps with LazorKit SDK. Generate React Native (Expo) or Next.js projects with passkey authentication, gasless transactions, and a ready-to-use swap interface.
|
|
4
5
|
|
|
5
6
|
## Quick Start
|
|
6
7
|
|
|
@@ -8,24 +9,29 @@ CLI to scaffold projects with LazorKit SDK integration. Generate React Native (E
|
|
|
8
9
|
npx create-lightning-scaffold
|
|
9
10
|
```
|
|
10
11
|
|
|
11
|
-
##
|
|
12
|
+
## What You Get
|
|
12
13
|
|
|
13
|
-
|
|
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
|
|
14
|
+
Every generated project includes:
|
|
21
15
|
|
|
22
|
-
|
|
16
|
+
- **Passkey Onboarding** - Create wallets with Face ID/Touch ID, no seed phrases
|
|
17
|
+
- **Token Swap UI** - Real swap interface powered by Jupiter aggregator (SOL ↔ USDC)
|
|
18
|
+
- **Balance Display** - Real-time token balances with Max button
|
|
19
|
+
- **Transaction History** - View past transactions with Solscan links
|
|
20
|
+
- **Recovery & Backup** - Add backup passkeys from other devices
|
|
21
|
+
- **Message Signing** - Verify wallet ownership with `signMessage`
|
|
22
|
+
- **Gasless Transactions** - Users don't pay gas fees (paymaster sponsored)
|
|
23
|
+
- **Smart Wallets** - LazorKit PDA-based accounts with recovery options
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
## Features
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
- 🚀 **5 Presets**: Mobile App, Web App, Full-Stack Mobile, Full-Stack Web, Monorepo
|
|
28
|
+
- 🔐 **LazorKit SDK**: Official `@lazorkit/wallet` (web) and `@lazorkit/wallet-mobile-adapter` (mobile)
|
|
29
|
+
- 🔄 **Jupiter Integration**: Real-time quotes and swaps via Jupiter aggregator API
|
|
30
|
+
- 📱 **React Native + Expo**: Native passkey support with `expo-web-browser`
|
|
31
|
+
- 🌐 **Next.js + Vite**: Both frameworks supported with proper polyfills
|
|
32
|
+
- 🎨 **Styling**: TailwindCSS or pure CSS/StyleSheet (your choice)
|
|
33
|
+
- 📦 **State**: Zustand or Redux Toolkit
|
|
34
|
+
- 🗄️ **Backend**: Supabase, Firebase, or NestJS integration
|
|
29
35
|
|
|
30
36
|
## Usage
|
|
31
37
|
|
|
@@ -36,29 +42,105 @@ npx create-lightning-scaffold
|
|
|
36
42
|
# You'll be prompted for:
|
|
37
43
|
# - Project name
|
|
38
44
|
# - Preset (Mobile, Web, Full-Stack, Monorepo)
|
|
39
|
-
# -
|
|
45
|
+
# - Web framework (Next.js or Vite)
|
|
46
|
+
# - Styling preference (tailwind or none)
|
|
47
|
+
# - State management
|
|
40
48
|
# - Package manager
|
|
41
49
|
```
|
|
42
50
|
|
|
43
51
|
## Presets
|
|
44
52
|
|
|
45
|
-
| Preset | Description |
|
|
46
|
-
|
|
47
|
-
| Mobile App | React Native + Expo |
|
|
48
|
-
| Web App | Next.js |
|
|
49
|
-
| Full-Stack Mobile | React Native + Backend |
|
|
50
|
-
| Full-Stack Web | Next.js + Backend |
|
|
51
|
-
| Monorepo | Mobile + Web + Backend |
|
|
53
|
+
| Preset | Description | SDK |
|
|
54
|
+
|--------|-------------|-----|
|
|
55
|
+
| Mobile App | React Native + Expo | `@lazorkit/wallet-mobile-adapter` |
|
|
56
|
+
| Web App | Next.js or Vite | `@lazorkit/wallet` |
|
|
57
|
+
| Full-Stack Mobile | React Native + Backend | `@lazorkit/wallet-mobile-adapter` |
|
|
58
|
+
| Full-Stack Web | Next.js/Vite + Backend | `@lazorkit/wallet` |
|
|
59
|
+
| Monorepo | Mobile + Web + Backend | Both |
|
|
52
60
|
|
|
53
61
|
## Configuration
|
|
54
62
|
|
|
55
|
-
After scaffolding, copy `.env.example` to `.env
|
|
63
|
+
After scaffolding, copy `.env.example` to `.env`:
|
|
56
64
|
|
|
57
65
|
```bash
|
|
58
66
|
cp .env.example .env
|
|
59
67
|
```
|
|
60
68
|
|
|
61
|
-
|
|
69
|
+
Default config uses Devnet. For production, get your API key from [portal.lazor.sh](https://portal.lazor.sh).
|
|
70
|
+
|
|
71
|
+
## SDK Integration
|
|
72
|
+
|
|
73
|
+
### Web (Next.js / Vite)
|
|
74
|
+
|
|
75
|
+
Uses `@lazorkit/wallet` with `LazorkitProvider`:
|
|
76
|
+
|
|
77
|
+
```tsx
|
|
78
|
+
import { LazorkitProvider, useWallet } from '@lazorkit/wallet';
|
|
79
|
+
|
|
80
|
+
// Provider setup
|
|
81
|
+
<LazorkitProvider
|
|
82
|
+
rpcUrl="https://api.devnet.solana.com"
|
|
83
|
+
portalUrl="https://portal.lazor.sh"
|
|
84
|
+
paymasterConfig={{ paymasterUrl: "https://kora.devnet.lazorkit.com" }}
|
|
85
|
+
>
|
|
86
|
+
{children}
|
|
87
|
+
</LazorkitProvider>
|
|
88
|
+
|
|
89
|
+
// In components
|
|
90
|
+
const { connect, disconnect, signAndSendTransaction, signMessage, wallet } = useWallet();
|
|
91
|
+
|
|
92
|
+
// Connect with passkey
|
|
93
|
+
await connect();
|
|
94
|
+
|
|
95
|
+
// Sign a message
|
|
96
|
+
const { signature } = await signMessage("Verify ownership");
|
|
97
|
+
|
|
98
|
+
// Send gasless transaction
|
|
99
|
+
const sig = await signAndSendTransaction({
|
|
100
|
+
instructions: [instruction],
|
|
101
|
+
transactionOptions: { feeToken: 'USDC' }
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Mobile (Expo)
|
|
106
|
+
|
|
107
|
+
Uses `@lazorkit/wallet-mobile-adapter` with `LazorKitProvider`:
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
import { LazorKitProvider, useWallet } from '@lazorkit/wallet-mobile-adapter';
|
|
111
|
+
|
|
112
|
+
// Provider setup
|
|
113
|
+
<LazorKitProvider
|
|
114
|
+
rpcUrl="https://api.devnet.solana.com"
|
|
115
|
+
portalUrl="https://portal.lazor.sh"
|
|
116
|
+
configPaymaster={{ paymasterUrl: "https://kora.devnet.lazorkit.com" }}
|
|
117
|
+
>
|
|
118
|
+
{children}
|
|
119
|
+
</LazorKitProvider>
|
|
120
|
+
|
|
121
|
+
// In components - requires redirectUrl for mobile
|
|
122
|
+
const { connect, signAndSendTransaction, signMessage, wallet } = useWallet();
|
|
123
|
+
|
|
124
|
+
await connect({ redirectUrl: 'myapp://callback' });
|
|
125
|
+
await signMessage("Hello", { redirectUrl: 'myapp://callback' });
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Jupiter Swap Integration
|
|
129
|
+
|
|
130
|
+
The generated swap UI uses Jupiter's aggregator API for real-time quotes:
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
// Get quote
|
|
134
|
+
const quote = await fetch(
|
|
135
|
+
`https://api.jup.ag/swap/v1/quote?inputMint=${SOL}&outputMint=${USDC}&amount=${amount}&slippageBps=50`
|
|
136
|
+
).then(r => r.json());
|
|
137
|
+
|
|
138
|
+
// Build swap transaction
|
|
139
|
+
const { swapTransaction } = await fetch('https://api.jup.ag/swap/v1/swap', {
|
|
140
|
+
method: 'POST',
|
|
141
|
+
body: JSON.stringify({ quoteResponse: quote, userPublicKey: wallet })
|
|
142
|
+
}).then(r => r.json());
|
|
143
|
+
```
|
|
62
144
|
|
|
63
145
|
## License
|
|
64
146
|
|
package/dist/index.js
CHANGED
|
@@ -14,12 +14,12 @@ import pc from "picocolors";
|
|
|
14
14
|
// src/utils/types.ts
|
|
15
15
|
var PRESET_INFO = {
|
|
16
16
|
mobile: { label: "Mobile App", hint: "React Native + Expo", platforms: ["mobile"] },
|
|
17
|
-
web: { label: "Web App", hint: "Next.js", platforms: ["web"] },
|
|
17
|
+
web: { label: "Web App", hint: "Next.js or Vite", platforms: ["web"] },
|
|
18
18
|
"fullstack-mobile": { label: "Full-Stack Mobile", hint: "React Native + Backend", platforms: ["mobile"] },
|
|
19
|
-
"fullstack-web": { label: "Full-Stack Web", hint: "
|
|
19
|
+
"fullstack-web": { label: "Full-Stack Web", hint: "Web + Backend", platforms: ["web"] },
|
|
20
20
|
monorepo: { label: "Monorepo", hint: "Mobile + Web + Backend", platforms: ["mobile", "web"] }
|
|
21
21
|
};
|
|
22
|
-
function getCompatibleOptions(preset) {
|
|
22
|
+
function getCompatibleOptions(preset, webFramework) {
|
|
23
23
|
const info = PRESET_INFO[preset];
|
|
24
24
|
const hasMobile = info.platforms.includes("mobile");
|
|
25
25
|
const hasWeb = info.platforms.includes("web");
|
|
@@ -27,13 +27,20 @@ function getCompatibleOptions(preset) {
|
|
|
27
27
|
return {
|
|
28
28
|
styling: hasMobile ? ["nativewind", "none"] : ["tailwind", "none"],
|
|
29
29
|
components: hasMobile ? ["nativewind-ui", "none"] : ["shadcn", "none"],
|
|
30
|
-
backend: hasBackend ? ["supabase", "firebase"] : ["none"],
|
|
31
|
-
state: ["zustand", "redux"]
|
|
30
|
+
backend: hasBackend ? ["nestjs-postgres", "nestjs-mongodb", "supabase", "firebase"] : ["none"],
|
|
31
|
+
state: ["zustand", "redux"],
|
|
32
|
+
animation: hasMobile ? ["reanimated", "moti", "none"] : ["framer", "none"]
|
|
32
33
|
};
|
|
33
34
|
}
|
|
34
35
|
function isMonorepoPreset(preset) {
|
|
35
36
|
return preset === "fullstack-mobile" || preset === "fullstack-web" || preset === "monorepo";
|
|
36
37
|
}
|
|
38
|
+
function hasWebPlatform(preset) {
|
|
39
|
+
return PRESET_INFO[preset].platforms.includes("web");
|
|
40
|
+
}
|
|
41
|
+
function hasMobilePlatform(preset) {
|
|
42
|
+
return PRESET_INFO[preset].platforms.includes("mobile");
|
|
43
|
+
}
|
|
37
44
|
|
|
38
45
|
// src/utils/helpers.ts
|
|
39
46
|
import fs from "fs-extra";
|
|
@@ -68,16 +75,45 @@ async function runPrompts() {
|
|
|
68
75
|
}))
|
|
69
76
|
});
|
|
70
77
|
if (p.isCancel(preset)) return null;
|
|
71
|
-
const
|
|
78
|
+
const showWebFramework = hasWebPlatform(preset);
|
|
79
|
+
const showMobileOptions = hasMobilePlatform(preset);
|
|
80
|
+
const hasBackend = preset.includes("fullstack") || preset === "monorepo";
|
|
81
|
+
let webFramework = "nextjs";
|
|
82
|
+
if (showWebFramework) {
|
|
83
|
+
const webChoice = await p.select({
|
|
84
|
+
message: "Web framework:",
|
|
85
|
+
options: [
|
|
86
|
+
{ value: "nextjs", label: "Next.js", hint: "Full-featured React framework" },
|
|
87
|
+
{ value: "vite", label: "Vite", hint: "Fast, lightweight SPA" }
|
|
88
|
+
]
|
|
89
|
+
});
|
|
90
|
+
if (p.isCancel(webChoice)) return null;
|
|
91
|
+
webFramework = webChoice;
|
|
92
|
+
}
|
|
93
|
+
let backend = "none";
|
|
94
|
+
if (hasBackend) {
|
|
95
|
+
const backendChoice = await p.select({
|
|
96
|
+
message: "Backend:",
|
|
97
|
+
options: [
|
|
98
|
+
{ value: "nestjs-postgres", label: "NestJS + PostgreSQL", hint: "Prisma ORM" },
|
|
99
|
+
{ value: "nestjs-mongodb", label: "NestJS + MongoDB", hint: "Mongoose ODM" },
|
|
100
|
+
{ value: "supabase", label: "Supabase", hint: "BaaS with Postgres" },
|
|
101
|
+
{ value: "firebase", label: "Firebase", hint: "Google BaaS" }
|
|
102
|
+
]
|
|
103
|
+
});
|
|
104
|
+
if (p.isCancel(backendChoice)) return null;
|
|
105
|
+
backend = backendChoice;
|
|
106
|
+
}
|
|
107
|
+
const options = getCompatibleOptions(preset, webFramework);
|
|
72
108
|
const customize = await p.confirm({
|
|
73
|
-
message: "Customize
|
|
109
|
+
message: "Customize styling, state & animations?",
|
|
74
110
|
initialValue: false
|
|
75
111
|
});
|
|
76
112
|
if (p.isCancel(customize)) return null;
|
|
77
113
|
let styling = options.styling[0];
|
|
78
114
|
let components = options.components[0];
|
|
79
115
|
let state = "zustand";
|
|
80
|
-
let
|
|
116
|
+
let animation = "none";
|
|
81
117
|
if (customize) {
|
|
82
118
|
const stylingChoice = await p.select({
|
|
83
119
|
message: "Styling:",
|
|
@@ -88,8 +124,8 @@ async function runPrompts() {
|
|
|
88
124
|
const stateChoice = await p.select({
|
|
89
125
|
message: "State management:",
|
|
90
126
|
options: [
|
|
91
|
-
{ value: "zustand", label: "Zustand" },
|
|
92
|
-
{ value: "redux", label: "Redux Toolkit" }
|
|
127
|
+
{ value: "zustand", label: "Zustand", hint: "Simple, lightweight" },
|
|
128
|
+
{ value: "redux", label: "Redux Toolkit", hint: "Full-featured" }
|
|
93
129
|
]
|
|
94
130
|
});
|
|
95
131
|
if (p.isCancel(stateChoice)) return null;
|
|
@@ -100,17 +136,25 @@ async function runPrompts() {
|
|
|
100
136
|
});
|
|
101
137
|
if (p.isCancel(componentsChoice)) return null;
|
|
102
138
|
components = componentsChoice;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
139
|
+
const animationChoice = await p.select({
|
|
140
|
+
message: "Animations:",
|
|
141
|
+
options: options.animation.map((v) => ({
|
|
142
|
+
value: v,
|
|
143
|
+
label: v === "none" ? "None" : v === "reanimated" ? "React Native Reanimated" : v === "framer" ? "Framer Motion" : "Moti",
|
|
144
|
+
hint: v === "moti" ? "Cross-platform, uses Reanimated" : void 0
|
|
145
|
+
}))
|
|
146
|
+
});
|
|
147
|
+
if (p.isCancel(animationChoice)) return null;
|
|
148
|
+
animation = animationChoice;
|
|
149
|
+
}
|
|
150
|
+
let eas = false;
|
|
151
|
+
if (showMobileOptions) {
|
|
152
|
+
const easChoice = await p.confirm({
|
|
153
|
+
message: "Setup EAS Build for app store deployment?",
|
|
154
|
+
initialValue: true
|
|
155
|
+
});
|
|
156
|
+
if (p.isCancel(easChoice)) return null;
|
|
157
|
+
eas = easChoice;
|
|
114
158
|
}
|
|
115
159
|
const packageManager = await p.select({
|
|
116
160
|
message: "Package manager:",
|
|
@@ -130,10 +174,13 @@ async function runPrompts() {
|
|
|
130
174
|
return {
|
|
131
175
|
name,
|
|
132
176
|
preset,
|
|
177
|
+
webFramework,
|
|
133
178
|
backend,
|
|
134
179
|
styling,
|
|
135
180
|
state,
|
|
136
181
|
components,
|
|
182
|
+
animation,
|
|
183
|
+
eas,
|
|
137
184
|
packageManager,
|
|
138
185
|
gitInit
|
|
139
186
|
};
|
|
@@ -148,38 +195,62 @@ async function scaffold(config) {
|
|
|
148
195
|
const templatesDir = getTemplatesDir();
|
|
149
196
|
const isMonorepo = isMonorepoPreset(config.preset);
|
|
150
197
|
const platforms = PRESET_INFO[config.preset].platforms;
|
|
151
|
-
await fs2.ensureDir(targetDir);
|
|
152
198
|
if (isMonorepo) {
|
|
199
|
+
await fs2.ensureDir(targetDir);
|
|
153
200
|
await setupMonorepo(targetDir, config);
|
|
154
201
|
if (platforms.includes("mobile")) {
|
|
155
|
-
await
|
|
202
|
+
await scaffoldMobile(path2.join(targetDir, "apps"), "mobile", config);
|
|
203
|
+
if (config.eas) await addEasConfig(path2.join(targetDir, "apps/mobile"), config);
|
|
156
204
|
}
|
|
157
205
|
if (platforms.includes("web")) {
|
|
158
|
-
await
|
|
206
|
+
await scaffoldWeb(path2.join(targetDir, "apps"), "web", config);
|
|
159
207
|
}
|
|
160
208
|
if (config.backend !== "none") {
|
|
161
|
-
await
|
|
162
|
-
await copyTemplate(path2.join(templatesDir, "backend", config.backend), path2.join(targetDir, "packages/backend"), config);
|
|
209
|
+
await scaffoldBackend(path2.join(targetDir, "packages"), "backend", config);
|
|
163
210
|
}
|
|
164
211
|
} else {
|
|
165
|
-
|
|
166
|
-
|
|
212
|
+
if (platforms[0] === "mobile") {
|
|
213
|
+
await scaffoldMobile(process.cwd(), config.name, config);
|
|
214
|
+
if (config.eas) await addEasConfig(targetDir, config);
|
|
215
|
+
} else {
|
|
216
|
+
await scaffoldWeb(process.cwd(), config.name, config);
|
|
217
|
+
}
|
|
167
218
|
}
|
|
168
219
|
const appDir = isMonorepo ? path2.join(targetDir, platforms[0] === "mobile" ? "apps/mobile" : "apps/web") : targetDir;
|
|
169
|
-
|
|
170
|
-
await
|
|
171
|
-
await
|
|
172
|
-
|
|
220
|
+
const platform = platforms[0];
|
|
221
|
+
await addDependencies(appDir, platform, config);
|
|
222
|
+
await copyTemplate(path2.join(templatesDir, "state", config.state), path2.join(appDir, "lib/store"), config);
|
|
223
|
+
if (config.components !== "none") {
|
|
224
|
+
await copyTemplate(path2.join(templatesDir, "components", config.components), path2.join(appDir, "components/ui"), config);
|
|
225
|
+
}
|
|
226
|
+
if (config.backend !== "none") {
|
|
227
|
+
await copyTemplate(path2.join(templatesDir, "backend", config.backend), path2.join(appDir, "lib/backend"), config);
|
|
228
|
+
await copyTemplate(path2.join(templatesDir, "lib"), path2.join(appDir, "lib"), config);
|
|
229
|
+
}
|
|
173
230
|
await copyTemplate(path2.join(templatesDir, "base"), targetDir, config);
|
|
174
231
|
return targetDir;
|
|
175
232
|
}
|
|
233
|
+
async function scaffoldMobile(cwd, name, config) {
|
|
234
|
+
const templatesDir = getTemplatesDir();
|
|
235
|
+
await copyTemplate(path2.join(templatesDir, "mobile"), path2.join(cwd, name), config);
|
|
236
|
+
}
|
|
237
|
+
async function scaffoldWeb(cwd, name, config) {
|
|
238
|
+
const templatesDir = getTemplatesDir();
|
|
239
|
+
const templateName = config.webFramework === "vite" ? "vite" : "web";
|
|
240
|
+
await copyTemplate(path2.join(templatesDir, templateName), path2.join(cwd, name), config);
|
|
241
|
+
}
|
|
242
|
+
async function scaffoldBackend(cwd, name, config) {
|
|
243
|
+
const templatesDir = getTemplatesDir();
|
|
244
|
+
await copyTemplate(path2.join(templatesDir, "backend", config.backend), path2.join(cwd, name), config);
|
|
245
|
+
}
|
|
176
246
|
async function setupMonorepo(targetDir, config) {
|
|
177
247
|
const pkg = {
|
|
178
248
|
name: config.name,
|
|
179
249
|
private: true,
|
|
180
250
|
scripts: {
|
|
181
|
-
dev: "
|
|
182
|
-
|
|
251
|
+
"dev:mobile": "cd apps/mobile && npm run start",
|
|
252
|
+
"dev:web": "cd apps/web && npm run dev",
|
|
253
|
+
"dev:backend": "cd packages/backend && npm run start:dev"
|
|
183
254
|
}
|
|
184
255
|
};
|
|
185
256
|
if (config.packageManager === "pnpm") {
|
|
@@ -191,52 +262,80 @@ async function setupMonorepo(targetDir, config) {
|
|
|
191
262
|
await fs2.ensureDir(path2.join(targetDir, "apps"));
|
|
192
263
|
await fs2.ensureDir(path2.join(targetDir, "packages"));
|
|
193
264
|
}
|
|
194
|
-
async function
|
|
195
|
-
if (config.styling === "none") return;
|
|
265
|
+
async function addDependencies(appDir, platform, config) {
|
|
196
266
|
const pkgPath = path2.join(appDir, "package.json");
|
|
197
267
|
if (!await fs2.pathExists(pkgPath)) return;
|
|
198
268
|
const pkg = await fs2.readJson(pkgPath);
|
|
199
269
|
pkg.dependencies = pkg.dependencies || {};
|
|
200
270
|
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
271
|
if (config.state === "zustand") {
|
|
215
272
|
pkg.dependencies["zustand"] = "^4.5.0";
|
|
216
273
|
} else {
|
|
217
274
|
pkg.dependencies["@reduxjs/toolkit"] = "^2.0.0";
|
|
218
275
|
pkg.dependencies["react-redux"] = "^9.0.0";
|
|
219
276
|
}
|
|
277
|
+
if (config.animation === "reanimated") {
|
|
278
|
+
pkg.dependencies["react-native-reanimated"] = "^3.10.0";
|
|
279
|
+
pkg.dependencies["react-native-gesture-handler"] = "^2.16.0";
|
|
280
|
+
} else if (config.animation === "moti") {
|
|
281
|
+
pkg.dependencies["moti"] = "^0.29.0";
|
|
282
|
+
pkg.dependencies["react-native-reanimated"] = "^3.10.0";
|
|
283
|
+
} else if (config.animation === "framer") {
|
|
284
|
+
pkg.dependencies["framer-motion"] = "^11.0.0";
|
|
285
|
+
}
|
|
286
|
+
if (platform === "mobile") {
|
|
287
|
+
pkg.dependencies["@lazorkit/wallet-mobile-adapter"] = "latest";
|
|
288
|
+
pkg.dependencies["@solana/web3.js"] = "^1.95.0";
|
|
289
|
+
pkg.dependencies["react-native-get-random-values"] = "~1.11.0";
|
|
290
|
+
pkg.dependencies["react-native-url-polyfill"] = "^2.0.0";
|
|
291
|
+
pkg.dependencies["buffer"] = "^6.0.3";
|
|
292
|
+
pkg.dependencies["expo-crypto"] = "~15.0.0";
|
|
293
|
+
pkg.dependencies["expo-linking"] = "~8.0.11";
|
|
294
|
+
pkg.dependencies["expo-web-browser"] = "~15.0.10";
|
|
295
|
+
pkg.dependencies["expo-clipboard"] = "~7.0.0";
|
|
296
|
+
if (config.styling === "nativewind") {
|
|
297
|
+
pkg.dependencies["nativewind"] = "^4.0.0";
|
|
298
|
+
pkg.devDependencies["tailwindcss"] = "^3.4.0";
|
|
299
|
+
}
|
|
300
|
+
} else {
|
|
301
|
+
pkg.dependencies["@lazorkit/wallet"] = "latest";
|
|
302
|
+
pkg.dependencies["@solana/web3.js"] = "^1.95.0";
|
|
303
|
+
pkg.dependencies["@coral-xyz/anchor"] = "^0.30.0";
|
|
304
|
+
pkg.dependencies["buffer"] = "^6.0.3";
|
|
305
|
+
if (config.webFramework === "vite") {
|
|
306
|
+
pkg.devDependencies["vite-plugin-node-polyfills"] = "^0.22.0";
|
|
307
|
+
}
|
|
308
|
+
if (config.styling === "tailwind" && config.webFramework === "vite") {
|
|
309
|
+
pkg.devDependencies["tailwindcss"] = "^3.4.0";
|
|
310
|
+
pkg.devDependencies["postcss"] = "^8.4.0";
|
|
311
|
+
pkg.devDependencies["autoprefixer"] = "^10.4.0";
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (config.backend === "supabase") {
|
|
315
|
+
pkg.dependencies["@supabase/supabase-js"] = "^2.39.0";
|
|
316
|
+
} else if (config.backend === "firebase") {
|
|
317
|
+
pkg.dependencies["firebase"] = "^10.7.0";
|
|
318
|
+
}
|
|
220
319
|
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
320
|
}
|
|
229
|
-
async function
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
321
|
+
async function addEasConfig(appDir, config) {
|
|
322
|
+
const easConfig = {
|
|
323
|
+
cli: { version: ">= 7.0.0" },
|
|
324
|
+
build: {
|
|
325
|
+
development: { developmentClient: true, distribution: "internal" },
|
|
326
|
+
preview: { distribution: "internal" },
|
|
327
|
+
production: {}
|
|
328
|
+
},
|
|
329
|
+
submit: { production: {} }
|
|
330
|
+
};
|
|
331
|
+
await fs2.writeJson(path2.join(appDir, "eas.json"), easConfig, { spaces: 2 });
|
|
234
332
|
}
|
|
235
|
-
async function copyTemplate(src, dest, config) {
|
|
333
|
+
async function copyTemplate(src, dest, config, exclude = []) {
|
|
236
334
|
if (!await fs2.pathExists(src)) return;
|
|
237
335
|
await fs2.ensureDir(dest);
|
|
238
336
|
const files = await fs2.readdir(src, { withFileTypes: true });
|
|
239
337
|
for (const file of files) {
|
|
338
|
+
if (exclude.includes(file.name)) continue;
|
|
240
339
|
const srcPath = path2.join(src, file.name);
|
|
241
340
|
const destName = file.name.replace(/\.ejs$/, "");
|
|
242
341
|
const destPath = path2.join(dest, destName);
|
|
@@ -245,7 +344,9 @@ async function copyTemplate(src, dest, config) {
|
|
|
245
344
|
} else if (file.name.endsWith(".ejs")) {
|
|
246
345
|
const content = await fs2.readFile(srcPath, "utf-8");
|
|
247
346
|
const rendered = ejs.render(content, config);
|
|
248
|
-
|
|
347
|
+
if (rendered.trim()) {
|
|
348
|
+
await fs2.writeFile(destPath, rendered);
|
|
349
|
+
}
|
|
249
350
|
} else {
|
|
250
351
|
await fs2.copy(srcPath, destPath);
|
|
251
352
|
}
|
|
@@ -285,13 +386,17 @@ program.name("create-lightning-scaffold").description("Scaffold projects with La
|
|
|
285
386
|
const preset = opts.preset || "mobile";
|
|
286
387
|
const isMobile = preset === "mobile" || preset === "fullstack-mobile";
|
|
287
388
|
const hasBackend = preset.includes("fullstack") || preset === "monorepo";
|
|
389
|
+
const hasWeb = preset === "web" || preset === "fullstack-web" || preset === "monorepo";
|
|
288
390
|
config = {
|
|
289
391
|
name,
|
|
290
392
|
preset,
|
|
291
|
-
|
|
393
|
+
webFramework: "nextjs",
|
|
394
|
+
backend: hasBackend ? "nestjs-postgres" : "none",
|
|
292
395
|
styling: isMobile ? "nativewind" : "tailwind",
|
|
293
396
|
state: "zustand",
|
|
294
397
|
components: isMobile ? "nativewind-ui" : "shadcn",
|
|
398
|
+
animation: isMobile ? "reanimated" : "framer",
|
|
399
|
+
eas: isMobile,
|
|
295
400
|
packageManager: "npm",
|
|
296
401
|
gitInit: true
|
|
297
402
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { initializeApp } from "firebase/app";
|
|
2
|
-
import {
|
|
3
|
-
import { getFirestore, doc, setDoc, collection, query, where, getDocs, orderBy } from "firebase/firestore";
|
|
2
|
+
import { getFirestore, doc, setDoc, getDoc, collection, query, where, getDocs, orderBy, limit, addDoc, serverTimestamp } from "firebase/firestore";
|
|
4
3
|
|
|
5
4
|
const firebaseConfig = {
|
|
6
5
|
apiKey: process.env.EXPO_PUBLIC_FIREBASE_API_KEY || process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
|
|
@@ -9,17 +8,95 @@ const firebaseConfig = {
|
|
|
9
8
|
};
|
|
10
9
|
|
|
11
10
|
const app = initializeApp(firebaseConfig);
|
|
12
|
-
export const auth = getAuth(app);
|
|
13
11
|
export const db = getFirestore(app);
|
|
14
12
|
|
|
15
|
-
//
|
|
16
|
-
export
|
|
17
|
-
|
|
13
|
+
// Types
|
|
14
|
+
export interface User {
|
|
15
|
+
walletAddress: string;
|
|
16
|
+
createdAt: Date;
|
|
18
17
|
}
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
export interface SwapRecord {
|
|
20
|
+
walletAddress: string;
|
|
21
|
+
fromToken: string;
|
|
22
|
+
toToken: string;
|
|
23
|
+
fromAmount: string;
|
|
24
|
+
toAmount: string;
|
|
25
|
+
signature: string;
|
|
26
|
+
createdAt: Date;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// User Management
|
|
30
|
+
export async function createOrGetUser(walletAddress: string) {
|
|
31
|
+
const userRef = doc(db, "users", walletAddress);
|
|
32
|
+
const userSnap = await getDoc(userRef);
|
|
33
|
+
|
|
34
|
+
if (userSnap.exists()) return { id: userSnap.id, ...userSnap.data() };
|
|
35
|
+
|
|
36
|
+
await setDoc(userRef, { walletAddress, createdAt: serverTimestamp() });
|
|
37
|
+
return { id: walletAddress, walletAddress, createdAt: new Date() };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Swap History
|
|
41
|
+
export async function saveSwap(swap: Omit<SwapRecord, "createdAt">) {
|
|
42
|
+
const docRef = await addDoc(collection(db, "swaps"), {
|
|
43
|
+
...swap,
|
|
44
|
+
createdAt: serverTimestamp(),
|
|
45
|
+
});
|
|
46
|
+
return { id: docRef.id, ...swap };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function getSwapHistory(walletAddress: string, maxResults = 20) {
|
|
50
|
+
const q = query(
|
|
51
|
+
collection(db, "swaps"),
|
|
52
|
+
where("walletAddress", "==", walletAddress),
|
|
53
|
+
orderBy("createdAt", "desc"),
|
|
54
|
+
limit(maxResults)
|
|
55
|
+
);
|
|
23
56
|
const snapshot = await getDocs(q);
|
|
24
57
|
return snapshot.docs.map((d) => ({ id: d.id, ...d.data() }));
|
|
25
58
|
}
|
|
59
|
+
|
|
60
|
+
// Message Signatures (for verification records)
|
|
61
|
+
export async function saveSignature(walletAddress: string, message: string, signature: string) {
|
|
62
|
+
const docRef = await addDoc(collection(db, "signatures"), {
|
|
63
|
+
walletAddress,
|
|
64
|
+
message,
|
|
65
|
+
signature,
|
|
66
|
+
createdAt: serverTimestamp(),
|
|
67
|
+
});
|
|
68
|
+
return { id: docRef.id };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/*
|
|
72
|
+
Firestore Security Rules - Add to firestore.rules:
|
|
73
|
+
|
|
74
|
+
rules_version = '2';
|
|
75
|
+
service cloud.firestore {
|
|
76
|
+
match /databases/{database}/documents {
|
|
77
|
+
match /users/{walletAddress} {
|
|
78
|
+
allow read, write: if true; // Adjust based on your auth
|
|
79
|
+
}
|
|
80
|
+
match /swaps/{swapId} {
|
|
81
|
+
allow read, write: if true;
|
|
82
|
+
}
|
|
83
|
+
match /signatures/{sigId} {
|
|
84
|
+
allow read, write: if true;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
Firestore Indexes - Add to firestore.indexes.json:
|
|
90
|
+
{
|
|
91
|
+
"indexes": [
|
|
92
|
+
{
|
|
93
|
+
"collectionGroup": "swaps",
|
|
94
|
+
"queryScope": "COLLECTION",
|
|
95
|
+
"fields": [
|
|
96
|
+
{ "fieldPath": "walletAddress", "order": "ASCENDING" },
|
|
97
|
+
{ "fieldPath": "createdAt", "order": "DESCENDING" }
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
*/
|