kitcn 0.13.1 → 0.13.3
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/dist/aggregate/index.d.ts +1 -1
- package/dist/{backend-core-yq-eWLRJ.mjs → backend-core-BV61sNRx.mjs} +1159 -57
- package/dist/cli.mjs +4 -4
- package/dist/orm/index.d.ts +1 -1
- package/dist/watcher.mjs +1 -1
- package/dist/{where-clause-compiler-TMppDl9g.d.ts → where-clause-compiler-DcEhkJ12.d.ts} +59 -59
- package/package.json +1 -1
- package/skills/convex/references/setup/expo.md +73 -0
- package/skills/convex/references/setup/index.md +9 -0
- package/skills/convex/references/setup/server.md +1 -1
- package/dist/convex-plugin-tWTDqoKJ.mjs +0 -276
- package/dist/upstream-BUCdbLok.mjs +0 -26
|
@@ -5967,7 +5967,7 @@ const syncEnv = pushEnv;
|
|
|
5967
5967
|
//#endregion
|
|
5968
5968
|
//#region src/cli/project-context.ts
|
|
5969
5969
|
function isReactScaffoldFramework(framework) {
|
|
5970
|
-
return framework !== "next-app";
|
|
5970
|
+
return framework !== "next-app" && framework !== "expo";
|
|
5971
5971
|
}
|
|
5972
5972
|
const NEXT_CONFIG_FILES = [
|
|
5973
5973
|
"next.config.ts",
|
|
@@ -5999,6 +5999,13 @@ const GATSBY_CONFIG_FILES = [
|
|
|
5999
5999
|
"gatsby-config.mjs",
|
|
6000
6000
|
"gatsby-config.cjs"
|
|
6001
6001
|
];
|
|
6002
|
+
const EXPO_CONFIG_FILES = [
|
|
6003
|
+
"app.json",
|
|
6004
|
+
"app.config.ts",
|
|
6005
|
+
"app.config.js",
|
|
6006
|
+
"app.config.mjs",
|
|
6007
|
+
"app.config.cjs"
|
|
6008
|
+
];
|
|
6002
6009
|
const LEADING_SLASH_RE = /^\/+/;
|
|
6003
6010
|
const TS_ALIAS_RE = /"@\/\*"\s*:\s*\[\s*"([^"]+)"\s*\]/m;
|
|
6004
6011
|
function hasAnyFile(cwd, relativePaths) {
|
|
@@ -6054,7 +6061,7 @@ function inferUsesSrcFromAppDirs(cwd) {
|
|
|
6054
6061
|
function inferUsesSrc(cwd, mode) {
|
|
6055
6062
|
const signals = [
|
|
6056
6063
|
inferUsesSrcFromComponentsJson(cwd),
|
|
6057
|
-
mode === "next-app" ? inferUsesSrcFromAppDirs(cwd) : null,
|
|
6064
|
+
mode === "next-app" || mode === "expo" ? inferUsesSrcFromAppDirs(cwd) : null,
|
|
6058
6065
|
inferUsesSrcFromComponentRoots(cwd),
|
|
6059
6066
|
inferUsesSrcFromTsconfig(cwd),
|
|
6060
6067
|
fs.existsSync(resolve(cwd, "src")) ? true : null
|
|
@@ -6085,6 +6092,7 @@ function detectProjectFramework(cwd = process.cwd()) {
|
|
|
6085
6092
|
if (fs.existsSync(resolve(cwd, "composer.json"))) return "laravel";
|
|
6086
6093
|
if (hasDependency$1(packageJson, (dependency) => dependency.startsWith("@remix-run/"))) return "remix";
|
|
6087
6094
|
if (hasDependency$1(packageJson, (dependency) => dependency.startsWith("@tanstack/react-start"))) return "tanstack-start";
|
|
6095
|
+
if (hasAnyFile(cwd, EXPO_CONFIG_FILES) && hasDependency$1(packageJson, (dependency) => dependency === "expo" || dependency === "expo-router")) return "expo";
|
|
6088
6096
|
if (hasAnyFile(cwd, REACT_ROUTER_CONFIG_FILES)) return "react-router";
|
|
6089
6097
|
if (hasAnyFile(cwd, VITE_CONFIG_FILES)) return "vite";
|
|
6090
6098
|
if (hasDependency$1(packageJson, (dependency) => dependency === "react" || dependency === "react-dom")) return "manual";
|
|
@@ -6092,15 +6100,16 @@ function detectProjectFramework(cwd = process.cwd()) {
|
|
|
6092
6100
|
}
|
|
6093
6101
|
function mapFrameworkToScaffoldMode(framework) {
|
|
6094
6102
|
if (framework === "next-app") return "next-app";
|
|
6103
|
+
if (framework === "expo") return "expo";
|
|
6095
6104
|
if (framework === "next-pages" || framework === "vite" || framework === "react-router" || framework === "tanstack-start" || framework === "manual") return "react";
|
|
6096
|
-
throw new Error(`Unsupported framework "${framework}" for kitcn init. Supported frameworks map to: next-app, next-pages, vite, react-router, tanstack-start, manual.`);
|
|
6105
|
+
throw new Error(`Unsupported framework "${framework}" for kitcn init. Supported frameworks map to: next-app, expo, next-pages, vite, react-router, tanstack-start, manual.`);
|
|
6097
6106
|
}
|
|
6098
6107
|
function resolveProjectScaffoldContext(params = {}) {
|
|
6099
6108
|
const cwd = params.cwd ?? process.cwd();
|
|
6100
|
-
const detectedFramework = (params.template === "next" ? "next-app" : params.template === "start" ? "tanstack-start" : params.template === "vite" ? "vite" : null) ?? detectProjectFramework(cwd);
|
|
6109
|
+
const detectedFramework = (params.template === "next" ? "next-app" : params.template === "expo" ? "expo" : params.template === "start" ? "tanstack-start" : params.template === "vite" ? "vite" : null) ?? detectProjectFramework(cwd);
|
|
6101
6110
|
if (!detectedFramework) {
|
|
6102
6111
|
if (params.allowMissing) return null;
|
|
6103
|
-
throw new Error("Could not detect a supported app scaffold. Supported modes currently start from `next`, `start`, or `vite`.");
|
|
6112
|
+
throw new Error("Could not detect a supported app scaffold. Supported modes currently start from `next`, `expo`, `start`, or `vite`.");
|
|
6104
6113
|
}
|
|
6105
6114
|
let mode;
|
|
6106
6115
|
try {
|
|
@@ -6132,6 +6141,23 @@ function resolveProjectScaffoldContext(params = {}) {
|
|
|
6132
6141
|
convexSiteUrlEnvKey: "NEXT_PUBLIC_CONVEX_SITE_URL"
|
|
6133
6142
|
};
|
|
6134
6143
|
}
|
|
6144
|
+
if (mode === "expo") {
|
|
6145
|
+
const appDir = posix.join(rootPrefix, "app").replace(LEADING_SLASH_RE, "");
|
|
6146
|
+
return {
|
|
6147
|
+
framework: "expo",
|
|
6148
|
+
mode,
|
|
6149
|
+
usesSrc,
|
|
6150
|
+
appDir,
|
|
6151
|
+
componentsDir,
|
|
6152
|
+
libDir,
|
|
6153
|
+
convexClientDir,
|
|
6154
|
+
tailwindCssPath: null,
|
|
6155
|
+
tsconfigAliasPath,
|
|
6156
|
+
clientSiteUrlEnvKey: "EXPO_PUBLIC_SITE_URL",
|
|
6157
|
+
convexUrlEnvKey: "EXPO_PUBLIC_CONVEX_URL",
|
|
6158
|
+
convexSiteUrlEnvKey: "EXPO_PUBLIC_CONVEX_SITE_URL"
|
|
6159
|
+
};
|
|
6160
|
+
}
|
|
6135
6161
|
const clientEntryFile = resolveReactClientEntryFile(cwd, usesSrc);
|
|
6136
6162
|
const viteConfigFile = VITE_CONFIG_FILES.find((relativePath) => fs.existsSync(resolve(cwd, relativePath)));
|
|
6137
6163
|
const tsconfigAppFile = ["tsconfig.app.json", "tsconfig.json"].find((relativePath) => fs.existsSync(resolve(cwd, relativePath)));
|
|
@@ -6467,6 +6493,51 @@ const createRegistryFile = (params) => {
|
|
|
6467
6493
|
};
|
|
6468
6494
|
};
|
|
6469
6495
|
|
|
6496
|
+
//#endregion
|
|
6497
|
+
//#region src/cli/registry/init/expo/init-expo-convex-provider.template.ts
|
|
6498
|
+
const INIT_EXPO_CONVEX_PROVIDER_TEMPLATE = `import { QueryClientProvider as TanstackQueryClientProvider } from '@tanstack/react-query';
|
|
6499
|
+
import {
|
|
6500
|
+
ConvexProvider,
|
|
6501
|
+
ConvexReactClient,
|
|
6502
|
+
getConvexQueryClientSingleton,
|
|
6503
|
+
getQueryClientSingleton,
|
|
6504
|
+
} from 'kitcn/react';
|
|
6505
|
+
import type { ReactNode } from 'react';
|
|
6506
|
+
|
|
6507
|
+
import { CRPCProvider } from '@/lib/convex/crpc';
|
|
6508
|
+
import { createQueryClient } from '@/lib/convex/query-client';
|
|
6509
|
+
|
|
6510
|
+
const convex = new ConvexReactClient(process.env.EXPO_PUBLIC_CONVEX_URL!);
|
|
6511
|
+
|
|
6512
|
+
export function AppConvexProvider({
|
|
6513
|
+
children,
|
|
6514
|
+
}: {
|
|
6515
|
+
children: ReactNode;
|
|
6516
|
+
}) {
|
|
6517
|
+
return (
|
|
6518
|
+
<ConvexProvider client={convex}>
|
|
6519
|
+
<QueryProvider>{children}</QueryProvider>
|
|
6520
|
+
</ConvexProvider>
|
|
6521
|
+
);
|
|
6522
|
+
}
|
|
6523
|
+
|
|
6524
|
+
function QueryProvider({ children }: { children: ReactNode }) {
|
|
6525
|
+
const queryClient = getQueryClientSingleton(createQueryClient);
|
|
6526
|
+
const convexQueryClient = getConvexQueryClientSingleton({
|
|
6527
|
+
convex,
|
|
6528
|
+
queryClient,
|
|
6529
|
+
});
|
|
6530
|
+
|
|
6531
|
+
return (
|
|
6532
|
+
<TanstackQueryClientProvider client={queryClient}>
|
|
6533
|
+
<CRPCProvider convexClient={convex} convexQueryClient={convexQueryClient}>
|
|
6534
|
+
{children}
|
|
6535
|
+
</CRPCProvider>
|
|
6536
|
+
</TanstackQueryClientProvider>
|
|
6537
|
+
);
|
|
6538
|
+
}
|
|
6539
|
+
`;
|
|
6540
|
+
|
|
6470
6541
|
//#endregion
|
|
6471
6542
|
//#region src/cli/scaffold-placeholders.ts
|
|
6472
6543
|
const FUNCTIONS_DIR_IMPORT_PLACEHOLDER$5 = "__KITCN_FUNCTIONS_DIR__";
|
|
@@ -7135,10 +7206,10 @@ const resolvePluginScaffoldFiles = (templates, roots, functionsDir, existingTemp
|
|
|
7135
7206
|
if (template.target === "lib") rootDir = roots.libRootDir;
|
|
7136
7207
|
else if (template.target === "functions") rootDir = roots.functionsRootDir;
|
|
7137
7208
|
else if (template.target === "app") {
|
|
7138
|
-
if (!roots.appRootDir) throw new Error(`${descriptor.label} scaffolding requires a supported app baseline. Run \`kitcn init --yes\` in a supported app, or bootstrap one with \`kitcn init -t <next|start|vite>\` first.`);
|
|
7209
|
+
if (!roots.appRootDir) throw new Error(`${descriptor.label} scaffolding requires a supported app baseline. Run \`kitcn init --yes\` in a supported app, or bootstrap one with \`kitcn init -t <next|expo|start|vite>\` first.`);
|
|
7139
7210
|
rootDir = roots.appRootDir;
|
|
7140
7211
|
} else {
|
|
7141
|
-
if (!roots.clientLibRootDir) throw new Error(`${descriptor.label} scaffolding requires a supported app baseline. Run \`kitcn init --yes\` in a supported app, or bootstrap one with \`kitcn init -t <next|start|vite>\` first.`);
|
|
7212
|
+
if (!roots.clientLibRootDir) throw new Error(`${descriptor.label} scaffolding requires a supported app baseline. Run \`kitcn init --yes\` in a supported app, or bootstrap one with \`kitcn init -t <next|expo|start|vite>\` first.`);
|
|
7142
7213
|
rootDir = roots.clientLibRootDir;
|
|
7143
7214
|
}
|
|
7144
7215
|
const mappedLockfilePath = existingTemplatePathMap?.[template.id];
|
|
@@ -8049,6 +8120,32 @@ export default defineAuth(() => ({
|
|
|
8049
8120
|
trustedOrigins: [getEnv().SITE_URL],
|
|
8050
8121
|
}));
|
|
8051
8122
|
`;
|
|
8123
|
+
const AUTH_EXPO_TEMPLATE = `import { expo } from '@better-auth/expo';
|
|
8124
|
+
import { convex } from 'kitcn/auth';
|
|
8125
|
+
import { getEnv } from '../lib/get-env';
|
|
8126
|
+
import authConfig from './auth.config';
|
|
8127
|
+
import { defineAuth } from './generated/auth';
|
|
8128
|
+
|
|
8129
|
+
export default defineAuth(() => ({
|
|
8130
|
+
emailAndPassword: {
|
|
8131
|
+
enabled: true,
|
|
8132
|
+
},
|
|
8133
|
+
baseURL: getEnv().CONVEX_SITE_URL ?? getEnv().SITE_URL,
|
|
8134
|
+
plugins: [
|
|
8135
|
+
expo(),
|
|
8136
|
+
convex({
|
|
8137
|
+
authConfig,
|
|
8138
|
+
jwks: getEnv().JWKS,
|
|
8139
|
+
}),
|
|
8140
|
+
],
|
|
8141
|
+
session: {
|
|
8142
|
+
expiresIn: 60 * 60 * 24 * 30,
|
|
8143
|
+
updateAge: 60 * 60 * 24 * 15,
|
|
8144
|
+
},
|
|
8145
|
+
telemetry: { enabled: false },
|
|
8146
|
+
trustedOrigins: [getEnv().SITE_URL],
|
|
8147
|
+
}));
|
|
8148
|
+
`;
|
|
8052
8149
|
const AUTH_CONVEX_TEMPLATE = `import { convex } from 'kitcn/auth';
|
|
8053
8150
|
import authConfig from './auth.config';
|
|
8054
8151
|
import { defineAuth } from './generated/auth';
|
|
@@ -8117,6 +8214,38 @@ export const authClient = createAuthClient({
|
|
|
8117
8214
|
plugins: [convexClient()],
|
|
8118
8215
|
});
|
|
8119
8216
|
|
|
8217
|
+
export const {
|
|
8218
|
+
useSignInMutationOptions,
|
|
8219
|
+
useSignOutMutationOptions,
|
|
8220
|
+
useSignUpMutationOptions,
|
|
8221
|
+
} = createAuthMutations(authClient);
|
|
8222
|
+
`;
|
|
8223
|
+
const AUTH_EXPO_CLIENT_TEMPLATE = `import { expoClient } from '@better-auth/expo/client';
|
|
8224
|
+
import Constants from 'expo-constants';
|
|
8225
|
+
import * as SecureStore from 'expo-secure-store';
|
|
8226
|
+
import { createAuthClient } from 'better-auth/react';
|
|
8227
|
+
import { convexClient } from 'kitcn/auth/client';
|
|
8228
|
+
import { createAuthMutations } from 'kitcn/react';
|
|
8229
|
+
import { Platform } from 'react-native';
|
|
8230
|
+
|
|
8231
|
+
const scheme = Constants.expoConfig?.scheme as string;
|
|
8232
|
+
|
|
8233
|
+
export const authClient = createAuthClient({
|
|
8234
|
+
baseURL: process.env.EXPO_PUBLIC_CONVEX_SITE_URL!,
|
|
8235
|
+
plugins: [
|
|
8236
|
+
convexClient(),
|
|
8237
|
+
...(Platform.OS === 'web'
|
|
8238
|
+
? []
|
|
8239
|
+
: [
|
|
8240
|
+
expoClient({
|
|
8241
|
+
scheme,
|
|
8242
|
+
storagePrefix: scheme,
|
|
8243
|
+
storage: SecureStore,
|
|
8244
|
+
}),
|
|
8245
|
+
]),
|
|
8246
|
+
],
|
|
8247
|
+
});
|
|
8248
|
+
|
|
8120
8249
|
export const {
|
|
8121
8250
|
useSignInMutationOptions,
|
|
8122
8251
|
useSignOutMutationOptions,
|
|
@@ -8372,6 +8501,293 @@ export const router = c.router;
|
|
|
8372
8501
|
`;
|
|
8373
8502
|
}
|
|
8374
8503
|
|
|
8504
|
+
//#endregion
|
|
8505
|
+
//#region src/cli/registry/items/auth/auth-expo-convex-provider.template.ts
|
|
8506
|
+
const AUTH_EXPO_CONVEX_PROVIDER_TEMPLATE = `import { QueryClientProvider as TanstackQueryClientProvider } from '@tanstack/react-query';
|
|
8507
|
+
import { useRouter } from 'expo-router';
|
|
8508
|
+
import { ConvexAuthProvider } from 'kitcn/auth/client';
|
|
8509
|
+
import {
|
|
8510
|
+
ConvexReactClient,
|
|
8511
|
+
getConvexQueryClientSingleton,
|
|
8512
|
+
getQueryClientSingleton,
|
|
8513
|
+
useAuthStore,
|
|
8514
|
+
} from 'kitcn/react';
|
|
8515
|
+
import type { ReactNode } from 'react';
|
|
8516
|
+
|
|
8517
|
+
import { authClient } from '@/lib/convex/auth-client';
|
|
8518
|
+
import { CRPCProvider } from '@/lib/convex/crpc';
|
|
8519
|
+
import { createQueryClient } from '@/lib/convex/query-client';
|
|
8520
|
+
|
|
8521
|
+
const convex = new ConvexReactClient(process.env.EXPO_PUBLIC_CONVEX_URL!);
|
|
8522
|
+
|
|
8523
|
+
export function AppConvexProvider({
|
|
8524
|
+
children,
|
|
8525
|
+
}: {
|
|
8526
|
+
children: ReactNode;
|
|
8527
|
+
}) {
|
|
8528
|
+
const router = useRouter();
|
|
8529
|
+
|
|
8530
|
+
return (
|
|
8531
|
+
<ConvexAuthProvider
|
|
8532
|
+
authClient={authClient}
|
|
8533
|
+
client={convex}
|
|
8534
|
+
onMutationUnauthorized={() => {
|
|
8535
|
+
router.push('/auth');
|
|
8536
|
+
}}
|
|
8537
|
+
onQueryUnauthorized={() => {
|
|
8538
|
+
router.push('/auth');
|
|
8539
|
+
}}
|
|
8540
|
+
>
|
|
8541
|
+
<QueryProvider>{children}</QueryProvider>
|
|
8542
|
+
</ConvexAuthProvider>
|
|
8543
|
+
);
|
|
8544
|
+
}
|
|
8545
|
+
|
|
8546
|
+
function QueryProvider({ children }: { children: ReactNode }) {
|
|
8547
|
+
const authStore = useAuthStore();
|
|
8548
|
+
const queryClient = getQueryClientSingleton(createQueryClient);
|
|
8549
|
+
const convexQueryClient = getConvexQueryClientSingleton({
|
|
8550
|
+
authStore,
|
|
8551
|
+
convex,
|
|
8552
|
+
queryClient,
|
|
8553
|
+
});
|
|
8554
|
+
|
|
8555
|
+
return (
|
|
8556
|
+
<TanstackQueryClientProvider client={queryClient}>
|
|
8557
|
+
<CRPCProvider convexClient={convex} convexQueryClient={convexQueryClient}>
|
|
8558
|
+
{children}
|
|
8559
|
+
</CRPCProvider>
|
|
8560
|
+
</TanstackQueryClientProvider>
|
|
8561
|
+
);
|
|
8562
|
+
}
|
|
8563
|
+
`;
|
|
8564
|
+
|
|
8565
|
+
//#endregion
|
|
8566
|
+
//#region src/cli/registry/items/auth/auth-expo-page.template.ts
|
|
8567
|
+
const AUTH_EXPO_PAGE_TEMPLATE = `import { useMutation } from '@tanstack/react-query';
|
|
8568
|
+
import { useAuth } from 'kitcn/react';
|
|
8569
|
+
import { useState } from 'react';
|
|
8570
|
+
import {
|
|
8571
|
+
Pressable,
|
|
8572
|
+
SafeAreaView,
|
|
8573
|
+
StyleSheet,
|
|
8574
|
+
Text,
|
|
8575
|
+
TextInput,
|
|
8576
|
+
View,
|
|
8577
|
+
} from 'react-native';
|
|
8578
|
+
|
|
8579
|
+
import {
|
|
8580
|
+
authClient,
|
|
8581
|
+
useSignInMutationOptions,
|
|
8582
|
+
useSignOutMutationOptions,
|
|
8583
|
+
useSignUpMutationOptions,
|
|
8584
|
+
} from '@/lib/convex/auth-client';
|
|
8585
|
+
|
|
8586
|
+
export default function AuthPage() {
|
|
8587
|
+
const { hasSession } = useAuth();
|
|
8588
|
+
const authSession = authClient.useSession();
|
|
8589
|
+
const session = authSession.data;
|
|
8590
|
+
const user = session?.user ?? null;
|
|
8591
|
+
const hasSignedInUser = hasSession || Boolean(user);
|
|
8592
|
+
const [mode, setMode] = useState<'signin' | 'signup'>('signin');
|
|
8593
|
+
const [name, setName] = useState('');
|
|
8594
|
+
const [email, setEmail] = useState('');
|
|
8595
|
+
const [password, setPassword] = useState('');
|
|
8596
|
+
|
|
8597
|
+
const signIn = useMutation(useSignInMutationOptions());
|
|
8598
|
+
const signUp = useMutation(useSignUpMutationOptions());
|
|
8599
|
+
const signOut = useMutation(useSignOutMutationOptions());
|
|
8600
|
+
|
|
8601
|
+
const errorMessage =
|
|
8602
|
+
signIn.error?.message ??
|
|
8603
|
+
signUp.error?.message ??
|
|
8604
|
+
signOut.error?.message ??
|
|
8605
|
+
null;
|
|
8606
|
+
const isPending =
|
|
8607
|
+
signIn.isPending || signUp.isPending || signOut.isPending;
|
|
8608
|
+
|
|
8609
|
+
function handleSubmit() {
|
|
8610
|
+
if (mode === 'signup') {
|
|
8611
|
+
signUp.mutate({
|
|
8612
|
+
email,
|
|
8613
|
+
name,
|
|
8614
|
+
password,
|
|
8615
|
+
});
|
|
8616
|
+
return;
|
|
8617
|
+
}
|
|
8618
|
+
|
|
8619
|
+
signIn.mutate({
|
|
8620
|
+
email,
|
|
8621
|
+
password,
|
|
8622
|
+
});
|
|
8623
|
+
}
|
|
8624
|
+
|
|
8625
|
+
if (hasSignedInUser) {
|
|
8626
|
+
return (
|
|
8627
|
+
<SafeAreaView style={styles.safeArea}>
|
|
8628
|
+
<View style={styles.screen}>
|
|
8629
|
+
<View style={styles.header}>
|
|
8630
|
+
<Text style={styles.kicker}>Signed in</Text>
|
|
8631
|
+
<Text style={styles.title}>
|
|
8632
|
+
{user?.name || user?.email || email}
|
|
8633
|
+
</Text>
|
|
8634
|
+
<Text style={styles.copy}>{user?.email || email}</Text>
|
|
8635
|
+
</View>
|
|
8636
|
+
<Pressable
|
|
8637
|
+
disabled={isPending}
|
|
8638
|
+
onPress={() => signOut.mutate()}
|
|
8639
|
+
style={[styles.button, isPending && styles.buttonDisabled]}
|
|
8640
|
+
>
|
|
8641
|
+
<Text style={styles.buttonText}>
|
|
8642
|
+
{signOut.isPending ? 'Signing out…' : 'Sign out'}
|
|
8643
|
+
</Text>
|
|
8644
|
+
</Pressable>
|
|
8645
|
+
</View>
|
|
8646
|
+
</SafeAreaView>
|
|
8647
|
+
);
|
|
8648
|
+
}
|
|
8649
|
+
|
|
8650
|
+
return (
|
|
8651
|
+
<SafeAreaView style={styles.safeArea}>
|
|
8652
|
+
<View style={styles.screen}>
|
|
8653
|
+
<View style={styles.header}>
|
|
8654
|
+
<Text style={styles.kicker}>Auth demo</Text>
|
|
8655
|
+
<Text style={styles.title}>
|
|
8656
|
+
{mode === 'signup' ? 'Create an account' : 'Sign in'}
|
|
8657
|
+
</Text>
|
|
8658
|
+
<Text style={styles.copy}>
|
|
8659
|
+
Minimal Better Auth wiring on top of the Expo baseline.
|
|
8660
|
+
</Text>
|
|
8661
|
+
</View>
|
|
8662
|
+
|
|
8663
|
+
<View style={styles.form}>
|
|
8664
|
+
{mode === 'signup' ? (
|
|
8665
|
+
<TextInput
|
|
8666
|
+
autoCapitalize="words"
|
|
8667
|
+
onChangeText={setName}
|
|
8668
|
+
placeholder="Name"
|
|
8669
|
+
style={styles.input}
|
|
8670
|
+
value={name}
|
|
8671
|
+
/>
|
|
8672
|
+
) : null}
|
|
8673
|
+
<TextInput
|
|
8674
|
+
autoCapitalize="none"
|
|
8675
|
+
keyboardType="email-address"
|
|
8676
|
+
onChangeText={setEmail}
|
|
8677
|
+
placeholder="Email"
|
|
8678
|
+
style={styles.input}
|
|
8679
|
+
value={email}
|
|
8680
|
+
/>
|
|
8681
|
+
<TextInput
|
|
8682
|
+
onChangeText={setPassword}
|
|
8683
|
+
placeholder="Password"
|
|
8684
|
+
secureTextEntry
|
|
8685
|
+
style={styles.input}
|
|
8686
|
+
value={password}
|
|
8687
|
+
/>
|
|
8688
|
+
<Pressable
|
|
8689
|
+
disabled={isPending}
|
|
8690
|
+
onPress={handleSubmit}
|
|
8691
|
+
style={[styles.button, isPending && styles.buttonDisabled]}
|
|
8692
|
+
>
|
|
8693
|
+
<Text style={styles.buttonText}>
|
|
8694
|
+
{isPending
|
|
8695
|
+
? 'Working…'
|
|
8696
|
+
: mode === 'signup'
|
|
8697
|
+
? 'Create account'
|
|
8698
|
+
: 'Sign in'}
|
|
8699
|
+
</Text>
|
|
8700
|
+
</Pressable>
|
|
8701
|
+
</View>
|
|
8702
|
+
|
|
8703
|
+
<Pressable
|
|
8704
|
+
onPress={() => setMode(mode === 'signin' ? 'signup' : 'signin')}
|
|
8705
|
+
>
|
|
8706
|
+
<Text style={styles.link}>
|
|
8707
|
+
{mode === 'signin'
|
|
8708
|
+
? "Don't have an account? Sign up"
|
|
8709
|
+
: 'Already have an account? Sign in'}
|
|
8710
|
+
</Text>
|
|
8711
|
+
</Pressable>
|
|
8712
|
+
|
|
8713
|
+
{errorMessage ? <Text style={styles.error}>{errorMessage}</Text> : null}
|
|
8714
|
+
</View>
|
|
8715
|
+
</SafeAreaView>
|
|
8716
|
+
);
|
|
8717
|
+
}
|
|
8718
|
+
|
|
8719
|
+
const styles = StyleSheet.create({
|
|
8720
|
+
safeArea: {
|
|
8721
|
+
flex: 1,
|
|
8722
|
+
backgroundColor: '#ffffff',
|
|
8723
|
+
},
|
|
8724
|
+
screen: {
|
|
8725
|
+
flex: 1,
|
|
8726
|
+
gap: 20,
|
|
8727
|
+
justifyContent: 'center',
|
|
8728
|
+
paddingHorizontal: 24,
|
|
8729
|
+
paddingVertical: 32,
|
|
8730
|
+
},
|
|
8731
|
+
header: {
|
|
8732
|
+
gap: 8,
|
|
8733
|
+
},
|
|
8734
|
+
kicker: {
|
|
8735
|
+
color: '#6b7280',
|
|
8736
|
+
fontSize: 12,
|
|
8737
|
+
fontWeight: '600',
|
|
8738
|
+
letterSpacing: 1.5,
|
|
8739
|
+
textTransform: 'uppercase',
|
|
8740
|
+
},
|
|
8741
|
+
title: {
|
|
8742
|
+
color: '#111827',
|
|
8743
|
+
fontSize: 28,
|
|
8744
|
+
fontWeight: '700',
|
|
8745
|
+
},
|
|
8746
|
+
copy: {
|
|
8747
|
+
color: '#4b5563',
|
|
8748
|
+
fontSize: 15,
|
|
8749
|
+
lineHeight: 22,
|
|
8750
|
+
},
|
|
8751
|
+
form: {
|
|
8752
|
+
gap: 12,
|
|
8753
|
+
},
|
|
8754
|
+
input: {
|
|
8755
|
+
borderColor: '#d1d5db',
|
|
8756
|
+
borderRadius: 12,
|
|
8757
|
+
borderWidth: 1,
|
|
8758
|
+
fontSize: 16,
|
|
8759
|
+
minHeight: 48,
|
|
8760
|
+
paddingHorizontal: 14,
|
|
8761
|
+
paddingVertical: 12,
|
|
8762
|
+
},
|
|
8763
|
+
button: {
|
|
8764
|
+
alignItems: 'center',
|
|
8765
|
+
backgroundColor: '#111827',
|
|
8766
|
+
borderRadius: 12,
|
|
8767
|
+
minHeight: 48,
|
|
8768
|
+
justifyContent: 'center',
|
|
8769
|
+
paddingHorizontal: 16,
|
|
8770
|
+
},
|
|
8771
|
+
buttonDisabled: {
|
|
8772
|
+
opacity: 0.5,
|
|
8773
|
+
},
|
|
8774
|
+
buttonText: {
|
|
8775
|
+
color: '#ffffff',
|
|
8776
|
+
fontSize: 15,
|
|
8777
|
+
fontWeight: '600',
|
|
8778
|
+
},
|
|
8779
|
+
link: {
|
|
8780
|
+
color: '#4b5563',
|
|
8781
|
+
fontSize: 14,
|
|
8782
|
+
textDecorationLine: 'underline',
|
|
8783
|
+
},
|
|
8784
|
+
error: {
|
|
8785
|
+
color: '#dc2626',
|
|
8786
|
+
fontSize: 14,
|
|
8787
|
+
},
|
|
8788
|
+
});
|
|
8789
|
+
`;
|
|
8790
|
+
|
|
8375
8791
|
//#endregion
|
|
8376
8792
|
//#region src/cli/registry/items/auth/auth-next-route.template.ts
|
|
8377
8793
|
const AUTH_NEXT_ROUTE_TEMPLATE = `import { handler } from '@/lib/convex/server';
|
|
@@ -8610,6 +9026,136 @@ function QueryProvider({ children }: { children: ReactNode }) {
|
|
|
8610
9026
|
|
|
8611
9027
|
//#endregion
|
|
8612
9028
|
//#region src/cli/registry/items/auth/auth-schema.template.ts
|
|
9029
|
+
const DEFAULT_MANAGED_AUTH_EXTENSION_TEMPLATE = `// This file is auto-generated. Do not edit this file manually.
|
|
9030
|
+
// To regenerate the schema, run:
|
|
9031
|
+
// \`npx kitcn add auth --yes\`
|
|
9032
|
+
|
|
9033
|
+
import {
|
|
9034
|
+
boolean,
|
|
9035
|
+
convexTable,
|
|
9036
|
+
defineSchemaExtension,
|
|
9037
|
+
index,
|
|
9038
|
+
text,
|
|
9039
|
+
timestamp,
|
|
9040
|
+
} from "kitcn/orm";
|
|
9041
|
+
|
|
9042
|
+
export const userTable = convexTable(
|
|
9043
|
+
"user",
|
|
9044
|
+
{
|
|
9045
|
+
name: text().notNull(),
|
|
9046
|
+
email: text().notNull().unique(),
|
|
9047
|
+
emailVerified: boolean().notNull(),
|
|
9048
|
+
image: text(),
|
|
9049
|
+
createdAt: timestamp().notNull(),
|
|
9050
|
+
updatedAt: timestamp().notNull(),
|
|
9051
|
+
userId: text(),
|
|
9052
|
+
},
|
|
9053
|
+
(userTable) => [
|
|
9054
|
+
index("email_name").on(userTable.email, userTable.name),
|
|
9055
|
+
index("name").on(userTable.name),
|
|
9056
|
+
]
|
|
9057
|
+
);
|
|
9058
|
+
|
|
9059
|
+
export const sessionTable = convexTable(
|
|
9060
|
+
"session",
|
|
9061
|
+
{
|
|
9062
|
+
expiresAt: timestamp().notNull(),
|
|
9063
|
+
token: text().notNull().unique(),
|
|
9064
|
+
createdAt: timestamp().notNull(),
|
|
9065
|
+
updatedAt: timestamp().notNull(),
|
|
9066
|
+
ipAddress: text(),
|
|
9067
|
+
userAgent: text(),
|
|
9068
|
+
userId: text().notNull().references(() => userTable.id),
|
|
9069
|
+
},
|
|
9070
|
+
(sessionTable) => [
|
|
9071
|
+
index("expiresAt").on(sessionTable.expiresAt),
|
|
9072
|
+
index("expiresAt_userId").on(sessionTable.expiresAt, sessionTable.userId),
|
|
9073
|
+
index("userId").on(sessionTable.userId),
|
|
9074
|
+
]
|
|
9075
|
+
);
|
|
9076
|
+
|
|
9077
|
+
export const accountTable = convexTable(
|
|
9078
|
+
"account",
|
|
9079
|
+
{
|
|
9080
|
+
accountId: text().notNull(),
|
|
9081
|
+
providerId: text().notNull(),
|
|
9082
|
+
userId: text().notNull().references(() => userTable.id),
|
|
9083
|
+
accessToken: text(),
|
|
9084
|
+
refreshToken: text(),
|
|
9085
|
+
idToken: text(),
|
|
9086
|
+
accessTokenExpiresAt: timestamp(),
|
|
9087
|
+
refreshTokenExpiresAt: timestamp(),
|
|
9088
|
+
scope: text(),
|
|
9089
|
+
password: text(),
|
|
9090
|
+
createdAt: timestamp().notNull(),
|
|
9091
|
+
updatedAt: timestamp().notNull(),
|
|
9092
|
+
},
|
|
9093
|
+
(accountTable) => [
|
|
9094
|
+
index("accountId").on(accountTable.accountId),
|
|
9095
|
+
index("accountId_providerId").on(accountTable.accountId, accountTable.providerId),
|
|
9096
|
+
index("providerId_userId").on(accountTable.providerId, accountTable.userId),
|
|
9097
|
+
index("userId").on(accountTable.userId),
|
|
9098
|
+
]
|
|
9099
|
+
);
|
|
9100
|
+
|
|
9101
|
+
export const verificationTable = convexTable(
|
|
9102
|
+
"verification",
|
|
9103
|
+
{
|
|
9104
|
+
identifier: text().notNull(),
|
|
9105
|
+
value: text().notNull(),
|
|
9106
|
+
expiresAt: timestamp().notNull(),
|
|
9107
|
+
createdAt: timestamp().notNull(),
|
|
9108
|
+
updatedAt: timestamp().notNull(),
|
|
9109
|
+
},
|
|
9110
|
+
(verificationTable) => [
|
|
9111
|
+
index("expiresAt").on(verificationTable.expiresAt),
|
|
9112
|
+
index("identifier").on(verificationTable.identifier),
|
|
9113
|
+
]
|
|
9114
|
+
);
|
|
9115
|
+
|
|
9116
|
+
export const jwksTable = convexTable(
|
|
9117
|
+
"jwks",
|
|
9118
|
+
{
|
|
9119
|
+
publicKey: text().notNull(),
|
|
9120
|
+
privateKey: text().notNull(),
|
|
9121
|
+
createdAt: timestamp().notNull(),
|
|
9122
|
+
expiresAt: timestamp(),
|
|
9123
|
+
}
|
|
9124
|
+
);
|
|
9125
|
+
|
|
9126
|
+
export function authExtension() {
|
|
9127
|
+
return defineSchemaExtension("auth", {
|
|
9128
|
+
user: userTable,
|
|
9129
|
+
session: sessionTable,
|
|
9130
|
+
account: accountTable,
|
|
9131
|
+
verification: verificationTable,
|
|
9132
|
+
jwks: jwksTable,
|
|
9133
|
+
}).relations((r) => ({
|
|
9134
|
+
user: {
|
|
9135
|
+
sessions: r.many.session({
|
|
9136
|
+
from: r.user.id,
|
|
9137
|
+
to: r.session.userId,
|
|
9138
|
+
}),
|
|
9139
|
+
accounts: r.many.account({
|
|
9140
|
+
from: r.user.id,
|
|
9141
|
+
to: r.account.userId,
|
|
9142
|
+
}),
|
|
9143
|
+
},
|
|
9144
|
+
session: {
|
|
9145
|
+
user: r.one.user({
|
|
9146
|
+
from: r.session.userId,
|
|
9147
|
+
to: r.user.id,
|
|
9148
|
+
}),
|
|
9149
|
+
},
|
|
9150
|
+
account: {
|
|
9151
|
+
user: r.one.user({
|
|
9152
|
+
from: r.account.userId,
|
|
9153
|
+
to: r.user.id,
|
|
9154
|
+
}),
|
|
9155
|
+
},
|
|
9156
|
+
}));
|
|
9157
|
+
}
|
|
9158
|
+
`;
|
|
8613
9159
|
const AUTH_CONVEX_SCHEMA_TEMPLATE = `import { defineTable } from 'convex/server';
|
|
8614
9160
|
import { v } from 'convex/values';
|
|
8615
9161
|
|
|
@@ -8955,29 +9501,6 @@ export function runServerCall<T>(fn: (caller: ServerCaller) => Promise<T> | T) {
|
|
|
8955
9501
|
}
|
|
8956
9502
|
`;
|
|
8957
9503
|
|
|
8958
|
-
//#endregion
|
|
8959
|
-
//#region src/auth/auth-config.ts
|
|
8960
|
-
const createPublicJwks = (jwks, options) => {
|
|
8961
|
-
const keyPairConfig = options?.jwks?.keyPairConfig;
|
|
8962
|
-
const defaultCrv = keyPairConfig && "crv" in keyPairConfig ? keyPairConfig.crv : void 0;
|
|
8963
|
-
return { keys: jwks.map((keySet) => ({
|
|
8964
|
-
alg: keySet.alg ?? options?.jwks?.keyPairConfig?.alg ?? "EdDSA",
|
|
8965
|
-
crv: keySet.crv ?? defaultCrv,
|
|
8966
|
-
...JSON.parse(keySet.publicKey),
|
|
8967
|
-
kid: keySet.id
|
|
8968
|
-
})) };
|
|
8969
|
-
};
|
|
8970
|
-
const getAuthConfigProvider = (opts) => {
|
|
8971
|
-
const parsedJwks = opts?.jwks ? JSON.parse(opts.jwks) : void 0;
|
|
8972
|
-
return {
|
|
8973
|
-
type: "customJwt",
|
|
8974
|
-
issuer: `${process.env.CONVEX_SITE_URL}`,
|
|
8975
|
-
applicationID: "convex",
|
|
8976
|
-
algorithm: "RS256",
|
|
8977
|
-
jwks: parsedJwks ? `data:text/plain;charset=utf-8;base64,${btoa(JSON.stringify(createPublicJwks(parsedJwks)))}` : `${process.env.CONVEX_SITE_URL}${opts?.basePath ?? "/api/auth"}/convex/jwks`
|
|
8978
|
-
};
|
|
8979
|
-
};
|
|
8980
|
-
|
|
8981
9504
|
//#endregion
|
|
8982
9505
|
//#region src/auth/create-schema.ts
|
|
8983
9506
|
const indexFields = {
|
|
@@ -9397,7 +9920,6 @@ const DEFAULT_AUTH_SCHEMA_ENV = {
|
|
|
9397
9920
|
SITE_URL: "http://localhost:3000"
|
|
9398
9921
|
};
|
|
9399
9922
|
const loadGetAuthTables = async () => (await import("better-auth/db")).getAuthTables;
|
|
9400
|
-
const loadConvexAuthPlugin = async () => (await import("./convex-plugin-tWTDqoKJ.mjs")).convex;
|
|
9401
9923
|
const ts$1 = createTypeScriptProxy();
|
|
9402
9924
|
const withAuthSchemaEnv = async (run) => {
|
|
9403
9925
|
const globalScope = globalThis;
|
|
@@ -9493,16 +10015,11 @@ const renderManagedAuthSchemaUnits = async ({ authOptions }) => parseRootSchemaU
|
|
|
9493
10015
|
kind: "extension",
|
|
9494
10016
|
outputPath: "convex/lib/plugins/auth/schema.ts"
|
|
9495
10017
|
}));
|
|
9496
|
-
const
|
|
9497
|
-
const
|
|
9498
|
-
const
|
|
9499
|
-
|
|
9500
|
-
return
|
|
9501
|
-
baseURL: process.env.SITE_URL,
|
|
9502
|
-
emailAndPassword: { enabled: true },
|
|
9503
|
-
plugins: [convex({ authConfig: { providers: [provider] } })],
|
|
9504
|
-
trustedOrigins: [process.env.SITE_URL]
|
|
9505
|
-
}));
|
|
10018
|
+
const renderDefaultManagedAuthSchemaUnits = () => parseRootSchemaUnitsFromExtension(DEFAULT_MANAGED_AUTH_EXTENSION_TEMPLATE);
|
|
10019
|
+
const resolveManagedAuthSchemaUnits = async ({ authDefinitionPath, loadAuthOptions = loadAuthOptionsFromDefinition, renderManagedUnits = renderManagedAuthSchemaUnits }) => {
|
|
10020
|
+
const authOptions = await loadAuthOptions(authDefinitionPath);
|
|
10021
|
+
if (!authOptions) return renderDefaultManagedAuthSchemaUnits();
|
|
10022
|
+
return renderManagedUnits({ authOptions });
|
|
9506
10023
|
};
|
|
9507
10024
|
const loadAuthOptionsFromDefinition = async (authDefinitionPath) => {
|
|
9508
10025
|
if (!fs.existsSync(authDefinitionPath)) return null;
|
|
@@ -9578,14 +10095,24 @@ const AUTH_PROVIDER_REACT_NODE_IMPORT_RE = /import\s+type\s+\{\s*ReactNode\s*\}\
|
|
|
9578
10095
|
const AUTH_CONVEX_NEXT_PROVIDER_RETURN_RE = /<ConvexProvider client=\{convex\}>[\s\S]*?<\/ConvexProvider>/;
|
|
9579
10096
|
const AUTH_CONVEX_REACT_PROVIDER_OPEN_RE = /<ConvexProvider client=\{convex\}>/;
|
|
9580
10097
|
const AUTH_CONVEX_REACT_PROVIDER_CLOSE_RE = /<\/ConvexProvider>/;
|
|
9581
|
-
const AUTH_ENV_FIELDS = [
|
|
9582
|
-
|
|
9583
|
-
|
|
9584
|
-
|
|
9585
|
-
|
|
9586
|
-
|
|
9587
|
-
|
|
9588
|
-
|
|
10098
|
+
const AUTH_ENV_FIELDS = [
|
|
10099
|
+
{
|
|
10100
|
+
bootstrap: { kind: "generated-secret" },
|
|
10101
|
+
key: "BETTER_AUTH_SECRET",
|
|
10102
|
+
schema: "z.string().optional()"
|
|
10103
|
+
},
|
|
10104
|
+
{
|
|
10105
|
+
key: "JWKS",
|
|
10106
|
+
schema: "z.string().optional()"
|
|
10107
|
+
},
|
|
10108
|
+
{
|
|
10109
|
+
key: "CONVEX_SITE_URL",
|
|
10110
|
+
schema: "z.string().optional()"
|
|
10111
|
+
}
|
|
10112
|
+
];
|
|
10113
|
+
const BETTER_AUTH_EXPO_INSTALL_SPEC = "@better-auth/expo@1.6.5";
|
|
10114
|
+
const EXPO_SECURE_STORE_INSTALL_SPEC = "expo-secure-store@~55.0.8";
|
|
10115
|
+
const EXPO_NETWORK_INSTALL_SPEC = "expo-network@~55.0.8";
|
|
9589
10116
|
const AUTH_FILES = [
|
|
9590
10117
|
createRegistryFile({
|
|
9591
10118
|
id: "auth-config",
|
|
@@ -9652,7 +10179,7 @@ async function buildAuthSchemaRegistrationPlanFile(params) {
|
|
|
9652
10179
|
if (params.preset === "convex") return buildAuthConvexSchemaPlanFile(params);
|
|
9653
10180
|
const schemaPath = getSchemaFilePath(params.functionsDir);
|
|
9654
10181
|
const source = fs.readFileSync(schemaPath, "utf8");
|
|
9655
|
-
const
|
|
10182
|
+
const authDefinitionPath = resolve(params.functionsDir, "auth.ts");
|
|
9656
10183
|
const authSchemaLock = params.lockfile.plugins.auth?.schema ?? null;
|
|
9657
10184
|
const result = await reconcileRootSchemaOwnership({
|
|
9658
10185
|
claimMatchingManaged: params.applyScope === "schema" && authSchemaLock === null,
|
|
@@ -9664,7 +10191,10 @@ async function buildAuthSchemaRegistrationPlanFile(params) {
|
|
|
9664
10191
|
promptAdapter: params.promptAdapter,
|
|
9665
10192
|
schemaPath,
|
|
9666
10193
|
source,
|
|
9667
|
-
tables: await
|
|
10194
|
+
tables: await resolveManagedAuthSchemaUnits({
|
|
10195
|
+
authDefinitionPath,
|
|
10196
|
+
loadAuthOptions: loadAuthOptionsFromDefinition
|
|
10197
|
+
}),
|
|
9668
10198
|
yes: params.yes
|
|
9669
10199
|
});
|
|
9670
10200
|
return {
|
|
@@ -9740,7 +10270,16 @@ app.use(
|
|
|
9740
10270
|
}
|
|
9741
10271
|
function buildAuthProviderPlanFile(params) {
|
|
9742
10272
|
const projectContext = params.roots.projectContext;
|
|
9743
|
-
if (!projectContext) throw new Error("Auth scaffolding requires a supported app baseline. Run `kitcn init --yes` in a supported app, or bootstrap one with `kitcn init -t <next|start|vite>` first.");
|
|
10273
|
+
if (!projectContext) throw new Error("Auth scaffolding requires a supported app baseline. Run `kitcn init --yes` in a supported app, or bootstrap one with `kitcn init -t <next|expo|start|vite>` first.");
|
|
10274
|
+
if (projectContext.framework === "expo") return createPlanFile({
|
|
10275
|
+
kind: "scaffold",
|
|
10276
|
+
filePath: resolve(process.cwd(), projectContext.convexClientDir, "convex-provider.tsx"),
|
|
10277
|
+
content: AUTH_EXPO_CONVEX_PROVIDER_TEMPLATE,
|
|
10278
|
+
managedBaselineContent: INIT_EXPO_CONVEX_PROVIDER_TEMPLATE,
|
|
10279
|
+
createReason: "Create auth-aware kitcn provider for the Expo scaffold.",
|
|
10280
|
+
updateReason: "Update kitcn provider with auth-aware client wiring.",
|
|
10281
|
+
skipReason: "kitcn provider already matches the auth scaffold."
|
|
10282
|
+
});
|
|
9744
10283
|
if (projectContext.framework === "tanstack-start") return createPlanFile({
|
|
9745
10284
|
kind: "scaffold",
|
|
9746
10285
|
filePath: resolve(process.cwd(), projectContext.convexClientDir, "convex-provider.tsx"),
|
|
@@ -9835,6 +10374,18 @@ function buildAuthStartPagePlanFile(params) {
|
|
|
9835
10374
|
skipReason: "The Start auth demo route already exists."
|
|
9836
10375
|
});
|
|
9837
10376
|
}
|
|
10377
|
+
function buildAuthExpoPagePlanFile(params) {
|
|
10378
|
+
const projectContext = params.roots.projectContext;
|
|
10379
|
+
if (!projectContext || projectContext.framework !== "expo") throw new Error("Auth scaffolding requires a supported Expo shell.");
|
|
10380
|
+
return createPlanFile({
|
|
10381
|
+
kind: "scaffold",
|
|
10382
|
+
filePath: resolve(process.cwd(), projectContext.appDir, "auth.tsx"),
|
|
10383
|
+
content: AUTH_EXPO_PAGE_TEMPLATE,
|
|
10384
|
+
createReason: "Create the Expo auth demo route.",
|
|
10385
|
+
updateReason: "Update the Expo auth demo route.",
|
|
10386
|
+
skipReason: "The Expo auth demo route already exists."
|
|
10387
|
+
});
|
|
10388
|
+
}
|
|
9838
10389
|
function buildAuthConvexLocalEnvPlanFile(params) {
|
|
9839
10390
|
const envPath = resolve(params.functionsDir, ".env");
|
|
9840
10391
|
return createPlanFile({
|
|
@@ -9993,6 +10544,25 @@ const authRegistryItem = defineInternalRegistryItem({
|
|
|
9993
10544
|
})),
|
|
9994
10545
|
resolveTemplates: ({ roots, templates }) => {
|
|
9995
10546
|
if (!roots.projectContext || roots.projectContext.mode === "next-app") return templates;
|
|
10547
|
+
if (roots.projectContext.framework === "expo") return templates.filter((template) => template.id !== "auth-page").map((template) => {
|
|
10548
|
+
if (template.id === "auth-runtime") return {
|
|
10549
|
+
...template,
|
|
10550
|
+
content: AUTH_EXPO_TEMPLATE,
|
|
10551
|
+
dependencyHintMessage: "Expo auth runtime needs the Better Auth Expo plugin.",
|
|
10552
|
+
dependencyHints: [OPENTELEMETRY_API_INSTALL_SPEC, BETTER_AUTH_EXPO_INSTALL_SPEC]
|
|
10553
|
+
};
|
|
10554
|
+
if (template.id === "auth-client") return {
|
|
10555
|
+
...template,
|
|
10556
|
+
content: AUTH_EXPO_CLIENT_TEMPLATE,
|
|
10557
|
+
dependencyHintMessage: "Expo auth client needs native Better Auth and Expo storage dependencies.",
|
|
10558
|
+
dependencyHints: [
|
|
10559
|
+
BETTER_AUTH_EXPO_INSTALL_SPEC,
|
|
10560
|
+
EXPO_SECURE_STORE_INSTALL_SPEC,
|
|
10561
|
+
EXPO_NETWORK_INSTALL_SPEC
|
|
10562
|
+
]
|
|
10563
|
+
};
|
|
10564
|
+
return template;
|
|
10565
|
+
});
|
|
9996
10566
|
if (roots.projectContext.framework === "tanstack-start") return templates.filter((template) => template.id !== "auth-page" && template.id !== "auth-page-convex").map((template) => {
|
|
9997
10567
|
if (template.id === "auth-client") return {
|
|
9998
10568
|
...template,
|
|
@@ -10029,6 +10599,7 @@ const authRegistryItem = defineInternalRegistryItem({
|
|
|
10029
10599
|
buildAuthProviderPlanFile(params)
|
|
10030
10600
|
];
|
|
10031
10601
|
if (roots.projectContext?.mode === "next-app") files.push(buildAuthNextServerPlanFile(params), buildAuthNextRoutePlanFile(params));
|
|
10602
|
+
else if (roots.projectContext?.framework === "expo") files.push(buildAuthExpoPagePlanFile(params));
|
|
10032
10603
|
else if (roots.projectContext?.framework === "tanstack-start") files.push(buildAuthStartServerPlanFile(params), buildAuthStartRoutePlanFile(params), buildAuthStartServerCallPlanFile(params), buildAuthStartPagePlanFile(params));
|
|
10033
10604
|
return files;
|
|
10034
10605
|
},
|
|
@@ -11680,6 +12251,348 @@ const resolvePluginDocTopic = (topic) => {
|
|
|
11680
12251
|
};
|
|
11681
12252
|
};
|
|
11682
12253
|
|
|
12254
|
+
//#endregion
|
|
12255
|
+
//#region src/cli/registry/init/expo/init-expo-crpc.template.ts
|
|
12256
|
+
const INIT_EXPO_CRPC_TEMPLATE = `import { api } from '@convex/api';
|
|
12257
|
+
import { createCRPCContext } from 'kitcn/react';
|
|
12258
|
+
|
|
12259
|
+
export const { CRPCProvider, useCRPC, useCRPCClient } = createCRPCContext({
|
|
12260
|
+
api,
|
|
12261
|
+
convexSiteUrl: process.env.EXPO_PUBLIC_CONVEX_SITE_URL!,
|
|
12262
|
+
});
|
|
12263
|
+
`;
|
|
12264
|
+
|
|
12265
|
+
//#endregion
|
|
12266
|
+
//#region src/cli/registry/init/expo/init-expo-env.template.ts
|
|
12267
|
+
const INIT_EXPO_ENV_DEFAULTS = {
|
|
12268
|
+
EXPO_PUBLIC_CONVEX_URL: "http://127.0.0.1:3210",
|
|
12269
|
+
EXPO_PUBLIC_CONVEX_SITE_URL: "http://127.0.0.1:3211",
|
|
12270
|
+
EXPO_PUBLIC_SITE_URL: "http://localhost:3000"
|
|
12271
|
+
};
|
|
12272
|
+
function renderInitExpoEnvTemplate(source) {
|
|
12273
|
+
const existing = source ? parse(source) : {};
|
|
12274
|
+
const lines = Object.entries(INIT_EXPO_ENV_DEFAULTS).map(([key, value]) => `${key}=${existing[key] ?? value}`);
|
|
12275
|
+
for (const [key, value] of Object.entries(existing)) if (!(key in INIT_EXPO_ENV_DEFAULTS)) lines.push(`${key}=${value}`);
|
|
12276
|
+
return `${lines.join("\n")}\n`;
|
|
12277
|
+
}
|
|
12278
|
+
|
|
12279
|
+
//#endregion
|
|
12280
|
+
//#region src/cli/registry/init/expo/init-expo-env-types.template.ts
|
|
12281
|
+
const INIT_EXPO_ENV_TYPES_TEMPLATE = `/// <reference types="expo/types" />
|
|
12282
|
+
|
|
12283
|
+
declare module '*.module.css' {
|
|
12284
|
+
const classes: Record<string, string>;
|
|
12285
|
+
export default classes;
|
|
12286
|
+
}
|
|
12287
|
+
`;
|
|
12288
|
+
|
|
12289
|
+
//#endregion
|
|
12290
|
+
//#region src/cli/registry/init/expo/init-expo-explore.template.ts
|
|
12291
|
+
const INIT_EXPO_EXPLORE_TEMPLATE = `import { Redirect } from 'expo-router';
|
|
12292
|
+
|
|
12293
|
+
export default function ExploreScreen() {
|
|
12294
|
+
return <Redirect href="/" />;
|
|
12295
|
+
}
|
|
12296
|
+
`;
|
|
12297
|
+
|
|
12298
|
+
//#endregion
|
|
12299
|
+
//#region src/cli/registry/init/expo/init-expo-gitignore.template.ts
|
|
12300
|
+
const EXPO_ENV_TYPES_IGNORE_LINE = "expo-env.d.ts";
|
|
12301
|
+
const LINE_SPLIT_RE = /\r?\n/;
|
|
12302
|
+
function renderInitExpoGitignoreTemplate(source = "") {
|
|
12303
|
+
return `${source.split(LINE_SPLIT_RE).map((line) => line.trimEnd()).filter((line) => line.length > 0).filter((line) => line !== EXPO_ENV_TYPES_IGNORE_LINE).join("\n")}\n`;
|
|
12304
|
+
}
|
|
12305
|
+
|
|
12306
|
+
//#endregion
|
|
12307
|
+
//#region src/cli/registry/init/expo/init-expo-layout.template.ts
|
|
12308
|
+
const INIT_EXPO_LAYOUT_TEMPLATE = `import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
|
|
12309
|
+
import { Stack } from 'expo-router';
|
|
12310
|
+
import { StatusBar } from 'expo-status-bar';
|
|
12311
|
+
import { useColorScheme } from 'react-native';
|
|
12312
|
+
|
|
12313
|
+
import { Providers } from '@/components/providers';
|
|
12314
|
+
|
|
12315
|
+
export default function RootLayout() {
|
|
12316
|
+
const colorScheme = useColorScheme();
|
|
12317
|
+
|
|
12318
|
+
return (
|
|
12319
|
+
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
|
|
12320
|
+
<Providers>
|
|
12321
|
+
<Stack>
|
|
12322
|
+
<Stack.Screen name="index" options={{ title: 'Messages' }} />
|
|
12323
|
+
</Stack>
|
|
12324
|
+
<StatusBar style={colorScheme === 'dark' ? 'light' : 'dark'} />
|
|
12325
|
+
</Providers>
|
|
12326
|
+
</ThemeProvider>
|
|
12327
|
+
);
|
|
12328
|
+
}
|
|
12329
|
+
`;
|
|
12330
|
+
|
|
12331
|
+
//#endregion
|
|
12332
|
+
//#region src/cli/registry/init/expo/init-expo-messages-screen.template.ts
|
|
12333
|
+
const INIT_EXPO_MESSAGES_SCREEN_TEMPLATE = `import { useMutation, useQuery } from '@tanstack/react-query';
|
|
12334
|
+
import { useState } from 'react';
|
|
12335
|
+
import {
|
|
12336
|
+
Pressable,
|
|
12337
|
+
SafeAreaView,
|
|
12338
|
+
ScrollView,
|
|
12339
|
+
StyleSheet,
|
|
12340
|
+
Text,
|
|
12341
|
+
TextInput,
|
|
12342
|
+
View,
|
|
12343
|
+
} from 'react-native';
|
|
12344
|
+
|
|
12345
|
+
import { useCRPC } from '@/lib/convex/crpc';
|
|
12346
|
+
|
|
12347
|
+
export default function MessagesScreen() {
|
|
12348
|
+
const crpc = useCRPC();
|
|
12349
|
+
const [draft, setDraft] = useState('');
|
|
12350
|
+
const messagesQuery = useQuery(crpc.messages.list.queryOptions());
|
|
12351
|
+
const createMessage = useMutation(crpc.messages.create.mutationOptions());
|
|
12352
|
+
|
|
12353
|
+
async function handleSubmit() {
|
|
12354
|
+
const body = draft.trim();
|
|
12355
|
+
if (!body || createMessage.isPending) return;
|
|
12356
|
+
|
|
12357
|
+
try {
|
|
12358
|
+
await createMessage.mutateAsync({ body });
|
|
12359
|
+
setDraft('');
|
|
12360
|
+
} catch {}
|
|
12361
|
+
}
|
|
12362
|
+
|
|
12363
|
+
return (
|
|
12364
|
+
<SafeAreaView style={styles.safeArea}>
|
|
12365
|
+
<ScrollView
|
|
12366
|
+
contentContainerStyle={styles.content}
|
|
12367
|
+
keyboardShouldPersistTaps="handled"
|
|
12368
|
+
>
|
|
12369
|
+
<View style={styles.header}>
|
|
12370
|
+
<Text style={styles.kicker}>kitcn</Text>
|
|
12371
|
+
<Text style={styles.title}>Messages</Text>
|
|
12372
|
+
<Text style={styles.copy}>
|
|
12373
|
+
This screen is a tiny live query and mutation over kitcn. Start the
|
|
12374
|
+
backend, add a message, and watch the list update.
|
|
12375
|
+
</Text>
|
|
12376
|
+
</View>
|
|
12377
|
+
|
|
12378
|
+
<View style={styles.form}>
|
|
12379
|
+
<TextInput
|
|
12380
|
+
autoCapitalize="sentences"
|
|
12381
|
+
maxLength={120}
|
|
12382
|
+
onChangeText={setDraft}
|
|
12383
|
+
placeholder="Write a message"
|
|
12384
|
+
style={styles.input}
|
|
12385
|
+
value={draft}
|
|
12386
|
+
/>
|
|
12387
|
+
<Pressable
|
|
12388
|
+
disabled={createMessage.isPending || draft.trim().length === 0}
|
|
12389
|
+
onPress={() => {
|
|
12390
|
+
void handleSubmit();
|
|
12391
|
+
}}
|
|
12392
|
+
style={[
|
|
12393
|
+
styles.button,
|
|
12394
|
+
(createMessage.isPending || draft.trim().length === 0) &&
|
|
12395
|
+
styles.buttonDisabled,
|
|
12396
|
+
]}
|
|
12397
|
+
>
|
|
12398
|
+
<Text style={styles.buttonText}>
|
|
12399
|
+
{createMessage.isPending ? 'Saving...' : 'Add message'}
|
|
12400
|
+
</Text>
|
|
12401
|
+
</Pressable>
|
|
12402
|
+
</View>
|
|
12403
|
+
|
|
12404
|
+
{messagesQuery.isPending ? (
|
|
12405
|
+
<Text style={styles.muted}>Loading messages...</Text>
|
|
12406
|
+
) : messagesQuery.isError ? (
|
|
12407
|
+
<View style={styles.notice}>
|
|
12408
|
+
<Text style={styles.noticeText}>
|
|
12409
|
+
Backend not ready. Start kitcn dev and reload the app.
|
|
12410
|
+
</Text>
|
|
12411
|
+
</View>
|
|
12412
|
+
) : messagesQuery.data.length === 0 ? (
|
|
12413
|
+
<View style={styles.notice}>
|
|
12414
|
+
<Text style={styles.noticeText}>No messages yet. Add the first one.</Text>
|
|
12415
|
+
</View>
|
|
12416
|
+
) : (
|
|
12417
|
+
<View style={styles.list}>
|
|
12418
|
+
{messagesQuery.data.map((message) => (
|
|
12419
|
+
<View key={message.id} style={styles.card}>
|
|
12420
|
+
<View style={styles.cardHeader}>
|
|
12421
|
+
<Text style={styles.cardBody}>{message.body}</Text>
|
|
12422
|
+
<Text style={styles.cardTime}>
|
|
12423
|
+
{message.createdAt.toLocaleTimeString([], {
|
|
12424
|
+
hour: '2-digit',
|
|
12425
|
+
minute: '2-digit',
|
|
12426
|
+
})}
|
|
12427
|
+
</Text>
|
|
12428
|
+
</View>
|
|
12429
|
+
</View>
|
|
12430
|
+
))}
|
|
12431
|
+
</View>
|
|
12432
|
+
)}
|
|
12433
|
+
</ScrollView>
|
|
12434
|
+
</SafeAreaView>
|
|
12435
|
+
);
|
|
12436
|
+
}
|
|
12437
|
+
|
|
12438
|
+
const styles = StyleSheet.create({
|
|
12439
|
+
safeArea: {
|
|
12440
|
+
flex: 1,
|
|
12441
|
+
backgroundColor: '#ffffff',
|
|
12442
|
+
},
|
|
12443
|
+
content: {
|
|
12444
|
+
gap: 20,
|
|
12445
|
+
paddingHorizontal: 20,
|
|
12446
|
+
paddingVertical: 24,
|
|
12447
|
+
},
|
|
12448
|
+
header: {
|
|
12449
|
+
gap: 8,
|
|
12450
|
+
},
|
|
12451
|
+
kicker: {
|
|
12452
|
+
color: '#6b7280',
|
|
12453
|
+
fontSize: 12,
|
|
12454
|
+
fontWeight: '600',
|
|
12455
|
+
letterSpacing: 2,
|
|
12456
|
+
textTransform: 'uppercase',
|
|
12457
|
+
},
|
|
12458
|
+
title: {
|
|
12459
|
+
color: '#111827',
|
|
12460
|
+
fontSize: 28,
|
|
12461
|
+
fontWeight: '700',
|
|
12462
|
+
},
|
|
12463
|
+
copy: {
|
|
12464
|
+
color: '#4b5563',
|
|
12465
|
+
fontSize: 15,
|
|
12466
|
+
lineHeight: 22,
|
|
12467
|
+
},
|
|
12468
|
+
form: {
|
|
12469
|
+
gap: 12,
|
|
12470
|
+
},
|
|
12471
|
+
input: {
|
|
12472
|
+
borderColor: '#d1d5db',
|
|
12473
|
+
borderRadius: 12,
|
|
12474
|
+
borderWidth: 1,
|
|
12475
|
+
fontSize: 16,
|
|
12476
|
+
minHeight: 48,
|
|
12477
|
+
paddingHorizontal: 14,
|
|
12478
|
+
paddingVertical: 12,
|
|
12479
|
+
},
|
|
12480
|
+
button: {
|
|
12481
|
+
alignItems: 'center',
|
|
12482
|
+
backgroundColor: '#111827',
|
|
12483
|
+
borderRadius: 12,
|
|
12484
|
+
minHeight: 48,
|
|
12485
|
+
justifyContent: 'center',
|
|
12486
|
+
paddingHorizontal: 16,
|
|
12487
|
+
},
|
|
12488
|
+
buttonDisabled: {
|
|
12489
|
+
opacity: 0.5,
|
|
12490
|
+
},
|
|
12491
|
+
buttonText: {
|
|
12492
|
+
color: '#ffffff',
|
|
12493
|
+
fontSize: 15,
|
|
12494
|
+
fontWeight: '600',
|
|
12495
|
+
},
|
|
12496
|
+
muted: {
|
|
12497
|
+
color: '#6b7280',
|
|
12498
|
+
fontSize: 14,
|
|
12499
|
+
},
|
|
12500
|
+
notice: {
|
|
12501
|
+
borderColor: '#d1d5db',
|
|
12502
|
+
borderRadius: 12,
|
|
12503
|
+
borderStyle: 'dashed',
|
|
12504
|
+
borderWidth: 1,
|
|
12505
|
+
paddingHorizontal: 16,
|
|
12506
|
+
paddingVertical: 18,
|
|
12507
|
+
},
|
|
12508
|
+
noticeText: {
|
|
12509
|
+
color: '#4b5563',
|
|
12510
|
+
fontSize: 14,
|
|
12511
|
+
lineHeight: 20,
|
|
12512
|
+
},
|
|
12513
|
+
list: {
|
|
12514
|
+
gap: 12,
|
|
12515
|
+
},
|
|
12516
|
+
card: {
|
|
12517
|
+
backgroundColor: '#f9fafb',
|
|
12518
|
+
borderColor: '#e5e7eb',
|
|
12519
|
+
borderRadius: 12,
|
|
12520
|
+
borderWidth: 1,
|
|
12521
|
+
paddingHorizontal: 16,
|
|
12522
|
+
paddingVertical: 14,
|
|
12523
|
+
},
|
|
12524
|
+
cardHeader: {
|
|
12525
|
+
flexDirection: 'row',
|
|
12526
|
+
gap: 12,
|
|
12527
|
+
justifyContent: 'space-between',
|
|
12528
|
+
},
|
|
12529
|
+
cardBody: {
|
|
12530
|
+
color: '#111827',
|
|
12531
|
+
flex: 1,
|
|
12532
|
+
fontSize: 15,
|
|
12533
|
+
lineHeight: 22,
|
|
12534
|
+
},
|
|
12535
|
+
cardTime: {
|
|
12536
|
+
color: '#6b7280',
|
|
12537
|
+
fontSize: 12,
|
|
12538
|
+
fontVariant: ['tabular-nums'],
|
|
12539
|
+
},
|
|
12540
|
+
});
|
|
12541
|
+
`;
|
|
12542
|
+
|
|
12543
|
+
//#endregion
|
|
12544
|
+
//#region src/cli/registry/init/expo/init-expo-package-json.template.ts
|
|
12545
|
+
const INIT_EXPO_CODEGEN_SCRIPT = "kitcn codegen";
|
|
12546
|
+
const INIT_EXPO_PRIMARY_CODEGEN_SCRIPT_NAME = "codegen";
|
|
12547
|
+
const INIT_EXPO_FALLBACK_CODEGEN_SCRIPT_NAME = "convex:codegen";
|
|
12548
|
+
const INIT_EXPO_CONVEX_DEV_SCRIPT_NAME = "convex:dev";
|
|
12549
|
+
const INIT_EXPO_CONVEX_DEV_SCRIPT = "kitcn dev";
|
|
12550
|
+
const INIT_EXPO_CONVEX_TYPECHECK_SCRIPT_NAME = "typecheck:convex";
|
|
12551
|
+
const getInitExpoConvexTypecheckScript = (functionsDirRelative = "convex/functions") => `tsc --noEmit --project ${functionsDirRelative}/tsconfig.json`;
|
|
12552
|
+
const INIT_EXPO_PACKAGE_JSON_DEPENDENCIES = {
|
|
12553
|
+
"@opentelemetry/api": SUPPORTED_DEPENDENCY_VERSIONS.opentelemetryApi.exact,
|
|
12554
|
+
superjson: "2.2.6"
|
|
12555
|
+
};
|
|
12556
|
+
const INIT_EXPO_PACKAGE_JSON_DEV_DEPENDENCIES = { "@types/bun": "latest" };
|
|
12557
|
+
const getInitExpoPackageJsonDevDependencies = (options) => ({
|
|
12558
|
+
...INIT_EXPO_PACKAGE_JSON_DEV_DEPENDENCIES,
|
|
12559
|
+
...options.backend === "concave" ? { "@concavejs/cli": "latest" } : {}
|
|
12560
|
+
});
|
|
12561
|
+
function renderInitExpoPackageJsonTemplate(source, options = {}) {
|
|
12562
|
+
const existing = source ? JSON.parse(source) : {};
|
|
12563
|
+
const nextScripts = {
|
|
12564
|
+
...existing.scripts,
|
|
12565
|
+
typecheck: "tsc --noEmit && bun run typecheck:convex"
|
|
12566
|
+
};
|
|
12567
|
+
if (!nextScripts[INIT_EXPO_PRIMARY_CODEGEN_SCRIPT_NAME]) nextScripts[INIT_EXPO_PRIMARY_CODEGEN_SCRIPT_NAME] = INIT_EXPO_CODEGEN_SCRIPT;
|
|
12568
|
+
else if (!nextScripts[INIT_EXPO_FALLBACK_CODEGEN_SCRIPT_NAME]) nextScripts[INIT_EXPO_FALLBACK_CODEGEN_SCRIPT_NAME] = INIT_EXPO_CODEGEN_SCRIPT;
|
|
12569
|
+
if (!nextScripts[INIT_EXPO_CONVEX_DEV_SCRIPT_NAME]) nextScripts[INIT_EXPO_CONVEX_DEV_SCRIPT_NAME] = INIT_EXPO_CONVEX_DEV_SCRIPT;
|
|
12570
|
+
if (!nextScripts[INIT_EXPO_CONVEX_TYPECHECK_SCRIPT_NAME]) nextScripts[INIT_EXPO_CONVEX_TYPECHECK_SCRIPT_NAME] = getInitExpoConvexTypecheckScript(options.functionsDirRelative);
|
|
12571
|
+
return `${JSON.stringify({
|
|
12572
|
+
...existing,
|
|
12573
|
+
scripts: nextScripts,
|
|
12574
|
+
dependencies: {
|
|
12575
|
+
...existing.dependencies,
|
|
12576
|
+
...INIT_EXPO_PACKAGE_JSON_DEPENDENCIES
|
|
12577
|
+
},
|
|
12578
|
+
devDependencies: {
|
|
12579
|
+
...existing.devDependencies,
|
|
12580
|
+
...getInitExpoPackageJsonDevDependencies(options)
|
|
12581
|
+
}
|
|
12582
|
+
}, null, 2)}\n`;
|
|
12583
|
+
}
|
|
12584
|
+
|
|
12585
|
+
//#endregion
|
|
12586
|
+
//#region src/cli/registry/init/expo/init-expo-providers.template.ts
|
|
12587
|
+
const INIT_EXPO_PROVIDERS_TEMPLATE = `import type { ReactNode } from 'react';
|
|
12588
|
+
|
|
12589
|
+
import { AppConvexProvider } from '@/lib/convex/convex-provider';
|
|
12590
|
+
|
|
12591
|
+
export function Providers({ children }: { children: ReactNode }) {
|
|
12592
|
+
return <AppConvexProvider>{children}</AppConvexProvider>;
|
|
12593
|
+
}
|
|
12594
|
+
`;
|
|
12595
|
+
|
|
11683
12596
|
//#endregion
|
|
11684
12597
|
//#region src/cli/registry/init/init-convex-config.template.ts
|
|
11685
12598
|
const INIT_CONVEX_CONFIG_TEMPLATE = `{
|
|
@@ -12563,6 +13476,8 @@ const LEADING_SLASHES_RE = /^\/+/;
|
|
|
12563
13476
|
const AGGREGATE_STATE_RELATIVE_PATH = join(".convex", "kitcn", "aggregate-backfill-state.json");
|
|
12564
13477
|
const AGGREGATE_STATE_VERSION = 1;
|
|
12565
13478
|
const INIT_SHADCN_PACKAGE_SPEC = "shadcn@4.3.0";
|
|
13479
|
+
const INIT_EXPO_PACKAGE_SPEC = "create-expo-app@latest";
|
|
13480
|
+
const INIT_EXPO_TEMPLATE_SPEC = "default@sdk-55";
|
|
12566
13481
|
const INIT_LOCAL_BOOTSTRAP_TIMEOUT_MS = 3e4;
|
|
12567
13482
|
const LOCAL_BACKEND_NOT_RUNNING_RE = /Local backend isn't running/i;
|
|
12568
13483
|
const INIT_GENERATED_SERVER_STUB_TEMPLATE = `// @ts-nocheck
|
|
@@ -12597,6 +13512,7 @@ const SUPPORTED_PLUGINS = new Set(getSupportedPluginKeys());
|
|
|
12597
13512
|
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
12598
13513
|
const SUPPORTED_INIT_TEMPLATES = [
|
|
12599
13514
|
"next",
|
|
13515
|
+
"expo",
|
|
12600
13516
|
"start",
|
|
12601
13517
|
"vite"
|
|
12602
13518
|
];
|
|
@@ -12992,6 +13908,38 @@ async function createProjectWithShadcn(params) {
|
|
|
12992
13908
|
});
|
|
12993
13909
|
}
|
|
12994
13910
|
}
|
|
13911
|
+
async function createProjectWithExpo(params) {
|
|
13912
|
+
const stagingRoot = fs.existsSync(params.projectDir) && fs.readdirSync(params.projectDir).length === 0 ? fs.mkdtempSync(join(dirname(params.projectDir), ".kitcn-expo-")) : null;
|
|
13913
|
+
const expoCwd = stagingRoot ?? dirname(params.projectDir);
|
|
13914
|
+
fs.mkdirSync(expoCwd, { recursive: true });
|
|
13915
|
+
const projectName = basename(params.projectDir);
|
|
13916
|
+
const command = detectPackageManager(expoCwd) === "bun" ? "bunx" : "npx";
|
|
13917
|
+
const args = [
|
|
13918
|
+
INIT_EXPO_PACKAGE_SPEC,
|
|
13919
|
+
projectName,
|
|
13920
|
+
"--template",
|
|
13921
|
+
INIT_EXPO_TEMPLATE_SPEC,
|
|
13922
|
+
"--no-install",
|
|
13923
|
+
...params.yes ? ["--yes"] : []
|
|
13924
|
+
];
|
|
13925
|
+
try {
|
|
13926
|
+
if (((await params.execaFn(command, args, {
|
|
13927
|
+
cwd: expoCwd,
|
|
13928
|
+
env: createCommandEnv(),
|
|
13929
|
+
reject: false,
|
|
13930
|
+
stdio: "inherit"
|
|
13931
|
+
})).exitCode ?? 0) !== 0) throw new Error(`Expo init failed: ${command} ${args.join(" ")}`);
|
|
13932
|
+
if (stagingRoot) moveStagedProjectIntoExistingDir({
|
|
13933
|
+
stagedProjectDir: join(stagingRoot, projectName),
|
|
13934
|
+
targetDir: params.projectDir
|
|
13935
|
+
});
|
|
13936
|
+
} finally {
|
|
13937
|
+
if (stagingRoot) fs.rmSync(stagingRoot, {
|
|
13938
|
+
recursive: true,
|
|
13939
|
+
force: true
|
|
13940
|
+
});
|
|
13941
|
+
}
|
|
13942
|
+
}
|
|
12995
13943
|
function buildMissingShadcnScaffoldMessage(projectDir) {
|
|
12996
13944
|
return [
|
|
12997
13945
|
"Shadcn exited without creating a supported local scaffold.",
|
|
@@ -12999,6 +13947,9 @@ function buildMissingShadcnScaffoldMessage(projectDir) {
|
|
|
12999
13947
|
`Run the generated shadcn command from ui.shadcn.com in ${normalizePath(relative(process.cwd(), projectDir) || ".")} then re-run \`kitcn init --yes\` to adopt it.`
|
|
13000
13948
|
].join(" ");
|
|
13001
13949
|
}
|
|
13950
|
+
function buildMissingExpoScaffoldMessage(projectDir) {
|
|
13951
|
+
return ["create-expo-app exited without creating a supported local scaffold.", `Re-run \`kitcn init -t expo --yes\` in ${normalizePath(relative(process.cwd(), projectDir) || ".")}, or try \`${INIT_EXPO_PACKAGE_SPEC} ${basename(projectDir)} --template ${INIT_EXPO_TEMPLATE_SPEC}\` directly.`].join(" ");
|
|
13952
|
+
}
|
|
13002
13953
|
function moveStagedProjectIntoExistingDir(params) {
|
|
13003
13954
|
if (!fs.existsSync(params.stagedProjectDir)) throw new Error(buildMissingShadcnScaffoldMessage(params.targetDir));
|
|
13004
13955
|
if (!fs.existsSync(params.targetDir) || fs.readdirSync(params.targetDir).length > 0) throw new Error(`Cannot move staged project into non-empty target ${params.targetDir}.`);
|
|
@@ -13150,6 +14101,137 @@ function buildInitNextOwnedScaffoldFiles(context, functionsDirRelative, backend,
|
|
|
13150
14101
|
});
|
|
13151
14102
|
return files;
|
|
13152
14103
|
}
|
|
14104
|
+
function buildInitExpoOwnedScaffoldFiles(context, functionsDirRelative, backend, includeDemoFiles) {
|
|
14105
|
+
const files = [
|
|
14106
|
+
{
|
|
14107
|
+
kind: "config",
|
|
14108
|
+
relativePath: "package.json",
|
|
14109
|
+
requiresExplicitOverwrite: false,
|
|
14110
|
+
content: ({ existingContent }) => renderInitExpoPackageJsonTemplate(existingContent, {
|
|
14111
|
+
backend,
|
|
14112
|
+
functionsDirRelative
|
|
14113
|
+
}),
|
|
14114
|
+
createReason: "Create baseline package.json scripts for the Expo scaffold.",
|
|
14115
|
+
updateReason: "Update package.json scripts for the Expo scaffold.",
|
|
14116
|
+
skipReason: "package.json scripts already match the Expo scaffold."
|
|
14117
|
+
},
|
|
14118
|
+
{
|
|
14119
|
+
kind: "env",
|
|
14120
|
+
relativePath: ".env.local",
|
|
14121
|
+
requiresExplicitOverwrite: false,
|
|
14122
|
+
content: ({ existingContent }) => renderInitExpoEnvTemplate(existingContent),
|
|
14123
|
+
createReason: "Create baseline .env.local for the Expo scaffold.",
|
|
14124
|
+
updateReason: "Update baseline .env.local for the Expo scaffold.",
|
|
14125
|
+
skipReason: ".env.local already matches the Expo scaffold."
|
|
14126
|
+
},
|
|
14127
|
+
{
|
|
14128
|
+
kind: "config",
|
|
14129
|
+
relativePath: "expo-env.d.ts",
|
|
14130
|
+
requiresExplicitOverwrite: true,
|
|
14131
|
+
content: INIT_EXPO_ENV_TYPES_TEMPLATE,
|
|
14132
|
+
createReason: "Create expo-env.d.ts for the Expo scaffold.",
|
|
14133
|
+
updateReason: "Update expo-env.d.ts for the Expo scaffold.",
|
|
14134
|
+
skipReason: "expo-env.d.ts already matches the Expo scaffold."
|
|
14135
|
+
},
|
|
14136
|
+
{
|
|
14137
|
+
kind: "config",
|
|
14138
|
+
relativePath: ".gitignore",
|
|
14139
|
+
requiresExplicitOverwrite: false,
|
|
14140
|
+
content: ({ existingContent }) => renderInitExpoGitignoreTemplate(existingContent ?? ""),
|
|
14141
|
+
createReason: "Create .gitignore for the Expo scaffold.",
|
|
14142
|
+
updateReason: "Update .gitignore so the Expo scaffold keeps expo-env.d.ts tracked.",
|
|
14143
|
+
skipReason: ".gitignore already matches the Expo scaffold."
|
|
14144
|
+
},
|
|
14145
|
+
{
|
|
14146
|
+
kind: "scaffold",
|
|
14147
|
+
relativePath: `${context.componentsDir}/providers.tsx`,
|
|
14148
|
+
requiresExplicitOverwrite: true,
|
|
14149
|
+
content: INIT_EXPO_PROVIDERS_TEMPLATE,
|
|
14150
|
+
createReason: `Create baseline ${context.componentsDir}/providers.tsx for the Expo scaffold.`,
|
|
14151
|
+
updateReason: `Update ${context.componentsDir}/providers.tsx for the Expo scaffold.`,
|
|
14152
|
+
skipReason: `${context.componentsDir}/providers.tsx already matches the Expo scaffold.`
|
|
14153
|
+
},
|
|
14154
|
+
{
|
|
14155
|
+
kind: "scaffold",
|
|
14156
|
+
relativePath: `${context.convexClientDir}/query-client.ts`,
|
|
14157
|
+
requiresExplicitOverwrite: true,
|
|
14158
|
+
content: INIT_NEXT_QUERY_CLIENT_TEMPLATE,
|
|
14159
|
+
createReason: `Create baseline ${context.convexClientDir}/query-client.ts for the Expo scaffold.`,
|
|
14160
|
+
updateReason: `Update ${context.convexClientDir}/query-client.ts for the Expo scaffold.`,
|
|
14161
|
+
skipReason: `${context.convexClientDir}/query-client.ts already matches the Expo scaffold.`
|
|
14162
|
+
},
|
|
14163
|
+
{
|
|
14164
|
+
kind: "scaffold",
|
|
14165
|
+
relativePath: `${context.convexClientDir}/crpc.tsx`,
|
|
14166
|
+
requiresExplicitOverwrite: true,
|
|
14167
|
+
content: INIT_EXPO_CRPC_TEMPLATE,
|
|
14168
|
+
createReason: `Create baseline ${context.convexClientDir}/crpc.tsx for the Expo scaffold.`,
|
|
14169
|
+
updateReason: `Update ${context.convexClientDir}/crpc.tsx for the Expo scaffold.`,
|
|
14170
|
+
skipReason: `${context.convexClientDir}/crpc.tsx already matches the Expo scaffold.`
|
|
14171
|
+
},
|
|
14172
|
+
{
|
|
14173
|
+
kind: "scaffold",
|
|
14174
|
+
relativePath: `${context.convexClientDir}/convex-provider.tsx`,
|
|
14175
|
+
requiresExplicitOverwrite: true,
|
|
14176
|
+
content: INIT_EXPO_CONVEX_PROVIDER_TEMPLATE,
|
|
14177
|
+
createReason: `Create baseline ${context.convexClientDir}/convex-provider.tsx for the Expo scaffold.`,
|
|
14178
|
+
updateReason: `Update ${context.convexClientDir}/convex-provider.tsx for the Expo scaffold.`,
|
|
14179
|
+
skipReason: `${context.convexClientDir}/convex-provider.tsx already matches the Expo scaffold.`
|
|
14180
|
+
},
|
|
14181
|
+
{
|
|
14182
|
+
kind: "config",
|
|
14183
|
+
relativePath: join(functionsDirRelative, "tsconfig.json"),
|
|
14184
|
+
managedBaselineContent: getManagedConvexTsconfigBaselines(functionsDirRelative),
|
|
14185
|
+
requiresExplicitOverwrite: true,
|
|
14186
|
+
content: ({ existingContent }) => typeof existingContent === "string" ? patchInitConvexTsconfigContent(existingContent, functionsDirRelative) : renderInitConvexTsconfigTemplate(functionsDirRelative),
|
|
14187
|
+
createReason: `Create ${join(functionsDirRelative, "tsconfig.json")} for kitcn functions.`,
|
|
14188
|
+
updateReason: `Patch ${join(functionsDirRelative, "tsconfig.json")} for kitcn functions.`,
|
|
14189
|
+
skipReason: `${join(functionsDirRelative, "tsconfig.json")} already matches the kitcn functions project.`
|
|
14190
|
+
}
|
|
14191
|
+
];
|
|
14192
|
+
if (includeDemoFiles) files.push({
|
|
14193
|
+
kind: "scaffold",
|
|
14194
|
+
relativePath: `${context.appDir}/_layout.tsx`,
|
|
14195
|
+
requiresExplicitOverwrite: true,
|
|
14196
|
+
content: INIT_EXPO_LAYOUT_TEMPLATE,
|
|
14197
|
+
createReason: `Create ${context.appDir}/_layout.tsx for the Expo scaffold.`,
|
|
14198
|
+
updateReason: `Update ${context.appDir}/_layout.tsx for the Expo scaffold.`,
|
|
14199
|
+
skipReason: `${context.appDir}/_layout.tsx already matches the Expo scaffold.`
|
|
14200
|
+
}, {
|
|
14201
|
+
kind: "scaffold",
|
|
14202
|
+
relativePath: `${context.appDir}/index.tsx`,
|
|
14203
|
+
requiresExplicitOverwrite: true,
|
|
14204
|
+
content: INIT_EXPO_MESSAGES_SCREEN_TEMPLATE,
|
|
14205
|
+
createReason: `Create ${context.appDir}/index.tsx as the minimal kitcn demo route.`,
|
|
14206
|
+
updateReason: `Update ${context.appDir}/index.tsx for the kitcn demo route.`,
|
|
14207
|
+
skipReason: `${context.appDir}/index.tsx already matches the kitcn demo route.`
|
|
14208
|
+
}, {
|
|
14209
|
+
kind: "scaffold",
|
|
14210
|
+
relativePath: `${context.appDir}/explore.tsx`,
|
|
14211
|
+
requiresExplicitOverwrite: true,
|
|
14212
|
+
content: INIT_EXPO_EXPLORE_TEMPLATE,
|
|
14213
|
+
createReason: `Create ${context.appDir}/explore.tsx as a redirect back to the kitcn demo route.`,
|
|
14214
|
+
updateReason: `Update ${context.appDir}/explore.tsx as a redirect back to the kitcn demo route.`,
|
|
14215
|
+
skipReason: `${context.appDir}/explore.tsx already redirects to the kitcn demo route.`
|
|
14216
|
+
}, {
|
|
14217
|
+
kind: "schema",
|
|
14218
|
+
relativePath: `${functionsDirRelative}/schema.ts`,
|
|
14219
|
+
requiresExplicitOverwrite: true,
|
|
14220
|
+
content: INIT_NEXT_SCHEMA_TEMPLATE,
|
|
14221
|
+
createReason: `Create ${functionsDirRelative}/schema.ts with the minimal kitcn demo schema.`,
|
|
14222
|
+
updateReason: `Update ${functionsDirRelative}/schema.ts with the minimal kitcn demo schema.`,
|
|
14223
|
+
skipReason: `${functionsDirRelative}/schema.ts already matches the kitcn demo schema.`
|
|
14224
|
+
}, {
|
|
14225
|
+
kind: "scaffold",
|
|
14226
|
+
relativePath: `${functionsDirRelative}/messages.ts`,
|
|
14227
|
+
requiresExplicitOverwrite: true,
|
|
14228
|
+
content: renderInitNextMessagesTemplate(functionsDirRelative),
|
|
14229
|
+
createReason: `Create ${functionsDirRelative}/messages.ts for the kitcn demo route.`,
|
|
14230
|
+
updateReason: `Update ${functionsDirRelative}/messages.ts for the kitcn demo route.`,
|
|
14231
|
+
skipReason: `${functionsDirRelative}/messages.ts already matches the kitcn demo route.`
|
|
14232
|
+
});
|
|
14233
|
+
return files;
|
|
14234
|
+
}
|
|
13153
14235
|
function buildInitReactOwnedScaffoldFiles(context, functionsDirRelative, backend) {
|
|
13154
14236
|
return [
|
|
13155
14237
|
{
|
|
@@ -13557,6 +14639,19 @@ function buildInitNextLayoutPlanFile(context) {
|
|
|
13557
14639
|
skipReason: `${context.appDir}/layout.tsx already mounts Providers.`
|
|
13558
14640
|
});
|
|
13559
14641
|
}
|
|
14642
|
+
function buildInitExpoTsconfigPlanFile(context) {
|
|
14643
|
+
const filePath = resolve(process.cwd(), "tsconfig.json");
|
|
14644
|
+
if (!fs.existsSync(filePath)) throw new Error("Could not patch tsconfig.json: the Expo scaffold did not create a tsconfig file.");
|
|
14645
|
+
return createPlanFile({
|
|
14646
|
+
kind: "config",
|
|
14647
|
+
filePath,
|
|
14648
|
+
requiresExplicitOverwrite: false,
|
|
14649
|
+
content: patchInitTsconfigContent(fs.readFileSync(filePath, "utf8"), context),
|
|
14650
|
+
updateReason: "Patch tsconfig.json to keep the Expo alias and add @convex/*.",
|
|
14651
|
+
createReason: "Patch tsconfig.json to keep the Expo alias and add @convex/*.",
|
|
14652
|
+
skipReason: "tsconfig.json already includes the kitcn alias."
|
|
14653
|
+
});
|
|
14654
|
+
}
|
|
13560
14655
|
function buildInitNextTsconfigPlanFile(context) {
|
|
13561
14656
|
const filePath = resolve(process.cwd(), "tsconfig.json");
|
|
13562
14657
|
if (!fs.existsSync(filePath)) throw new Error("Could not patch tsconfig.json: shadcn did not create a tsconfig file.");
|
|
@@ -13669,7 +14764,7 @@ function buildTemplateInitializationPlanFiles(params) {
|
|
|
13669
14764
|
allowUnsupported: true
|
|
13670
14765
|
});
|
|
13671
14766
|
if (!projectContext) return [];
|
|
13672
|
-
const plannedOwnedFiles = (projectContext.mode === "next-app" ? buildInitNextOwnedScaffoldFiles(projectContext, params.functionsDirRelative, params.backend, params.includeDemoFiles) : projectContext.framework === "tanstack-start" ? buildInitStartOwnedScaffoldFiles(projectContext, params.functionsDirRelative, params.backend, params.includeDemoFiles) : buildInitReactOwnedScaffoldFiles(projectContext, params.functionsDirRelative, params.backend)).map((file) => {
|
|
14767
|
+
const plannedOwnedFiles = (projectContext.mode === "next-app" ? buildInitNextOwnedScaffoldFiles(projectContext, params.functionsDirRelative, params.backend, params.includeDemoFiles) : projectContext.framework === "expo" ? buildInitExpoOwnedScaffoldFiles(projectContext, params.functionsDirRelative, params.backend, params.includeDemoFiles) : projectContext.framework === "tanstack-start" ? buildInitStartOwnedScaffoldFiles(projectContext, params.functionsDirRelative, params.backend, params.includeDemoFiles) : buildInitReactOwnedScaffoldFiles(projectContext, params.functionsDirRelative, params.backend)).map((file) => {
|
|
13673
14768
|
const filePath = resolve(process.cwd(), file.relativePath);
|
|
13674
14769
|
const existingContent = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8") : void 0;
|
|
13675
14770
|
const nextContent = typeof file.content === "function" ? file.content({ existingContent }) : file.content;
|
|
@@ -13699,6 +14794,7 @@ function buildTemplateInitializationPlanFiles(params) {
|
|
|
13699
14794
|
buildInitNextLayoutPlanFile(projectContext)
|
|
13700
14795
|
];
|
|
13701
14796
|
}
|
|
14797
|
+
if (projectContext.framework === "expo") return [...plannedOwnedFiles, buildInitExpoTsconfigPlanFile(projectContext)];
|
|
13702
14798
|
if (projectContext.framework === "tanstack-start") return [
|
|
13703
14799
|
...plannedOwnedFiles,
|
|
13704
14800
|
buildInitReactRootTsconfigPlanFile(projectContext),
|
|
@@ -13927,7 +15023,12 @@ async function withWorkingDirectory(cwd, fn) {
|
|
|
13927
15023
|
}
|
|
13928
15024
|
}
|
|
13929
15025
|
async function runScaffoldCommandFlow(params) {
|
|
13930
|
-
if (params.template) await
|
|
15026
|
+
if (params.template) if (params.template === "expo") await createProjectWithExpo({
|
|
15027
|
+
projectDir: params.projectDir,
|
|
15028
|
+
yes: params.yes,
|
|
15029
|
+
execaFn: params.execaFn
|
|
15030
|
+
});
|
|
15031
|
+
else await createProjectWithShadcn({
|
|
13931
15032
|
projectDir: params.projectDir,
|
|
13932
15033
|
template: params.template,
|
|
13933
15034
|
yes: params.yes,
|
|
@@ -13939,7 +15040,7 @@ async function runScaffoldCommandFlow(params) {
|
|
|
13939
15040
|
cwd: scaffoldProjectDir,
|
|
13940
15041
|
allowMissing: true,
|
|
13941
15042
|
allowUnsupported: true
|
|
13942
|
-
})) throw new Error(buildMissingShadcnScaffoldMessage(scaffoldProjectDir));
|
|
15043
|
+
})) throw new Error(params.template === "expo" ? buildMissingExpoScaffoldMessage(scaffoldProjectDir) : buildMissingShadcnScaffoldMessage(scaffoldProjectDir));
|
|
13943
15044
|
return withWorkingDirectory(scaffoldProjectDir, async () => {
|
|
13944
15045
|
const config = params.loadCliConfigFn(params.configPath);
|
|
13945
15046
|
const backend = resolveConfiguredBackend({
|
|
@@ -14005,7 +15106,7 @@ async function runInitCommandFlow(params) {
|
|
|
14005
15106
|
allowUnsupported: true
|
|
14006
15107
|
});
|
|
14007
15108
|
if (template !== void 0 || params.initArgs.defaults || params.initArgs.name !== void 0) {
|
|
14008
|
-
if (!template) throw new Error("Fresh app scaffolding requires `kitcn init -t <next|start|vite>`.");
|
|
15109
|
+
if (!template) throw new Error("Fresh app scaffolding requires `kitcn init -t <next|expo|start|vite>`.");
|
|
14009
15110
|
if (existingProjectContext) throw new Error(`Existing supported app scaffold detected. Run \`kitcn init --yes\` in ${normalizePath(relative(process.cwd(), projectDir) || ".")} to adopt the current project.`);
|
|
14010
15111
|
return runScaffoldCommandFlow({
|
|
14011
15112
|
allowCodegenBootstrapFallback: !params.initArgs.json,
|
|
@@ -14027,7 +15128,8 @@ async function runInitCommandFlow(params) {
|
|
|
14027
15128
|
realConcavePath: params.realConcavePath
|
|
14028
15129
|
});
|
|
14029
15130
|
}
|
|
14030
|
-
if (
|
|
15131
|
+
if (existingProjectContext?.framework === "expo") throw new Error("Expo adoption is not supported yet. Start with `kitcn init -t expo --yes`.");
|
|
15132
|
+
if (!existingProjectContext) throw new Error("Could not detect a supported app scaffold. Use `kitcn init -t <next|expo|start|vite>` for a fresh app.");
|
|
14031
15133
|
return runScaffoldCommandFlow({
|
|
14032
15134
|
allowCodegenBootstrapFallback: !params.initArgs.json,
|
|
14033
15135
|
projectDir,
|