@ramonclaudio/create-vexpo 0.1.0 → 0.1.1
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 +10 -10
- package/dist/index.js +8 -7
- package/dist/templates/default/.eas/workflows/asc-events.yml +9 -6
- package/dist/templates/default/.eas/workflows/deploy-production.yml +28 -15
- package/dist/templates/default/.eas/workflows/e2e-tests.yml +3 -2
- package/dist/templates/default/.eas/workflows/pr-preview.yml +12 -21
- package/dist/templates/default/.eas/workflows/release.yml +3 -7
- package/dist/templates/default/.eas/workflows/rollback.yml +54 -28
- package/dist/templates/default/.eas/workflows/rollout.yml +27 -33
- package/dist/templates/default/.eas/workflows/rotate-apple-jwt.yml +1 -5
- package/dist/templates/default/.eas/workflows/testflight.yml +3 -7
- package/dist/templates/default/.github/workflows/check.yml +20 -12
- package/dist/templates/default/.maestro/launch.yaml +19 -10
- package/dist/templates/default/AGENTS.md +25 -8
- package/dist/templates/default/DESIGN.md +14 -10
- package/dist/templates/default/README.md +83 -78
- package/dist/templates/default/SETUP.md +159 -152
- package/dist/templates/default/__tests__/convex/_auth-harness.test.ts +112 -0
- package/dist/templates/default/__tests__/convex/_harness.ts +132 -0
- package/dist/templates/default/__tests__/convex/appAttest.test.ts +172 -0
- package/dist/templates/default/__tests__/convex/appAttestStore.test.ts +48 -0
- package/dist/templates/default/__tests__/convex/pushTokens-remove.test.ts +106 -0
- package/dist/templates/default/__tests__/convex/pushTokens-upsert.test.ts +146 -0
- package/dist/templates/default/__tests__/convex/users-deleteAccount.test.ts +140 -0
- package/dist/templates/default/__tests__/convex/users-deleteAvatar.test.ts +104 -0
- package/dist/templates/default/__tests__/convex/users-getMe.test.ts +98 -0
- package/dist/templates/default/__tests__/convex/users-getUser.test.ts +120 -0
- package/dist/templates/default/__tests__/convex/users-hardDeleteExpired.test.ts +67 -0
- package/dist/templates/default/__tests__/convex/users-restoreAccount.test.ts +96 -0
- package/dist/templates/default/__tests__/convex/users-updateAvatar.test.ts +92 -0
- package/dist/templates/default/__tests__/convex/users-updateProfile.test.ts +126 -0
- package/dist/templates/default/__tests__/convex/webhook.test.ts +31 -0
- package/dist/templates/default/__tests__/lib/deep-link.test.ts +51 -6
- package/dist/templates/default/__tests__/lib/schemas.test.ts +205 -0
- package/dist/templates/default/__tests__/lib/text-style.test.ts +31 -0
- package/dist/templates/default/_env.example +7 -7
- package/dist/templates/default/_gitattributes +1 -1
- package/dist/templates/default/_gitignore +17 -2
- package/dist/templates/default/_npmrc +7 -0
- package/dist/templates/default/_oxlintrc.json +1 -1
- package/dist/templates/default/app-store/accessibility.config.json +20 -0
- package/dist/templates/default/app-store/privacy.config.json +27 -0
- package/dist/templates/default/app.config.ts +105 -33
- package/dist/templates/default/app.json +1 -9
- package/dist/templates/default/convex/_generated/api.d.ts +12 -0
- package/dist/templates/default/convex/admin.ts +0 -13
- package/dist/templates/default/convex/appAttest.ts +467 -0
- package/dist/templates/default/convex/appAttestStore.ts +141 -0
- package/dist/templates/default/convex/apple.ts +53 -0
- package/dist/templates/default/convex/auth.ts +6 -45
- package/dist/templates/default/convex/constants.ts +2 -7
- package/dist/templates/default/convex/crons.ts +12 -5
- package/dist/templates/default/convex/email.ts +4 -24
- package/dist/templates/default/convex/env.ts +0 -4
- package/dist/templates/default/convex/errors.ts +0 -7
- package/dist/templates/default/convex/functions.ts +0 -26
- package/dist/templates/default/convex/http.ts +3 -5
- package/dist/templates/default/convex/log.ts +2 -25
- package/dist/templates/default/convex/pushSender.ts +145 -0
- package/dist/templates/default/convex/pushTokens.ts +110 -13
- package/dist/templates/default/convex/rateLimit.ts +8 -39
- package/dist/templates/default/convex/schema.ts +48 -5
- package/dist/templates/default/convex/tsconfig.json +1 -0
- package/dist/templates/default/convex/users.ts +143 -61
- package/dist/templates/default/convex/validators.ts +1 -38
- package/dist/templates/default/convex/webhook.ts +1 -31
- package/dist/templates/default/convex.json +1 -2
- package/dist/templates/default/metro.config.js +9 -1
- package/dist/templates/default/package.json +67 -70
- package/dist/templates/default/plugins/README.md +5 -1
- package/dist/templates/default/scripts/README.md +9 -9
- package/dist/templates/default/scripts/_run.mjs +3 -20
- package/dist/templates/default/scripts/clean.ts +81 -69
- package/dist/templates/default/scripts/gen-update-cert.mjs +98 -0
- package/dist/templates/default/{app → src/app}/(app)/(tabs)/(home)/index.tsx +21 -6
- package/dist/templates/default/{app → src/app}/(app)/(tabs)/(home,search)/_layout.tsx +9 -8
- package/dist/templates/default/{app → src/app}/(app)/(tabs)/(search)/index.tsx +26 -24
- package/dist/templates/default/{app → src/app}/(app)/(tabs)/_layout.tsx +3 -4
- package/dist/templates/default/{app → src/app}/(app)/(tabs)/settings/_layout.tsx +10 -6
- package/dist/templates/default/{app → src/app}/(app)/(tabs)/settings/index.tsx +81 -51
- package/dist/templates/default/{app → src/app}/(app)/(tabs)/settings/preferences.tsx +72 -12
- package/dist/templates/default/src/app/(app)/_layout.tsx +147 -0
- package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/_layout.tsx +4 -5
- package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/forgot-password.tsx +15 -9
- package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/reset-password.tsx +88 -14
- package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/sign-in.tsx +65 -35
- package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/sign-up.tsx +131 -196
- package/dist/templates/default/src/app/(app)/debug.tsx +479 -0
- package/dist/templates/default/{app → src/app}/(app)/help.tsx +76 -64
- package/dist/templates/default/{app → src/app}/(app)/linked.tsx +21 -27
- package/dist/templates/default/{app → src/app}/(app)/privacy.tsx +35 -8
- package/dist/templates/default/src/app/(app)/profile/change-password.tsx +264 -0
- package/dist/templates/default/{app/(app)/profile.tsx → src/app/(app)/profile/index.tsx} +179 -255
- package/dist/templates/default/src/app/(app)/restore-account.tsx +192 -0
- package/dist/templates/default/src/app/(app)/sessions.tsx +287 -0
- package/dist/templates/default/src/app/(app)/welcome.tsx +194 -0
- package/dist/templates/default/src/app/+native-intent.tsx +25 -0
- package/dist/templates/default/src/app/+not-found.tsx +43 -0
- package/dist/templates/default/{app → src/app}/_layout.tsx +28 -37
- package/dist/templates/default/src/components/auth/apple-button.tsx +51 -0
- package/dist/templates/default/{components → src/components}/auth/otp-verification.tsx +79 -58
- package/dist/templates/default/{components → src/components}/auth/password-field.tsx +74 -18
- package/dist/templates/default/src/components/auth/segmented-toggle.tsx +71 -0
- package/dist/templates/default/src/components/ui/content-unavailable.tsx +81 -0
- package/dist/templates/default/src/components/ui/convex-error.tsx +21 -0
- package/dist/templates/default/src/components/ui/error-boundary.tsx +89 -0
- package/dist/templates/default/{components → src/components}/ui/loading-screen.tsx +5 -4
- package/dist/templates/default/{components → src/components}/ui/material.tsx +50 -17
- package/dist/templates/default/src/components/ui/offline-banner.tsx +59 -0
- package/dist/templates/default/{components → src/components}/ui/prominent-button.tsx +8 -11
- package/dist/templates/default/{components → src/components}/ui/skeleton.tsx +31 -13
- package/dist/templates/default/src/components/ui/status-text.tsx +64 -0
- package/dist/templates/default/src/components/ui/update-banner.tsx +85 -0
- package/dist/templates/default/{constants → src/constants}/layout.ts +0 -6
- package/dist/templates/default/{constants → src/constants}/theme.ts +49 -64
- package/dist/templates/default/{constants → src/constants}/ui.ts +13 -4
- package/dist/templates/default/src/hooks/use-debounce.ts +12 -0
- package/dist/templates/default/src/hooks/use-deep-link.ts +51 -0
- package/dist/templates/default/src/hooks/use-delete-account.ts +35 -0
- package/dist/templates/default/src/hooks/use-motion-screen-options.ts +13 -0
- package/dist/templates/default/src/hooks/use-network.ts +34 -0
- package/dist/templates/default/{hooks → src/hooks}/use-notifications.ts +39 -30
- package/dist/templates/default/src/hooks/use-reduce-transparency.ts +30 -0
- package/dist/templates/default/{hooks → src/hooks}/use-theme.ts +0 -5
- package/dist/templates/default/src/lib/appAttest.ts +78 -0
- package/dist/templates/default/src/lib/assets.ts +9 -0
- package/dist/templates/default/src/lib/deep-link.ts +82 -0
- package/dist/templates/default/{lib → src/lib}/dev-menu.ts +0 -4
- package/dist/templates/default/{lib → src/lib}/device.ts +1 -13
- package/dist/templates/default/{lib → src/lib}/dynamic-font.ts +13 -10
- package/dist/templates/default/src/lib/dynamic-symbol-size.ts +33 -0
- package/dist/templates/default/src/lib/masks.ts +21 -0
- package/dist/templates/default/src/lib/native-state.ts +20 -0
- package/dist/templates/default/{lib → src/lib}/notifications.ts +7 -45
- package/dist/templates/default/{lib → src/lib}/preferences.ts +0 -2
- package/dist/templates/default/{lib → src/lib}/schemas.ts +19 -16
- package/dist/templates/default/src/lib/text-style.ts +20 -0
- package/dist/templates/default/{lib → src/lib}/updates.ts +0 -7
- package/dist/templates/default/store.config.json +1 -1
- package/dist/templates/default/tsconfig.json +3 -1
- package/dist/templates/default/vitest.config.ts +8 -1
- package/package.json +5 -5
- package/dist/templates/default/app/(app)/_layout.tsx +0 -73
- package/dist/templates/default/app/(app)/debug.tsx +0 -389
- package/dist/templates/default/app/(app)/sessions.tsx +0 -191
- package/dist/templates/default/app/(app)/welcome.tsx +0 -140
- package/dist/templates/default/app/+native-intent.tsx +0 -14
- package/dist/templates/default/app/+not-found.tsx +0 -51
- package/dist/templates/default/bun.lock +0 -1860
- package/dist/templates/default/components/auth/segmented-toggle.tsx +0 -47
- package/dist/templates/default/components/ui/convex-error.tsx +0 -32
- package/dist/templates/default/components/ui/error-boundary.tsx +0 -57
- package/dist/templates/default/components/ui/offline-banner.tsx +0 -58
- package/dist/templates/default/components/ui/status-text.tsx +0 -49
- package/dist/templates/default/components/ui/update-banner.tsx +0 -82
- package/dist/templates/default/fingerprint.config.js +0 -9
- package/dist/templates/default/hooks/use-debounce.ts +0 -20
- package/dist/templates/default/hooks/use-deep-link.ts +0 -43
- package/dist/templates/default/hooks/use-network.ts +0 -11
- package/dist/templates/default/lib/assets.ts +0 -17
- package/dist/templates/default/lib/deep-link.ts +0 -71
- package/dist/templates/default/patches/PR-368.patch +0 -91
- package/dist/templates/default/patches/convex-dev-better-auth-0.12.2.tgz +0 -0
- /package/dist/templates/default/{hooks → src/hooks}/use-navigation-tracking.ts +0 -0
- /package/dist/templates/default/{hooks → src/hooks}/use-onboarding.ts +0 -0
- /package/dist/templates/default/{hooks → src/hooks}/use-reduced-motion.ts +0 -0
- /package/dist/templates/default/{hooks → src/hooks}/use-updates.ts +0 -0
- /package/dist/templates/default/{lib → src/lib}/a11y.ts +0 -0
- /package/dist/templates/default/{lib → src/lib}/app.ts +0 -0
- /package/dist/templates/default/{lib → src/lib}/auth-client.ts +0 -0
- /package/dist/templates/default/{lib → src/lib}/convex-auth.tsx +0 -0
- /package/dist/templates/default/{lib → src/lib}/env.ts +0 -0
- /package/dist/templates/default/{lib → src/lib}/haptics.ts +0 -0
- /package/dist/templates/default/{lib → src/lib}/storage.ts +0 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import {
|
|
2
|
+
attestKeyAsync,
|
|
3
|
+
generateAssertionAsync,
|
|
4
|
+
generateKeyAsync,
|
|
5
|
+
isSupported,
|
|
6
|
+
} from "@expo/app-integrity";
|
|
7
|
+
|
|
8
|
+
export type AppAttestClient = {
|
|
9
|
+
issueChallenge: () => Promise<{ nonce: string }>;
|
|
10
|
+
verifyAttestation: (args: {
|
|
11
|
+
keyId: string;
|
|
12
|
+
attestation: string;
|
|
13
|
+
challenge: string;
|
|
14
|
+
}) => Promise<{ keyId: string }>;
|
|
15
|
+
verifyAssertion: (args: {
|
|
16
|
+
keyId: string;
|
|
17
|
+
assertion: string;
|
|
18
|
+
payload: string;
|
|
19
|
+
}) => Promise<{ counter: number }>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const STORAGE_KEY = "vexpo.app-attest.key-id";
|
|
23
|
+
|
|
24
|
+
let cachedKeyId: string | null = null;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Whether the running device supports App Attest. False on iOS Simulator
|
|
28
|
+
* and on iOS < 14.
|
|
29
|
+
*/
|
|
30
|
+
export const supportsAppAttest = (): boolean => isSupported;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* App Attest requires the same keyId across a device's lifetime; the caller
|
|
34
|
+
* should persist the returned keyId and reuse it on every assertion. Rotating
|
|
35
|
+
* it costs a fresh attestation round-trip.
|
|
36
|
+
*/
|
|
37
|
+
export async function attestThisDevice(client: AppAttestClient): Promise<string> {
|
|
38
|
+
if (!isSupported) {
|
|
39
|
+
throw new Error("app-attest: device does not support App Attest");
|
|
40
|
+
}
|
|
41
|
+
const { nonce } = await client.issueChallenge();
|
|
42
|
+
const keyId = await generateKeyAsync();
|
|
43
|
+
const attestation = await attestKeyAsync(keyId, nonce);
|
|
44
|
+
await client.verifyAttestation({ keyId, attestation, challenge: nonce });
|
|
45
|
+
cachedKeyId = keyId;
|
|
46
|
+
return keyId;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* `payload` must be a deterministic encoding signed as the *exact* same bytes
|
|
51
|
+
* the server will verify (e.g. JSON.stringify with sorted keys, or the raw
|
|
52
|
+
* mutation arg string).
|
|
53
|
+
*/
|
|
54
|
+
export async function signRequest(
|
|
55
|
+
client: AppAttestClient,
|
|
56
|
+
keyId: string,
|
|
57
|
+
payload: string,
|
|
58
|
+
): Promise<{ counter: number }> {
|
|
59
|
+
if (!isSupported) {
|
|
60
|
+
throw new Error("app-attest: device does not support App Attest");
|
|
61
|
+
}
|
|
62
|
+
// Sign the exact bytes the server reconstructs: it hashes `payload` and
|
|
63
|
+
// verifies the signature over sha256(authData || sha256(payload)). Replay is
|
|
64
|
+
// blocked server-side by the strictly-increasing assertion counter, so no
|
|
65
|
+
// per-request challenge is needed here (challenges are an attestation step).
|
|
66
|
+
const assertion = await generateAssertionAsync(keyId, payload);
|
|
67
|
+
return client.verifyAssertion({ keyId, assertion, payload });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function getCachedKeyId(): string | null {
|
|
71
|
+
return cachedKeyId;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function setCachedKeyId(keyId: string | null): void {
|
|
75
|
+
cachedKeyId = keyId;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const APP_ATTEST_STORAGE_KEY = STORAGE_KEY;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const assets = {
|
|
2
|
+
icon: require("@/assets/icon.png"),
|
|
3
|
+
brandIconLight: require("@/assets/brand-icon-light.png"),
|
|
4
|
+
brandIconDark: require("@/assets/brand-icon-dark.png"),
|
|
5
|
+
splashLight: require("@/assets/splash-image-light.png"),
|
|
6
|
+
splashDark: require("@/assets/splash-image-dark.png"),
|
|
7
|
+
} as const;
|
|
8
|
+
|
|
9
|
+
export const assetModules = Object.values(assets);
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { Href } from "expo-router";
|
|
2
|
+
import { parse, createURL } from "expo-linking";
|
|
3
|
+
|
|
4
|
+
export const DeepLinkRoutes = {
|
|
5
|
+
"/": "/",
|
|
6
|
+
"/welcome": "/welcome",
|
|
7
|
+
"/settings": "/(app)/(tabs)/settings",
|
|
8
|
+
"/about": "/help",
|
|
9
|
+
"/help": "/help",
|
|
10
|
+
"/privacy": "/privacy",
|
|
11
|
+
"/auth/sign-in": "/auth/sign-in",
|
|
12
|
+
"/auth/sign-up": "/auth/sign-up",
|
|
13
|
+
"/auth/forgot-password": "/auth/forgot-password",
|
|
14
|
+
"/auth/reset-password": "/auth/reset-password",
|
|
15
|
+
"/sign-in": "/auth/sign-in",
|
|
16
|
+
"/sign-up": "/auth/sign-up",
|
|
17
|
+
"/forgot-password": "/auth/forgot-password",
|
|
18
|
+
"/reset-password": "/auth/reset-password",
|
|
19
|
+
"/linked": "/linked",
|
|
20
|
+
} as const satisfies Record<string, Href>;
|
|
21
|
+
|
|
22
|
+
export type DeepLinkPath = keyof typeof DeepLinkRoutes;
|
|
23
|
+
|
|
24
|
+
function normalizePath(raw: string | null | undefined): string {
|
|
25
|
+
const trimmed = "/" + (raw ?? "").replace(/^\//, "").replace(/\/+$/, "");
|
|
26
|
+
return trimmed === "/" ? "/" : trimmed;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function isDeepLinkPath(path: string): path is DeepLinkPath {
|
|
30
|
+
return path in DeepLinkRoutes;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function isValidDeepLink(url: string): boolean {
|
|
34
|
+
if (!url || typeof url !== "string") return false;
|
|
35
|
+
if (url.includes("..")) return false;
|
|
36
|
+
|
|
37
|
+
let parsed;
|
|
38
|
+
try {
|
|
39
|
+
parsed = parse(url);
|
|
40
|
+
} catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const isRelativePath = url.startsWith("/") && !url.startsWith("//");
|
|
45
|
+
if (!isRelativePath && !parsed.scheme) return false;
|
|
46
|
+
|
|
47
|
+
return isDeepLinkPath(normalizePath(parsed.path));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type ResolvedDeepLink = {
|
|
51
|
+
path: DeepLinkPath | null;
|
|
52
|
+
href: Href | null;
|
|
53
|
+
params: Record<string, string>;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export function resolveDeepLink(url: string): ResolvedDeepLink {
|
|
57
|
+
const empty: ResolvedDeepLink = { path: null, href: null, params: {} };
|
|
58
|
+
if (!url || typeof url !== "string") return empty;
|
|
59
|
+
|
|
60
|
+
let parsed;
|
|
61
|
+
try {
|
|
62
|
+
parsed = parse(url);
|
|
63
|
+
} catch {
|
|
64
|
+
return empty;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (!isValidDeepLink(url)) return empty;
|
|
68
|
+
|
|
69
|
+
const path = normalizePath(parsed.path) as DeepLinkPath;
|
|
70
|
+
|
|
71
|
+
const params: Record<string, string> = {};
|
|
72
|
+
if (parsed.queryParams) {
|
|
73
|
+
for (const [key, value] of Object.entries(parsed.queryParams)) {
|
|
74
|
+
if (value == null) continue;
|
|
75
|
+
params[key] = Array.isArray(value) ? value.join(",") : value;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { path, href: DeepLinkRoutes[path], params };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export { createURL };
|
|
@@ -43,10 +43,6 @@ function clearLocalStorage() {
|
|
|
43
43
|
console.log("[DevMenu] localStorage cleared");
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
/**
|
|
47
|
-
* Registers custom dev menu items visible when shaking the device.
|
|
48
|
-
* Call once at app startup. No-op in production builds.
|
|
49
|
-
*/
|
|
50
46
|
export function registerDevMenuItems() {
|
|
51
47
|
if (!__DEV__) return;
|
|
52
48
|
|
|
@@ -1,40 +1,28 @@
|
|
|
1
1
|
import Constants, { ExecutionEnvironment } from "expo-constants";
|
|
2
2
|
|
|
3
|
-
/** Current execution environment: Bare, Standalone, or StoreClient. */
|
|
4
3
|
export const executionEnvironment = Constants.executionEnvironment;
|
|
5
4
|
|
|
6
|
-
/** Production/release build created with or without EAS Build. */
|
|
7
5
|
export const isStandalone = executionEnvironment === ExecutionEnvironment.Standalone;
|
|
8
6
|
|
|
9
|
-
/** Running in Expo Go or a development build with expo-dev-client. */
|
|
10
7
|
export const isStoreClient = executionEnvironment === ExecutionEnvironment.StoreClient;
|
|
11
8
|
|
|
12
|
-
/** True when running in debug mode (__DEV__). */
|
|
13
9
|
export const debugMode = Constants.debugMode;
|
|
14
10
|
|
|
15
|
-
/** Unique per app session. Changes on every fresh launch. */
|
|
16
11
|
export const sessionId = Constants.sessionId;
|
|
17
12
|
|
|
18
|
-
/** True if the app is running headless (background task, no UI). */
|
|
19
13
|
export const isHeadless = Constants.isHeadless;
|
|
20
14
|
|
|
21
|
-
/**
|
|
15
|
+
/** Does not account for calls or location tracking. */
|
|
22
16
|
export const statusBarHeight = Constants.statusBarHeight;
|
|
23
17
|
|
|
24
|
-
/** Runtime version string. Null on web. */
|
|
25
18
|
export const expoRuntimeVersion = Constants.expoRuntimeVersion;
|
|
26
19
|
|
|
27
|
-
/** Human-readable device name (e.g. "Ramon's iPhone"). */
|
|
28
20
|
export const deviceName = Constants.deviceName;
|
|
29
21
|
|
|
30
|
-
/** System font names available on this device. */
|
|
31
22
|
export const systemFonts = Constants.systemFonts;
|
|
32
23
|
|
|
33
|
-
/** EAS config object. Non-null when built with EAS Build. */
|
|
34
24
|
export const easConfig = Constants.easConfig;
|
|
35
25
|
|
|
36
|
-
/** iOS-specific manifest: buildNumber, model, systemVersion, etc. */
|
|
37
26
|
export const iosManifest = Constants.platform?.ios;
|
|
38
27
|
|
|
39
|
-
/** Resolves the user agent string a webview would send from this device. */
|
|
40
28
|
export const getWebViewUserAgentAsync = Constants.getWebViewUserAgentAsync;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { useCallback } from "react";
|
|
2
|
-
import { useWindowDimensions } from "react-native";
|
|
3
2
|
import { font } from "@expo/ui/swift-ui/modifiers";
|
|
4
3
|
|
|
4
|
+
import { textStyleForSize } from "@/lib/text-style";
|
|
5
|
+
|
|
5
6
|
type FontParams = Parameters<typeof font>[0];
|
|
6
7
|
type Weight = NonNullable<FontParams["weight"]>;
|
|
7
8
|
type Design = NonNullable<FontParams["design"]>;
|
|
@@ -36,14 +37,16 @@ function resolveFamily(weight: Weight | undefined, design: Design | undefined):
|
|
|
36
37
|
return GEIST_BY_WEIGHT[w];
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
// upstream expo/expo#46007: passing the `font` modifier a `textStyle` scales the
|
|
41
|
+
// Geist family with iOS Dynamic Type natively (Apple's Larger Text path) instead
|
|
42
|
+
// of a JS-side `fontScale` multiply. `textStyleForSize` picks the style from the
|
|
43
|
+
// declared size, which stays the base, so default-size rendering is unchanged
|
|
44
|
+
// and SwiftUI rescales without a JS re-render.
|
|
39
45
|
export function useDynamicFont() {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
},
|
|
47
|
-
[fontScale],
|
|
48
|
-
);
|
|
46
|
+
return useCallback((params: FontParams) => {
|
|
47
|
+
const family = params.family ?? resolveFamily(params.weight, params.design);
|
|
48
|
+
const textStyle =
|
|
49
|
+
params.textStyle ?? (params.size != null ? textStyleForSize(params.size) : undefined);
|
|
50
|
+
return font({ ...params, family, textStyle });
|
|
51
|
+
}, []);
|
|
49
52
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import { useWindowDimensions } from "react-native";
|
|
3
|
+
|
|
4
|
+
// Ceiling on the Dynamic Type multiplier for SF Symbols. At the largest
|
|
5
|
+
// accessibility sizes `fontScale` passes 3x, which overruns icons pinned to a
|
|
6
|
+
// fixed frame (the 80x80 welcome glyph, the 44x44 eye toggle). `dynamicTypeSize`
|
|
7
|
+
// can't bound these: the size is computed here in JS, not through the SwiftUI
|
|
8
|
+
// Dynamic Type environment. So cap the multiplier, the icon analogue of the
|
|
9
|
+
// `dynamicTypeSize` clamp on fixed-geometry text (upstream expo/expo#46540).
|
|
10
|
+
//
|
|
11
|
+
// WORKAROUND, superseded by expo/expo#46714 (open). That PR makes ImageView's
|
|
12
|
+
// SF symbol branch honor font/imageScale/dynamicTypeSize modifiers, so the
|
|
13
|
+
// clamp can move into the SwiftUI Dynamic Type environment natively. Once it
|
|
14
|
+
// merges and ships in an `@expo/ui` release: put `dynamicTypeSize` on each
|
|
15
|
+
// `<Image systemName>` consumer and delete this hook. Consumers are every
|
|
16
|
+
// importer of `useSymbolSize` (grep it, 14 files).
|
|
17
|
+
const MAX_SYMBOL_SCALE = 1.6;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Scales an SF Symbol `<Image systemName>` size with the system Dynamic Type
|
|
21
|
+
* slider, bounded at `MAX_SYMBOL_SCALE` so icons in fixed frames don't overflow.
|
|
22
|
+
* SwiftUI `Label` carries this automatically when icons are paired with text;
|
|
23
|
+
* standalone `Image systemName=` calls don't, so multiply the base size here.
|
|
24
|
+
*
|
|
25
|
+
* Temporary: replaced by native modifiers when expo/expo#46714 ships.
|
|
26
|
+
*/
|
|
27
|
+
export function useSymbolSize(): (size: number) => number {
|
|
28
|
+
const { fontScale } = useWindowDimensions();
|
|
29
|
+
return useCallback(
|
|
30
|
+
(size: number) => Math.round(size * Math.min(fontScale, MAX_SYMBOL_SCALE)),
|
|
31
|
+
[fontScale],
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { USERNAME_MAX_LENGTH } from "@/convex/constants";
|
|
2
|
+
|
|
3
|
+
// Synchronous input masks for `@expo/ui` `TextField`/`SecureField`. Marked
|
|
4
|
+
// `"worklet"` so an `onTextChange` worklet can call them on the UI thread and
|
|
5
|
+
// rewrite the field's bound `useNativeState` on the same frame the keystroke
|
|
6
|
+
// lands. Sanitizing on the JS thread instead paints the raw character first,
|
|
7
|
+
// then strips it a frame later once the round-trip completes. that flicker is
|
|
8
|
+
// exactly what the worklet path removes (SDK 56 Expo UI worklet integration).
|
|
9
|
+
|
|
10
|
+
export function maskOtp(text: string): string {
|
|
11
|
+
"worklet";
|
|
12
|
+
return text.replace(/\D/g, "").slice(0, 6);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function maskUsername(text: string): string {
|
|
16
|
+
"worklet";
|
|
17
|
+
return text
|
|
18
|
+
.toLowerCase()
|
|
19
|
+
.replace(/[^a-z0-9._]/g, "")
|
|
20
|
+
.slice(0, USERNAME_MAX_LENGTH);
|
|
21
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { useNativeState } from "@expo/ui/swift-ui";
|
|
2
|
+
import { runOnUI } from "react-native-worklets";
|
|
3
|
+
|
|
4
|
+
type ObservableState<T> = ReturnType<typeof useNativeState<T>>;
|
|
5
|
+
|
|
6
|
+
// Hops a write to `ObservableState.value` onto the UI worklet runtime so the
|
|
7
|
+
// update lands synchronously on the thread that drives the SwiftUI host. Reads
|
|
8
|
+
// of `.value` are safe from any thread. A JS-thread write still applies, but
|
|
9
|
+
// it's scheduled to the UI thread asynchronously (not readable until it lands)
|
|
10
|
+
// and emits a one-time dev `console.warn` ("ObservableState.value was set from
|
|
11
|
+
// the JS thread..."). The worklet hop lands the write on the same frame and
|
|
12
|
+
// silences that warning. `@expo/ui/swift-ui` registers the SharedObject
|
|
13
|
+
// worklet serializer (`State/index.fx`) so `state` crosses thread boundaries
|
|
14
|
+
// safely.
|
|
15
|
+
export function setNativeValue<T>(state: ObservableState<T>, value: T): void {
|
|
16
|
+
runOnUI(() => {
|
|
17
|
+
"worklet";
|
|
18
|
+
state.value = value;
|
|
19
|
+
})();
|
|
20
|
+
}
|
|
@@ -3,10 +3,6 @@ import * as Device from "expo-device";
|
|
|
3
3
|
import * as TaskManager from "expo-task-manager";
|
|
4
4
|
import Constants from "expo-constants";
|
|
5
5
|
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
// Background task
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
|
|
10
6
|
export const BACKGROUND_NOTIFICATION_TASK = "BACKGROUND_NOTIFICATION";
|
|
11
7
|
|
|
12
8
|
try {
|
|
@@ -24,10 +20,6 @@ try {
|
|
|
24
20
|
if (__DEV__) console.warn("[Notification] defineTask failed:", e);
|
|
25
21
|
}
|
|
26
22
|
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
// Foreground handler
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
|
|
31
23
|
interface ForegroundOptions {
|
|
32
24
|
shouldShowBanner?: boolean;
|
|
33
25
|
shouldShowList?: boolean;
|
|
@@ -36,7 +28,9 @@ interface ForegroundOptions {
|
|
|
36
28
|
}
|
|
37
29
|
|
|
38
30
|
export function setForegroundHandler(options?: ForegroundOptions) {
|
|
39
|
-
|
|
31
|
+
// No Device guard: setNotificationHandler is pure JS (no APNs, no native
|
|
32
|
+
// gate) and runs fine on the simulator, where local notifications scheduled
|
|
33
|
+
// via the helpers below still surface a foreground banner.
|
|
40
34
|
try {
|
|
41
35
|
Notifications.setNotificationHandler({
|
|
42
36
|
handleNotification: async () => ({
|
|
@@ -53,17 +47,13 @@ export function setForegroundHandler(options?: ForegroundOptions) {
|
|
|
53
47
|
|
|
54
48
|
export function registerBackgroundTask() {
|
|
55
49
|
if (!Device.isDevice) return;
|
|
56
|
-
try
|
|
57
|
-
|
|
58
|
-
|
|
50
|
+
// registerTaskAsync is async: a try/catch around the un-awaited call can't
|
|
51
|
+
// trap its rejection (e.g. UnavailabilityError), so attach .catch instead.
|
|
52
|
+
Notifications.registerTaskAsync(BACKGROUND_NOTIFICATION_TASK).catch((e) => {
|
|
59
53
|
if (__DEV__) console.warn("[Notification] registerTaskAsync failed:", e);
|
|
60
|
-
}
|
|
54
|
+
});
|
|
61
55
|
}
|
|
62
56
|
|
|
63
|
-
// ---------------------------------------------------------------------------
|
|
64
|
-
// Permissions
|
|
65
|
-
// ---------------------------------------------------------------------------
|
|
66
|
-
|
|
67
57
|
export async function getPermissionStatus() {
|
|
68
58
|
const settings = await Notifications.getPermissionsAsync();
|
|
69
59
|
return {
|
|
@@ -93,10 +83,6 @@ export async function requestPermission() {
|
|
|
93
83
|
};
|
|
94
84
|
}
|
|
95
85
|
|
|
96
|
-
// ---------------------------------------------------------------------------
|
|
97
|
-
// Push tokens
|
|
98
|
-
// ---------------------------------------------------------------------------
|
|
99
|
-
|
|
100
86
|
export async function getExpoPushToken(): Promise<string | null> {
|
|
101
87
|
if (!Device.isDevice) return null;
|
|
102
88
|
|
|
@@ -127,10 +113,6 @@ export async function getDevicePushToken(): Promise<string | null> {
|
|
|
127
113
|
}
|
|
128
114
|
}
|
|
129
115
|
|
|
130
|
-
// ---------------------------------------------------------------------------
|
|
131
|
-
// Scheduling
|
|
132
|
-
// ---------------------------------------------------------------------------
|
|
133
|
-
|
|
134
116
|
type ContentInput = Notifications.NotificationContentInput;
|
|
135
117
|
type TriggerInput = Notifications.NotificationTriggerInput;
|
|
136
118
|
|
|
@@ -212,10 +194,6 @@ export function scheduleYearly(
|
|
|
212
194
|
});
|
|
213
195
|
}
|
|
214
196
|
|
|
215
|
-
// ---------------------------------------------------------------------------
|
|
216
|
-
// Schedule management
|
|
217
|
-
// ---------------------------------------------------------------------------
|
|
218
|
-
|
|
219
197
|
export function getAllScheduled() {
|
|
220
198
|
return Notifications.getAllScheduledNotificationsAsync();
|
|
221
199
|
}
|
|
@@ -235,10 +213,6 @@ export async function getNextTriggerDate(
|
|
|
235
213
|
return timestamp ? new Date(timestamp) : null;
|
|
236
214
|
}
|
|
237
215
|
|
|
238
|
-
// ---------------------------------------------------------------------------
|
|
239
|
-
// Badge
|
|
240
|
-
// ---------------------------------------------------------------------------
|
|
241
|
-
|
|
242
216
|
export function getBadgeCount() {
|
|
243
217
|
return Notifications.getBadgeCountAsync();
|
|
244
218
|
}
|
|
@@ -247,10 +221,6 @@ export function setBadgeCount(count: number) {
|
|
|
247
221
|
return Notifications.setBadgeCountAsync(count);
|
|
248
222
|
}
|
|
249
223
|
|
|
250
|
-
// ---------------------------------------------------------------------------
|
|
251
|
-
// Dismiss
|
|
252
|
-
// ---------------------------------------------------------------------------
|
|
253
|
-
|
|
254
224
|
export function dismissNotification(id: string) {
|
|
255
225
|
return Notifications.dismissNotificationAsync(id);
|
|
256
226
|
}
|
|
@@ -259,18 +229,10 @@ export function dismissAllNotifications() {
|
|
|
259
229
|
return Notifications.dismissAllNotificationsAsync();
|
|
260
230
|
}
|
|
261
231
|
|
|
262
|
-
// ---------------------------------------------------------------------------
|
|
263
|
-
// Presented notifications
|
|
264
|
-
// ---------------------------------------------------------------------------
|
|
265
|
-
|
|
266
232
|
export function getPresentedNotifications() {
|
|
267
233
|
return Notifications.getPresentedNotificationsAsync();
|
|
268
234
|
}
|
|
269
235
|
|
|
270
|
-
// ---------------------------------------------------------------------------
|
|
271
|
-
// Last notification response
|
|
272
|
-
// ---------------------------------------------------------------------------
|
|
273
|
-
|
|
274
236
|
export function clearLastNotificationResponse() {
|
|
275
237
|
Notifications.clearLastNotificationResponse();
|
|
276
238
|
}
|
|
@@ -6,8 +6,6 @@ export type ReduceMotionPref = "system" | "always" | "never";
|
|
|
6
6
|
|
|
7
7
|
const hapticsStore = createStorage<boolean>("pref.hapticsEnabled", true);
|
|
8
8
|
const reduceMotionStore = createStorage<ReduceMotionPref>("pref.reduceMotion", "system");
|
|
9
|
-
// Default on in dev, off in production. Reveals the Debug screen with version,
|
|
10
|
-
// device, OTA update, and push diagnostics.
|
|
11
9
|
const debugEnabledStore = createStorage<boolean>("pref.debugEnabled", __DEV__);
|
|
12
10
|
|
|
13
11
|
export const preferences = {
|
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Form validation schemas.
|
|
3
|
-
*
|
|
4
|
-
* Each form parses raw input via `schema.safeParse(values)` inside
|
|
5
|
-
* `useActionState`. Errors flatten to inline `Section.footer` text under each
|
|
6
|
-
* field. Constants and reserved-name helpers come from `@/convex/constants`
|
|
7
|
-
* to keep client/server in sync.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
1
|
import { z } from "zod";
|
|
11
2
|
|
|
12
3
|
import {
|
|
@@ -32,9 +23,6 @@ const usernameSchema = z
|
|
|
32
23
|
.regex(USERNAME_FORMAT_REGEX, { error: "Letters, numbers, dots, and underscores only" })
|
|
33
24
|
.refine((value) => !isReservedUsername(value), { error: "That username is reserved" });
|
|
34
25
|
|
|
35
|
-
// Optional variant used at sign-up: empty string is valid (the user can pick
|
|
36
|
-
// a handle later from the profile screen). Format and reserved checks only
|
|
37
|
-
// apply when the user actually typed something.
|
|
38
26
|
const optionalUsernameSchema = z
|
|
39
27
|
.string()
|
|
40
28
|
.trim()
|
|
@@ -117,6 +105,17 @@ export const profileUpdateSchema = z.object({
|
|
|
117
105
|
email: emailSchema,
|
|
118
106
|
});
|
|
119
107
|
|
|
108
|
+
// Accounts can exist without a username (the column is nullable), and the
|
|
109
|
+
// profile field shows "" for them. The strict `usernameSchema` rejects ""
|
|
110
|
+
// (min length 3), which would block those users from saving name/email/bio
|
|
111
|
+
// they never touched. This variant accepts "" so the username stays untouched
|
|
112
|
+
// while the other fields update, mirroring how `signUpSchema` treats it.
|
|
113
|
+
export const profileUpdateOptionalUsernameSchema = z.object({
|
|
114
|
+
name: nameSchema,
|
|
115
|
+
username: optionalUsernameSchema,
|
|
116
|
+
email: emailSchema,
|
|
117
|
+
});
|
|
118
|
+
|
|
120
119
|
export type SignInValues = z.infer<typeof signInSchema>;
|
|
121
120
|
export type SignInEmailValues = z.infer<typeof signInEmailSchema>;
|
|
122
121
|
export type SignInUsernameValues = z.infer<typeof signInUsernameSchema>;
|
|
@@ -125,13 +124,17 @@ export type ForgotPasswordValues = z.infer<typeof forgotPasswordSchema>;
|
|
|
125
124
|
export type ResetPasswordValues = z.infer<typeof resetPasswordSchema>;
|
|
126
125
|
export type ProfileUpdateValues = z.infer<typeof profileUpdateSchema>;
|
|
127
126
|
|
|
128
|
-
/**
|
|
129
|
-
* Extract the first error message from a zod safeParse result, formatted for
|
|
130
|
-
* inline display. Returns `null` if validation succeeded.
|
|
131
|
-
*/
|
|
132
127
|
export function firstError(
|
|
133
128
|
result: { success: false; error: z.ZodError } | { success: true; data: unknown },
|
|
134
129
|
): string | null {
|
|
135
130
|
if (result.success) return null;
|
|
136
131
|
return result.error.issues[0]?.message ?? "Invalid input";
|
|
137
132
|
}
|
|
133
|
+
|
|
134
|
+
export function firstErrorField(
|
|
135
|
+
result: { success: false; error: z.ZodError } | { success: true; data: unknown },
|
|
136
|
+
): string | null {
|
|
137
|
+
if (result.success) return null;
|
|
138
|
+
const first = result.error.issues[0]?.path[0];
|
|
139
|
+
return typeof first === "string" ? first : null;
|
|
140
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { font } from "@expo/ui/swift-ui/modifiers";
|
|
2
|
+
|
|
3
|
+
type TextStyle = NonNullable<Parameters<typeof font>[0]["textStyle"]>;
|
|
4
|
+
|
|
5
|
+
// upstream expo/expo#46007: map the template's point-size scale onto a SwiftUI
|
|
6
|
+
// `Font.TextStyle` so text rides Apple's Dynamic Type curves (the Larger Text
|
|
7
|
+
// path). The declared size stays the base, the style only sets the scaling
|
|
8
|
+
// curve, so default-size rendering is unchanged.
|
|
9
|
+
export function textStyleForSize(size: number): TextStyle {
|
|
10
|
+
if (size >= 31) return "largeTitle";
|
|
11
|
+
if (size >= 26) return "title";
|
|
12
|
+
if (size >= 21) return "title2";
|
|
13
|
+
if (size >= 18) return "title3";
|
|
14
|
+
if (size >= 17) return "body";
|
|
15
|
+
if (size >= 16) return "callout";
|
|
16
|
+
if (size >= 15) return "subheadline";
|
|
17
|
+
if (size >= 13) return "footnote";
|
|
18
|
+
if (size >= 12) return "caption";
|
|
19
|
+
return "caption2";
|
|
20
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import {
|
|
2
|
-
// Constants
|
|
3
2
|
isEnabled,
|
|
4
3
|
updateId,
|
|
5
4
|
channel,
|
|
@@ -11,8 +10,6 @@ import {
|
|
|
11
10
|
manifest,
|
|
12
11
|
createdAt,
|
|
13
12
|
launchDuration,
|
|
14
|
-
|
|
15
|
-
// Methods
|
|
16
13
|
checkForUpdateAsync,
|
|
17
14
|
fetchUpdateAsync,
|
|
18
15
|
reloadAsync,
|
|
@@ -24,15 +21,11 @@ import {
|
|
|
24
21
|
setUpdateURLAndRequestHeadersOverride,
|
|
25
22
|
showReloadScreen,
|
|
26
23
|
hideReloadScreen,
|
|
27
|
-
|
|
28
|
-
// Enums (runtime values)
|
|
29
24
|
UpdateCheckResultNotAvailableReason,
|
|
30
25
|
UpdatesLogEntryCode,
|
|
31
26
|
UpdatesLogEntryLevel,
|
|
32
27
|
UpdatesCheckAutomaticallyValue,
|
|
33
28
|
UpdateInfoType,
|
|
34
|
-
|
|
35
|
-
// Hook
|
|
36
29
|
useUpdates,
|
|
37
30
|
} from "expo-updates";
|
|
38
31
|
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"lastName": "YOUR_LAST_NAME",
|
|
41
41
|
"email": "reviewer@example.com",
|
|
42
42
|
"phone": "+15555555555",
|
|
43
|
-
"notes": "Run `
|
|
43
|
+
"notes": "Run `npx vexpo rebrand` (or `npx vexpo full`) before submitting. The wizard replaces every placeholder in this file with your fork's real values.",
|
|
44
44
|
"demoUsername": "review@example.com",
|
|
45
45
|
"demoPassword": "REPLACE_BEFORE_SUBMIT"
|
|
46
46
|
},
|
|
@@ -2,9 +2,16 @@ import { defineConfig } from "vitest/config";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
4
|
export default defineConfig({
|
|
5
|
+
// Metro injects `__DEV__` at bundle time; define it for node so RN/Expo
|
|
6
|
+
// modules that branch on it (e.g. `+native-intent.tsx`) are unit-testable.
|
|
7
|
+
define: {
|
|
8
|
+
__DEV__: "false",
|
|
9
|
+
},
|
|
5
10
|
resolve: {
|
|
6
11
|
alias: {
|
|
7
|
-
"
|
|
12
|
+
"@/convex": path.resolve(__dirname, "convex"),
|
|
13
|
+
"@/assets": path.resolve(__dirname, "assets"),
|
|
14
|
+
"@": path.resolve(__dirname, "src"),
|
|
8
15
|
},
|
|
9
16
|
},
|
|
10
17
|
test: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ramonclaudio/create-vexpo",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Scaffold a new vexpo project. Expo SDK 56 + Convex + Better Auth + Resend, wired for iOS, real auth, real push, real OTA, real App Store submission.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"better-auth",
|
|
@@ -31,8 +31,7 @@
|
|
|
31
31
|
"create-vexpo": "./dist/index.js"
|
|
32
32
|
},
|
|
33
33
|
"files": [
|
|
34
|
-
"dist"
|
|
35
|
-
"templates"
|
|
34
|
+
"dist"
|
|
36
35
|
],
|
|
37
36
|
"type": "module",
|
|
38
37
|
"exports": {
|
|
@@ -47,18 +46,19 @@
|
|
|
47
46
|
"scripts": {
|
|
48
47
|
"build": "tsup",
|
|
49
48
|
"dev": "tsup --watch",
|
|
49
|
+
"prepublishOnly": "npm run build",
|
|
50
50
|
"typecheck": "tsc --noEmit",
|
|
51
51
|
"test": "echo '(create-vexpo: no per-package tests yet. covered by template e2e)'"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"commander": "^
|
|
54
|
+
"commander": "^15.0.0",
|
|
55
55
|
"execa": "^9.6.1",
|
|
56
56
|
"kleur": "^4.1.5",
|
|
57
57
|
"ora": "^9.4.0",
|
|
58
58
|
"prompts": "^2.4.2"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
-
"@types/node": "^25.
|
|
61
|
+
"@types/node": "^25.9.0",
|
|
62
62
|
"@types/prompts": "^2.4.9",
|
|
63
63
|
"tsup": "^8.5.1",
|
|
64
64
|
"typescript": "^6.0.3"
|