anhedral 0.1.3 → 0.1.5
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 +136 -20
- package/dist/cli.d.ts +3 -5
- package/dist/cli.js +8 -27
- package/dist/commands.d.ts +3 -7
- package/dist/commands.js +12 -38
- package/dist/dependencies.d.ts +35 -0
- package/dist/dependencies.js +173 -0
- package/dist/scaffold.d.ts +6 -7
- package/dist/scaffold.js +409 -296
- package/dist/templates/backend.d.ts +1 -1
- package/dist/templates/backend.js +36 -45
- package/dist/templates/extension.d.ts +1 -1
- package/dist/templates/extension.js +81 -62
- package/dist/templates/frontend.d.ts +1 -1
- package/dist/templates/frontend.js +135 -179
- package/dist/toolchain.js +5 -7
- package/dist/util.d.ts +1 -0
- package/dist/util.js +25 -1
- package/package.json +5 -4
- package/dist/templates/default-web.d.ts +0 -2
- package/dist/templates/default-web.js +0 -1950
|
@@ -3,11 +3,12 @@ import path from 'node:path';
|
|
|
3
3
|
import { writeAnhedralLogo } from '../branding.js';
|
|
4
4
|
import { anhedralPrint } from '../print.js';
|
|
5
5
|
import { appendGitignore, writeFile, exec, execExpect, liftNestedProject } from '../util.js';
|
|
6
|
+
import { FRONTEND_ADDON_DEPENDENCIES, withVersions } from '../dependencies.js';
|
|
6
7
|
import { resolveToolchainChannel, resolveToolchain, toolPackageRef } from '../toolchain.js';
|
|
7
|
-
export async function scaffoldFrontend(root, { projectName, displayName }) {
|
|
8
|
-
const dir = path.join(root, '
|
|
8
|
+
export async function scaffoldFrontend(root, { projectName, displayName, skipInstall }) {
|
|
9
|
+
const dir = path.join(root, 'Frontend');
|
|
9
10
|
const toolchain = resolveToolchain(resolveToolchainChannel(process.env.ANHEDRAL_TOOLCHAIN));
|
|
10
|
-
anhedralPrint.section('
|
|
11
|
+
anhedralPrint.section('Frontend (Expo)');
|
|
11
12
|
anhedralPrint.step('Scaffolding Expo app with react-native-reusables');
|
|
12
13
|
fs.mkdirSync(dir, { recursive: true });
|
|
13
14
|
await execExpect(`pnpm dlx ${toolPackageRef('@react-native-reusables/cli', toolchain.reactNativeReusables)} init -t clerk-auth`, dir, [
|
|
@@ -17,11 +18,16 @@ export async function scaffoldFrontend(root, { projectName, displayName }) {
|
|
|
17
18
|
]);
|
|
18
19
|
liftNestedProject(dir, projectName);
|
|
19
20
|
anhedralPrint.done('Expo app scaffolded');
|
|
20
|
-
anhedralPrint.step('Installing
|
|
21
|
-
exec('pnpm install --no-frozen-lockfile', dir);
|
|
21
|
+
anhedralPrint.step('Installing frontend dependencies');
|
|
22
22
|
patchFrontendPackageJson(dir);
|
|
23
|
+
patchExpoAppConfig(dir);
|
|
24
|
+
patchFrontendTsConfig(dir);
|
|
25
|
+
if (!skipInstall) {
|
|
26
|
+
exec('pnpm install --no-frozen-lockfile', dir);
|
|
27
|
+
exec('pnpm exec expo install --fix --pnpm', dir);
|
|
28
|
+
}
|
|
23
29
|
appendGitignore(dir, ['.env', '.env.*', '!.env.example']);
|
|
24
|
-
anhedralPrint.done('
|
|
30
|
+
anhedralPrint.done(skipInstall ? 'Frontend dependency manifests written' : 'Frontend dependencies installed');
|
|
25
31
|
writeApiClient(dir);
|
|
26
32
|
writeConfigFile(dir);
|
|
27
33
|
writeApiHook(dir);
|
|
@@ -30,190 +36,97 @@ export async function scaffoldFrontend(root, { projectName, displayName }) {
|
|
|
30
36
|
writeAppShellFiles(dir, displayName);
|
|
31
37
|
writeEnvExample(dir);
|
|
32
38
|
writeEnvFile(dir);
|
|
39
|
+
writeVercelConfig(dir);
|
|
40
|
+
writeEasConfig(dir);
|
|
33
41
|
writeAnhedralLogo(dir);
|
|
34
42
|
writeSubscriptionProvider(dir);
|
|
35
43
|
writeUseSubscription(dir);
|
|
36
|
-
anhedralPrint.step('Installing
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
anhedralPrint.step('Installing Expo native packages');
|
|
45
|
+
if (skipInstall) {
|
|
46
|
+
anhedralPrint.info('Skipping Expo native package install (--skip-install)');
|
|
47
|
+
anhedralPrint.info('Run after init: pnpm --filter ./Frontend exec expo install --pnpm expo-image-picker');
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
exec('pnpm exec expo install --pnpm expo-image-picker', dir);
|
|
51
|
+
exec('pnpm exec expo install --fix --pnpm', dir);
|
|
52
|
+
}
|
|
53
|
+
anhedralPrint.done(skipInstall ? 'Expo native package install skipped' : 'Expo native packages installed');
|
|
54
|
+
anhedralPrint.step('Installing RevenueCat SDKs');
|
|
55
|
+
const revenueCatDependencies = withVersions({
|
|
56
|
+
'react-native-purchases': FRONTEND_ADDON_DEPENDENCIES['react-native-purchases'],
|
|
57
|
+
'react-native-purchases-ui': FRONTEND_ADDON_DEPENDENCIES['react-native-purchases-ui'],
|
|
58
|
+
'@revenuecat/purchases-js': FRONTEND_ADDON_DEPENDENCIES['@revenuecat/purchases-js'],
|
|
59
|
+
});
|
|
60
|
+
if (skipInstall) {
|
|
61
|
+
anhedralPrint.info(`Skipping RevenueCat SDK install (--skip-install). Run after init: pnpm --filter ./Frontend add ${revenueCatDependencies.join(' ')}`);
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
exec(`pnpm add ${revenueCatDependencies.join(' ')}`, dir);
|
|
65
|
+
}
|
|
66
|
+
anhedralPrint.done(skipInstall ? 'RevenueCat SDK install skipped' : 'Additional Expo SDKs installed');
|
|
40
67
|
}
|
|
41
68
|
function patchFrontendPackageJson(dir) {
|
|
42
69
|
const filePath = path.join(dir, 'package.json');
|
|
43
70
|
const packageJson = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
71
|
+
packageJson.dependencies = {
|
|
72
|
+
...(packageJson.dependencies ?? {}),
|
|
73
|
+
'@anhedral/api-client': FRONTEND_ADDON_DEPENDENCIES['@anhedral/api-client'],
|
|
74
|
+
'@clerk/expo': FRONTEND_ADDON_DEPENDENCIES['@clerk/expo'],
|
|
75
|
+
'react-native-purchases': FRONTEND_ADDON_DEPENDENCIES['react-native-purchases'],
|
|
76
|
+
'react-native-purchases-ui': FRONTEND_ADDON_DEPENDENCIES['react-native-purchases-ui'],
|
|
77
|
+
'@revenuecat/purchases-js': FRONTEND_ADDON_DEPENDENCIES['@revenuecat/purchases-js'],
|
|
78
|
+
};
|
|
44
79
|
packageJson.scripts = {
|
|
45
80
|
...(packageJson.scripts ?? {}),
|
|
46
|
-
build: 'pnpm typecheck',
|
|
81
|
+
build: 'pnpm typecheck && pnpm build:web',
|
|
82
|
+
'build:web': 'expo export --platform web',
|
|
47
83
|
typecheck: 'tsc --noEmit',
|
|
48
84
|
};
|
|
49
85
|
writeFile(filePath, JSON.stringify(packageJson, null, 2) + '\n');
|
|
50
86
|
}
|
|
51
|
-
function
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const SWR_MAX_SIZE = 200;
|
|
57
|
-
const SWR_DEFAULT_STALE_MS = 15_000;
|
|
58
|
-
const SWR_DEFAULT_MAX_AGE_MS = 300_000;
|
|
59
|
-
|
|
60
|
-
type SWREntry<T> = { data: T; fetchedAt: number; staleMs: number; maxAgeMs: number };
|
|
61
|
-
const SWR_CACHE = new Map<string, SWREntry<unknown>>();
|
|
62
|
-
const SWR_INFLIGHT = new Map<string, Promise<unknown>>();
|
|
63
|
-
|
|
64
|
-
function swrEvict() {
|
|
65
|
-
if (SWR_CACHE.size <= SWR_MAX_SIZE) return;
|
|
66
|
-
const entries = [...SWR_CACHE.entries()].sort((a, b) => a[1].fetchedAt - b[1].fetchedAt);
|
|
67
|
-
const toRemove = entries.slice(0, entries.length - SWR_MAX_SIZE);
|
|
68
|
-
for (const [key] of toRemove) SWR_CACHE.delete(key);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export class APIRequestError extends Error {
|
|
72
|
-
constructor(
|
|
73
|
-
public statusCode: number,
|
|
74
|
-
message: string,
|
|
75
|
-
public errorCode?: string,
|
|
76
|
-
) {
|
|
77
|
-
super(message);
|
|
78
|
-
this.name = 'APIRequestError';
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export class APIClient {
|
|
83
|
-
constructor(
|
|
84
|
-
private baseUrl: string,
|
|
85
|
-
private getToken: () => Promise<string | null>,
|
|
86
|
-
) {}
|
|
87
|
-
|
|
88
|
-
private async request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
|
89
|
-
const token = await this.getToken();
|
|
90
|
-
const headers: Record<string, string> = {
|
|
91
|
-
'Content-Type': 'application/json',
|
|
92
|
-
'X-Platform': 'web',
|
|
93
|
-
};
|
|
94
|
-
if (token) headers['Authorization'] = \`Bearer \${token}\`;
|
|
95
|
-
|
|
96
|
-
const res = await fetch(\`\${this.baseUrl}\${path}\`, {
|
|
97
|
-
method,
|
|
98
|
-
headers,
|
|
99
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
if (!res.ok) {
|
|
103
|
-
let err: { error?: string; message?: string } = {};
|
|
104
|
-
try { err = await res.json(); } catch {}
|
|
105
|
-
throw new APIRequestError(res.status, err.message || \`Request failed (\${res.status})\`, err.error);
|
|
87
|
+
function patchFrontendTsConfig(dir) {
|
|
88
|
+
const filePath = path.join(dir, 'tsconfig.json');
|
|
89
|
+
if (!fs.existsSync(filePath)) {
|
|
90
|
+
return;
|
|
106
91
|
}
|
|
107
|
-
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async get<T>(path: string): Promise<T> { return this.request<T>('GET', path); }
|
|
113
|
-
async post<T>(path: string, body?: unknown): Promise<T> { return this.request<T>('POST', path, body); }
|
|
114
|
-
async delete<T>(path: string): Promise<T> { return this.request<T>('DELETE', path); }
|
|
115
|
-
|
|
116
|
-
async getCached<T>(
|
|
117
|
-
path: string,
|
|
118
|
-
opts?: { staleMs?: number; maxAgeMs?: number },
|
|
119
|
-
): Promise<T> {
|
|
120
|
-
const staleMs = opts?.staleMs ?? SWR_DEFAULT_STALE_MS;
|
|
121
|
-
const maxAgeMs = opts?.maxAgeMs ?? SWR_DEFAULT_MAX_AGE_MS;
|
|
122
|
-
const now = Date.now();
|
|
123
|
-
const cached = SWR_CACHE.get(path) as SWREntry<T> | undefined;
|
|
124
|
-
|
|
125
|
-
if (cached) {
|
|
126
|
-
const age = now - cached.fetchedAt;
|
|
127
|
-
if (age < staleMs) return cached.data;
|
|
128
|
-
if (age < maxAgeMs) {
|
|
129
|
-
if (!SWR_INFLIGHT.has(path)) {
|
|
130
|
-
const p = this.get<T>(path).then((data) => {
|
|
131
|
-
SWR_CACHE.set(path, { data, fetchedAt: Date.now(), staleMs, maxAgeMs });
|
|
132
|
-
return data;
|
|
133
|
-
}).finally(() => SWR_INFLIGHT.delete(path));
|
|
134
|
-
SWR_INFLIGHT.set(path, p);
|
|
135
|
-
}
|
|
136
|
-
return cached.data;
|
|
137
|
-
}
|
|
92
|
+
const tsConfig = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
93
|
+
if (tsConfig.extends === 'expo/tsconfig.base') {
|
|
94
|
+
tsConfig.extends = 'expo/tsconfig.base.json';
|
|
138
95
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
swrEvict();
|
|
146
|
-
return data;
|
|
147
|
-
}).finally(() => SWR_INFLIGHT.delete(path));
|
|
148
|
-
SWR_INFLIGHT.set(path, p);
|
|
149
|
-
return p;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
invalidateCache(path: string): void { SWR_CACHE.delete(path); }
|
|
153
|
-
invalidateCachePrefix(prefix: string): void {
|
|
154
|
-
for (const key of SWR_CACHE.keys()) {
|
|
155
|
-
if (key.startsWith(prefix)) SWR_CACHE.delete(key);
|
|
96
|
+
writeFile(filePath, JSON.stringify(tsConfig, null, 2) + '\n');
|
|
97
|
+
}
|
|
98
|
+
function patchExpoAppConfig(dir) {
|
|
99
|
+
const filePath = path.join(dir, 'app.json');
|
|
100
|
+
if (!fs.existsSync(filePath)) {
|
|
101
|
+
return;
|
|
156
102
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
displayName: string;
|
|
173
|
-
imageUrl?: string | null;
|
|
174
|
-
avatarUrl?: string | null;
|
|
175
|
-
creditsBalance: number;
|
|
176
|
-
subscriptionTier: string;
|
|
177
|
-
subscriptionStatus: string;
|
|
178
|
-
};
|
|
179
|
-
}>('/auth/me');
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
async signOut() {
|
|
183
|
-
return this.post<{ success: boolean }>('/auth/signout');
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
async uploadAvatar(input: { base64: string; mimeType: string; fileName?: string }) {
|
|
187
|
-
return this.post<{ ok: boolean; avatarUrl: string }>('/auth/avatar', input);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
async deleteAccount() {
|
|
191
|
-
return this.delete<void>('/auth/account');
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// ── Subscription endpoints ───────────────────────────────────────────
|
|
195
|
-
async getSubscriptionPricing() {
|
|
196
|
-
return this.getCached<{
|
|
197
|
-
tiers: { tier: string; displayName: string; description: string; priceMonthly: number | null; priceYearly: number | null; currency: string; limits: { dailyLimit: number | null }; paymentInfo?: { revenueCatEntitlementId: string; revenueCatOfferingId: string } }[];
|
|
198
|
-
}>('/subscriptions/pricing', { staleMs: 300_000, maxAgeMs: 3_600_000 });
|
|
199
|
-
}
|
|
103
|
+
const appJson = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
104
|
+
appJson.expo ??= {};
|
|
105
|
+
appJson.expo.web = {
|
|
106
|
+
...(appJson.expo.web ?? {}),
|
|
107
|
+
bundler: 'metro',
|
|
108
|
+
output: 'static',
|
|
109
|
+
};
|
|
110
|
+
appJson.expo.plugins = Array.from(new Set([
|
|
111
|
+
...(Array.isArray(appJson.expo.plugins) ? appJson.expo.plugins : []),
|
|
112
|
+
'@clerk/expo',
|
|
113
|
+
]));
|
|
114
|
+
writeFile(filePath, JSON.stringify(appJson, null, 2) + '\n');
|
|
115
|
+
}
|
|
116
|
+
function writeApiClient(dir) {
|
|
117
|
+
writeFile(path.join(dir, 'api/client.ts'), `import { ApiClient, APIRequestError } from '@anhedral/api-client';
|
|
200
118
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
pro: boolean; inTrial: boolean; trialEndsAt?: string; expiresAt?: string;
|
|
205
|
-
periodStart?: string; periodEnd?: string;
|
|
206
|
-
method?: 'trialing' | 'redeemed' | 'paid' | null;
|
|
207
|
-
managementUrl?: string; cancelAtPeriodEnd?: boolean;
|
|
208
|
-
}>(path);
|
|
119
|
+
export class APIClient extends ApiClient {
|
|
120
|
+
constructor(baseUrl: string, getToken: () => Promise<string | null>) {
|
|
121
|
+
super({ baseUrl, getToken, platform: 'frontend' });
|
|
209
122
|
}
|
|
210
123
|
|
|
211
|
-
|
|
212
|
-
return this.
|
|
124
|
+
redeemPromoCode(code: string) {
|
|
125
|
+
return this.redeemCode(code);
|
|
213
126
|
}
|
|
214
127
|
}
|
|
215
128
|
|
|
216
|
-
export {
|
|
129
|
+
export { APIRequestError };
|
|
217
130
|
`);
|
|
218
131
|
writeFile(path.join(dir, 'api/index.ts'), `export { APIClient, APIRequestError } from './client';
|
|
219
132
|
`);
|
|
@@ -273,7 +186,7 @@ export function getPlatformRevenueCatApiKey(): string {
|
|
|
273
186
|
`);
|
|
274
187
|
}
|
|
275
188
|
function writeApiHook(dir) {
|
|
276
|
-
writeFile(path.join(dir, 'hooks/useAPI.ts'), `import { useAuth } from '@clerk/
|
|
189
|
+
writeFile(path.join(dir, 'hooks/useAPI.ts'), `import { useAuth } from '@clerk/expo';
|
|
277
190
|
import { useMemo } from 'react';
|
|
278
191
|
import { APIClient } from '@/api/client';
|
|
279
192
|
import { apiBaseUrl } from '@/lib/config';
|
|
@@ -284,8 +197,50 @@ export function useAPI() {
|
|
|
284
197
|
}
|
|
285
198
|
`);
|
|
286
199
|
}
|
|
200
|
+
function writeVercelConfig(dir) {
|
|
201
|
+
writeFile(path.join(dir, 'vercel.json'), JSON.stringify({
|
|
202
|
+
$schema: 'https://openapi.vercel.sh/vercel.json',
|
|
203
|
+
buildCommand: 'pnpm build:web',
|
|
204
|
+
outputDirectory: 'dist',
|
|
205
|
+
devCommand: 'pnpm dev',
|
|
206
|
+
cleanUrls: true,
|
|
207
|
+
framework: null,
|
|
208
|
+
rewrites: [
|
|
209
|
+
{
|
|
210
|
+
source: '/:path*',
|
|
211
|
+
destination: '/',
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
}, null, 2) + '\n');
|
|
215
|
+
}
|
|
216
|
+
function writeEasConfig(dir) {
|
|
217
|
+
writeFile(path.join(dir, 'eas.json'), JSON.stringify({
|
|
218
|
+
cli: {
|
|
219
|
+
version: '>= 16.24.0',
|
|
220
|
+
appVersionSource: 'remote',
|
|
221
|
+
},
|
|
222
|
+
build: {
|
|
223
|
+
development: {
|
|
224
|
+
developmentClient: true,
|
|
225
|
+
distribution: 'internal',
|
|
226
|
+
android: {
|
|
227
|
+
buildType: 'apk',
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
preview: {
|
|
231
|
+
distribution: 'internal',
|
|
232
|
+
},
|
|
233
|
+
production: {
|
|
234
|
+
autoIncrement: true,
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
submit: {
|
|
238
|
+
production: {},
|
|
239
|
+
},
|
|
240
|
+
}, null, 2) + '\n');
|
|
241
|
+
}
|
|
287
242
|
function writeAccountHook(dir) {
|
|
288
|
-
writeFile(path.join(dir, 'hooks/useAccount.ts'), `import { useAuth } from '@clerk/
|
|
243
|
+
writeFile(path.join(dir, 'hooks/useAccount.ts'), `import { useAuth } from '@clerk/expo';
|
|
289
244
|
import * as React from 'react';
|
|
290
245
|
import { useAPI } from '@/hooks/useAPI';
|
|
291
246
|
|
|
@@ -383,7 +338,7 @@ import Purchases, { type CustomerInfo, type PurchasesOfferings } from 'react-nat
|
|
|
383
338
|
import { Purchases as PurchasesWeb } from '@revenuecat/purchases-js';
|
|
384
339
|
import { AppState, Platform } from 'react-native';
|
|
385
340
|
import { subscriptionConfig, getPlatformRevenueCatApiKey } from '@/lib/config';
|
|
386
|
-
import { useAuth, useUser } from '@clerk/
|
|
341
|
+
import { useAuth, useUser } from '@clerk/expo';
|
|
387
342
|
import { useAPI } from '@/hooks/useAPI';
|
|
388
343
|
import RevenueCatUI, { PAYWALL_RESULT } from 'react-native-purchases-ui';
|
|
389
344
|
|
|
@@ -785,9 +740,10 @@ function writeAppShellFiles(dir, displayName) {
|
|
|
785
740
|
writeFile(path.join(dir, 'app/_layout.tsx'), `import '@/global.css';
|
|
786
741
|
|
|
787
742
|
import { SubscriptionProvider } from '@/contexts/SubscriptionProvider';
|
|
743
|
+
import { clerkPublishableKey } from '@/lib/config';
|
|
788
744
|
import { NAV_THEME } from '@/lib/theme';
|
|
789
|
-
import { ClerkProvider, useAuth } from '@clerk/
|
|
790
|
-
import { tokenCache } from '@clerk/
|
|
745
|
+
import { ClerkProvider, useAuth } from '@clerk/expo';
|
|
746
|
+
import { tokenCache } from '@clerk/expo/token-cache';
|
|
791
747
|
import { ThemeProvider } from '@react-navigation/native';
|
|
792
748
|
import { PortalHost } from '@rn-primitives/portal';
|
|
793
749
|
import { Stack } from 'expo-router';
|
|
@@ -802,7 +758,7 @@ export default function RootLayout() {
|
|
|
802
758
|
const { colorScheme } = useColorScheme();
|
|
803
759
|
|
|
804
760
|
return (
|
|
805
|
-
<ClerkProvider tokenCache={tokenCache}>
|
|
761
|
+
<ClerkProvider publishableKey={clerkPublishableKey} tokenCache={tokenCache}>
|
|
806
762
|
<ThemeProvider value={NAV_THEME[colorScheme ?? 'light']}>
|
|
807
763
|
<SubscriptionProvider>
|
|
808
764
|
<StatusBar style={colorScheme === 'dark' ? 'light' : 'dark'} />
|
|
@@ -851,7 +807,7 @@ function Routes() {
|
|
|
851
807
|
import { Button } from '@/components/ui/button';
|
|
852
808
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
853
809
|
import { Text } from '@/components/ui/text';
|
|
854
|
-
import { useAuth } from '@clerk/
|
|
810
|
+
import { useAuth } from '@clerk/expo';
|
|
855
811
|
import { Link } from 'expo-router';
|
|
856
812
|
import { CloudIcon, CreditCardIcon, ShieldCheckIcon, DatabaseIcon } from 'lucide-react-native';
|
|
857
813
|
import { ScrollView, View } from 'react-native';
|
|
@@ -859,7 +815,7 @@ import { ScrollView, View } from 'react-native';
|
|
|
859
815
|
const FEATURES = [
|
|
860
816
|
{
|
|
861
817
|
title: 'Landing page here',
|
|
862
|
-
description: 'Replace this placeholder copy with the actual acquisition story for your
|
|
818
|
+
description: 'Replace this placeholder copy with the actual acquisition story for your Expo app.',
|
|
863
819
|
icon: CloudIcon,
|
|
864
820
|
},
|
|
865
821
|
{
|
|
@@ -1191,7 +1147,7 @@ import { Input } from '@/components/ui/input';
|
|
|
1191
1147
|
import { Label } from '@/components/ui/label';
|
|
1192
1148
|
import { Separator } from '@/components/ui/separator';
|
|
1193
1149
|
import { Text } from '@/components/ui/text';
|
|
1194
|
-
import { useSignIn } from '@clerk/
|
|
1150
|
+
import { useSignIn } from '@clerk/expo/legacy';
|
|
1195
1151
|
import { Link } from 'expo-router';
|
|
1196
1152
|
import * as React from 'react';
|
|
1197
1153
|
import { type TextInput, View } from 'react-native';
|
|
@@ -1305,7 +1261,7 @@ import { Input } from '@/components/ui/input';
|
|
|
1305
1261
|
import { Label } from '@/components/ui/label';
|
|
1306
1262
|
import { Separator } from '@/components/ui/separator';
|
|
1307
1263
|
import { Text } from '@/components/ui/text';
|
|
1308
|
-
import { useSignUp } from '@clerk/
|
|
1264
|
+
import { useSignUp } from '@clerk/expo/legacy';
|
|
1309
1265
|
import { Link, router } from 'expo-router';
|
|
1310
1266
|
import * as React from 'react';
|
|
1311
1267
|
import { TextInput, View } from 'react-native';
|
|
@@ -1409,7 +1365,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover
|
|
|
1409
1365
|
import { Text } from '@/components/ui/text';
|
|
1410
1366
|
import { useAccount } from '@/hooks/useAccount';
|
|
1411
1367
|
import { useSubscription } from '@/hooks/useSubscription';
|
|
1412
|
-
import { useAuth } from '@clerk/
|
|
1368
|
+
import { useAuth } from '@clerk/expo';
|
|
1413
1369
|
import type { TriggerRef } from '@rn-primitives/popover';
|
|
1414
1370
|
import { CameraIcon, CreditCardIcon, CoinsIcon, LoaderCircleIcon, LogOutIcon } from 'lucide-react-native';
|
|
1415
1371
|
import * as React from 'react';
|
package/dist/toolchain.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TOOLCHAIN_DEPENDENCIES, VERIFIED_AT } from './dependencies.js';
|
|
1
2
|
export const TOOLCHAIN_CHANNELS = ['latest', 'stable'];
|
|
2
3
|
const TOOLCHAIN_SPECS = {
|
|
3
4
|
latest: {
|
|
@@ -7,13 +8,10 @@ const TOOLCHAIN_SPECS = {
|
|
|
7
8
|
wxt: 'latest',
|
|
8
9
|
},
|
|
9
10
|
stable: {
|
|
10
|
-
verifiedAt:
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
reactNativeReusables: '0.5.0',
|
|
15
|
-
// renovate: datasource=npm depName=wxt
|
|
16
|
-
wxt: '0.20.20',
|
|
11
|
+
verifiedAt: VERIFIED_AT,
|
|
12
|
+
shadcn: TOOLCHAIN_DEPENDENCIES.shadcn,
|
|
13
|
+
reactNativeReusables: TOOLCHAIN_DEPENDENCIES.reactNativeReusables,
|
|
14
|
+
wxt: TOOLCHAIN_DEPENDENCIES.wxt,
|
|
17
15
|
},
|
|
18
16
|
};
|
|
19
17
|
export function resolveToolchainChannel(value) {
|
package/dist/util.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export declare function writeFile(filePath: string, content: string): void;
|
|
2
2
|
export declare function appendGitignore(root: string, lines: string[]): void;
|
|
3
3
|
export declare function exec(cmd: string, cwd: string): void;
|
|
4
|
+
export declare function execCommand(command: string, args: string[], cwd: string): void;
|
|
4
5
|
export declare function execWithInput(cmd: string, cwd: string, stdinInput: string): void;
|
|
5
6
|
export declare function liftNestedProject(root: string, nestedName: string): void;
|
|
6
7
|
export declare function execExpect(cmd: string, cwd: string, prompts: [waitFor: string, answer: string][], timeoutMs?: number): Promise<void>;
|
package/dist/util.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { execSync, spawn } from 'node:child_process';
|
|
2
|
+
import { execSync, spawn, spawnSync } from 'node:child_process';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
export function writeFile(filePath, content) {
|
|
5
5
|
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
@@ -24,6 +24,9 @@ function dumpFailure(error, cmd) {
|
|
|
24
24
|
process.stderr.write(stderrText);
|
|
25
25
|
throw new Error(`Command failed (exit ${err.status ?? '?'}): ${cmd}`);
|
|
26
26
|
}
|
|
27
|
+
function commandLabel(command, args) {
|
|
28
|
+
return [command, ...args].join(' ');
|
|
29
|
+
}
|
|
27
30
|
export function exec(cmd, cwd) {
|
|
28
31
|
if (VERBOSE) {
|
|
29
32
|
console.log(` $ ${cmd}`);
|
|
@@ -37,6 +40,27 @@ export function exec(cmd, cwd) {
|
|
|
37
40
|
dumpFailure(error, cmd);
|
|
38
41
|
}
|
|
39
42
|
}
|
|
43
|
+
export function execCommand(command, args, cwd) {
|
|
44
|
+
const label = commandLabel(command, args);
|
|
45
|
+
if (VERBOSE) {
|
|
46
|
+
console.log(` $ ${label}`);
|
|
47
|
+
}
|
|
48
|
+
const result = spawnSync(command, args, {
|
|
49
|
+
cwd,
|
|
50
|
+
encoding: 'utf8',
|
|
51
|
+
stdio: VERBOSE ? 'inherit' : ['ignore', 'pipe', 'pipe'],
|
|
52
|
+
});
|
|
53
|
+
if (result.status === 0) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (!VERBOSE) {
|
|
57
|
+
if (result.stdout)
|
|
58
|
+
process.stderr.write(result.stdout);
|
|
59
|
+
if (result.stderr)
|
|
60
|
+
process.stderr.write(result.stderr);
|
|
61
|
+
}
|
|
62
|
+
throw new Error(`Command failed (exit ${result.status ?? '?'}): ${label}`);
|
|
63
|
+
}
|
|
40
64
|
export function execWithInput(cmd, cwd, stdinInput) {
|
|
41
65
|
if (VERBOSE) {
|
|
42
66
|
console.log(` $ ${cmd}`);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "anhedral",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Opinionated init CLI for Expo
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"description": "Opinionated init CLI for Expo, Fastify, WXT, shared packages, Neon, Drizzle, Clerk, RevenueCat, Stripe, R2, and Vercel.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"anhedral": "bin/anhedral.js"
|
|
@@ -38,9 +38,10 @@
|
|
|
38
38
|
"prepublishOnly": "pnpm test:all",
|
|
39
39
|
"test:cli": "node tests/test-cli.js",
|
|
40
40
|
"test:cli:validation": "node tests/test-cli-validation.js",
|
|
41
|
+
"test:deps": "pnpm build && node tests/test-dependency-manifest.js",
|
|
41
42
|
"test:e2e": "node tests/test-e2e-public-stacks.js",
|
|
42
43
|
"test:e2e:latest": "node tests/test-e2e-public-stacks.js --toolchain latest",
|
|
43
|
-
"test:all": "pnpm test && pnpm test:e2e",
|
|
44
|
+
"test:all": "pnpm test && pnpm test:deps && pnpm test:e2e",
|
|
44
45
|
"release:check": "pnpm test:all && npm pack --dry-run",
|
|
45
46
|
"demo:refresh": "pnpm build && node tests/refresh-demo.js"
|
|
46
47
|
},
|
|
@@ -54,7 +55,7 @@
|
|
|
54
55
|
"neondb",
|
|
55
56
|
"revenuecat",
|
|
56
57
|
"wxt",
|
|
57
|
-
"
|
|
58
|
+
"vercel"
|
|
58
59
|
],
|
|
59
60
|
"license": "MIT",
|
|
60
61
|
"packageManager": "pnpm@10.15.1",
|