@ramonclaudio/create-vexpo 0.1.2 → 0.1.4
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 +25 -15
- package/dist/index.js +47 -14
- package/dist/templates/default/.eas/workflows/e2e-tests.yml +14 -1
- package/dist/templates/default/.eas/workflows/rotate-apple-jwt.yml +3 -3
- package/dist/templates/default/.maestro/auth.yaml +229 -0
- package/dist/templates/default/.maestro/launch.yaml +5 -5
- package/dist/templates/default/.maestro/tour.yaml +294 -0
- package/dist/templates/default/.maestro/zz-delete-restore.yaml +174 -0
- package/dist/templates/default/AGENTS.md +3 -2
- package/dist/templates/default/DESIGN.md +41 -41
- package/dist/templates/default/README.md +38 -41
- package/dist/templates/default/SETUP.md +34 -19
- package/dist/templates/default/_easignore +0 -1
- package/dist/templates/default/_env.example +15 -10
- package/dist/templates/default/app.config.ts +5 -5
- package/dist/templates/default/convex/pushTokens.ts +1 -26
- package/dist/templates/default/convex/rateLimit.ts +1 -21
- package/dist/templates/default/convex/users.ts +1 -49
- package/dist/templates/default/convex/validators.ts +0 -10
- package/dist/templates/default/package.json +15 -15
- package/dist/templates/default/scripts/README.md +24 -8
- package/dist/templates/default/scripts/clean.ts +3 -3
- package/dist/templates/default/scripts/gen-update-cert.mjs +3 -1
- package/dist/templates/default/src/app/(app)/_layout.tsx +15 -1
- package/dist/templates/default/src/app/(app)/auth/forgot-password.tsx +3 -0
- package/dist/templates/default/src/app/(app)/auth/reset-password.tsx +3 -0
- package/dist/templates/default/src/app/(app)/auth/sign-up.tsx +3 -1
- package/dist/templates/default/src/app/(app)/privacy.tsx +3 -2
- package/dist/templates/default/src/app/(app)/restore-account.tsx +2 -2
- package/dist/templates/default/src/app/(app)/sessions.tsx +15 -5
- package/dist/templates/default/src/components/ui/convex-error.tsx +0 -7
- package/dist/templates/default/src/constants/ui.ts +0 -11
- package/dist/templates/default/src/lib/dev-menu.ts +11 -2
- package/dist/templates/default/src/lib/preferences.ts +9 -0
- package/package.json +3 -2
|
@@ -4,29 +4,45 @@ Build and maintenance scripts. Setup orchestration lives in the published `vexpo
|
|
|
4
4
|
|
|
5
5
|
## What's in this directory
|
|
6
6
|
|
|
7
|
-
| Script | What it does
|
|
8
|
-
| ---------------------- |
|
|
9
|
-
| `clean.ts` | Trash + reinstall. `--metro` for cache-only nuke (Metro/
|
|
10
|
-
| `
|
|
11
|
-
| `
|
|
7
|
+
| Script | What it does |
|
|
8
|
+
| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
9
|
+
| `clean.ts` | Trash + reinstall. `--metro` for cache-only nuke (Metro/Haste/node-compile-cache). `--state` also wipes `.setup-state.json`. |
|
|
10
|
+
| `gen-update-cert.mjs` | One-shot OTA update code-signing setup. Wraps `npx expo-updates codesigning:generate`, writes `certs/certificate.pem` (committed) and `../keys/private-key.pem` (gitignored). Run via `npm run updates:gen-cert -- --name "<Org>"`. |
|
|
11
|
+
| `rotate-apple-jwt.mjs` | Re-signs the Apple Sign In `client_secret` JWT from env vars only. Used by `.eas/workflows/rotate-apple-jwt.yml` every 90 days. |
|
|
12
|
+
| `_run.mjs` | Runtime selector for `clean.ts`. Picks `bun` if available, falls back to `tsx`. Not used by the CLI. |
|
|
12
13
|
|
|
13
14
|
Anything else (preflight checks, env validation, version bumps) lives in the `vexpo` CLI or in `eas-cli` directly.
|
|
14
15
|
|
|
16
|
+
## Cleaning
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm run clean # trash + reinstall
|
|
20
|
+
npm run clean:metro # just Metro/Haste/node-compile-cache
|
|
21
|
+
npm run clean:state # also wipe .setup-state.json
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
These run through `_run.mjs`, which picks `bun` if it's on PATH and falls back to `tsx`. To call it without the npm script: `node scripts/_run.mjs scripts/clean.ts --metro`.
|
|
25
|
+
|
|
15
26
|
## Setup orchestration
|
|
16
27
|
|
|
17
|
-
Use the `vexpo` CLI:
|
|
28
|
+
Use the `vexpo` CLI. `lite` and `full` are alternatives, pick the path you need:
|
|
18
29
|
|
|
19
30
|
```bash
|
|
20
31
|
npx vexpo lite # dev-mode setup (Convex + Better Auth only)
|
|
21
32
|
npx vexpo full # full provisioning to TestFlight-ready
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The rest are independent maintenance commands, run them when you need them:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
22
38
|
npx vexpo doctor # cross-source drift detection
|
|
23
|
-
npx vexpo env push # sync from .env.local + .env.prod to Convex
|
|
39
|
+
npx vexpo env push # sync from .env.local + .env.prod to Convex and EAS
|
|
24
40
|
npx vexpo apple asc-key # validate ASC API key
|
|
25
41
|
npx vexpo apple services-id # attach SIWA capability to App ID
|
|
26
42
|
npx vexpo apple jwt # sign client_secret JWT, push to Convex
|
|
27
43
|
```
|
|
28
44
|
|
|
29
|
-
Version bumps run through `eas build:version:set`
|
|
45
|
+
Version bumps run through `eas build:version:set` or `eas build:version:sync`. `appVersionSource: "remote"` in `eas.json` puts EAS in charge of the version.
|
|
30
46
|
|
|
31
47
|
The CLI itself ships from [`@ramonclaudio/vexpo` on npm](https://www.npmjs.com/package/@ramonclaudio/vexpo). Source lives at [`github.com/ramonclaudio/vexpo`](https://github.com/ramonclaudio/vexpo).
|
|
32
48
|
|
|
@@ -152,7 +152,7 @@ const HELP = `${BOLD}vexpo clean${RESET}
|
|
|
152
152
|
${BOLD}Usage:${RESET}
|
|
153
153
|
${DIM}npm run clean${RESET} wipe caches, keep lockfile, frozen install
|
|
154
154
|
${DIM}npm run clean --all${RESET} also wipe lockfile + convex/_generated
|
|
155
|
-
${DIM}npm run clean --metro${RESET} just Metro/Haste/
|
|
155
|
+
${DIM}npm run clean --metro${RESET} just Metro/Haste/node-compile caches
|
|
156
156
|
${DIM}npm run clean --state${RESET} also wipe .setup-state.json
|
|
157
157
|
${DIM}npm run clean --no-install${RESET} wipe everything but skip reinstall
|
|
158
158
|
${DIM}npm run clean --help${RESET}
|
|
@@ -172,7 +172,7 @@ after install to rebuild the Convex bindings. Use when the lockfile
|
|
|
172
172
|
is suspect or you want a true clean-slate reinstall.
|
|
173
173
|
|
|
174
174
|
${BOLD}--state${RESET} additionally wipes .setup-state.json so the next
|
|
175
|
-
${DIM}
|
|
175
|
+
${DIM}npx vexpo full${RESET} re-probes every phase against external services
|
|
176
176
|
(slower, but the cure when state has drifted from reality).
|
|
177
177
|
|
|
178
178
|
Bundlers (Metro, expo CLI, react-native start, Watchman) are stopped
|
|
@@ -474,7 +474,7 @@ async function stepSetupState(): Promise<void> {
|
|
|
474
474
|
return;
|
|
475
475
|
}
|
|
476
476
|
await trashPaths([path]);
|
|
477
|
-
ok("trashed .setup-state.json (next `
|
|
477
|
+
ok("trashed .setup-state.json (next `npx vexpo full` re-probes every phase)");
|
|
478
478
|
}
|
|
479
479
|
|
|
480
480
|
async function stepInstall(pm: PM): Promise<void> {
|
|
@@ -76,7 +76,9 @@ console.log(`1. Commit ${CERT.replace(`${PROJECT}/`, "")} (it's a public cert).`
|
|
|
76
76
|
console.log(`2. Upload the private key to EAS as a file-type secret:`);
|
|
77
77
|
console.log(` eas env:create --environment production --visibility secret \\`);
|
|
78
78
|
console.log(` --type file --name EAS_UPDATE_PRIVATE_KEY --value ${KEY}`);
|
|
79
|
-
console.log(
|
|
79
|
+
console.log(
|
|
80
|
+
`3. Keep ${KEY} off committed surface. It lands in ../keys/, outside the repo, so git never sees it.`,
|
|
81
|
+
);
|
|
80
82
|
console.log(
|
|
81
83
|
`4. The next \`expo prebuild\` picks up the cert automatically. Run \`npm run prebuild\`.`,
|
|
82
84
|
);
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { Stack } from "expo-router";
|
|
1
|
+
import { Stack, router } from "expo-router";
|
|
2
2
|
import { useQuery } from "convex/react";
|
|
3
|
+
import { useEffect } from "react";
|
|
3
4
|
|
|
4
5
|
import { api } from "@/convex/_generated/api";
|
|
5
6
|
import { authClient } from "@/lib/auth-client";
|
|
6
7
|
import { useDeepLinkHandler } from "@/hooks/use-deep-link";
|
|
8
|
+
import { useOnboarding } from "@/hooks/use-onboarding";
|
|
7
9
|
import { useColors } from "@/hooks/use-theme";
|
|
8
10
|
import { useMotionScreenOptions } from "@/hooks/use-motion-screen-options";
|
|
9
11
|
import { useReducedMotion } from "@/hooks/use-reduced-motion";
|
|
@@ -28,6 +30,18 @@ export default function AppLayout() {
|
|
|
28
30
|
const me = useQuery(api.users.getMe, isAuthenticated ? {} : "skip");
|
|
29
31
|
const isAccountDeleted = !!me?.deletedAt;
|
|
30
32
|
|
|
33
|
+
// First-launch gate. `seen` reads SecureStore-backed localStorage
|
|
34
|
+
// synchronously, so there is no async flash. Wait for `me` to resolve
|
|
35
|
+
// (undefined while loading) before routing so a fresh authed user lands
|
|
36
|
+
// on welcome only once the account state is known. Welcome is registered
|
|
37
|
+
// inside the authed guard below, so this only fires for signed-in users.
|
|
38
|
+
const { seen } = useOnboarding();
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (isAuthenticated && me !== undefined && !isAccountDeleted && !seen) {
|
|
41
|
+
router.replace("/welcome");
|
|
42
|
+
}
|
|
43
|
+
}, [isAuthenticated, me, isAccountDeleted, seen]);
|
|
44
|
+
|
|
31
45
|
useDeepLinkHandler();
|
|
32
46
|
|
|
33
47
|
const colors = useColors();
|
|
@@ -53,6 +53,9 @@ export default function ForgotPasswordScreen() {
|
|
|
53
53
|
// mode, but a deeplinked navigation could still land here.
|
|
54
54
|
useEffect(() => {
|
|
55
55
|
if (providers !== undefined && providers.emailFeatures === false) {
|
|
56
|
+
announce(
|
|
57
|
+
"Password reset is unavailable until email verification is set up. Run npx vexpo full.",
|
|
58
|
+
);
|
|
56
59
|
router.replace("/auth/sign-in");
|
|
57
60
|
}
|
|
58
61
|
}, [providers]);
|
|
@@ -72,6 +72,9 @@ export default function ResetPasswordScreen() {
|
|
|
72
72
|
// in lite mode (`REQUIRE_EMAIL_VERIFICATION` unset).
|
|
73
73
|
useEffect(() => {
|
|
74
74
|
if (providers !== undefined && providers.emailFeatures === false) {
|
|
75
|
+
announce(
|
|
76
|
+
"Password reset is unavailable until email verification is set up. Run npx vexpo full.",
|
|
77
|
+
);
|
|
75
78
|
router.replace("/auth/sign-in");
|
|
76
79
|
}
|
|
77
80
|
}, [providers]);
|
|
@@ -315,7 +315,9 @@ export default function SignUpScreen() {
|
|
|
315
315
|
<Text
|
|
316
316
|
modifiers={[dfont({ size: 16 }), foregroundStyle(colors.mutedForeground as string)]}
|
|
317
317
|
>
|
|
318
|
-
|
|
318
|
+
{emailFeatures
|
|
319
|
+
? "A verification code will be sent to confirm your email."
|
|
320
|
+
: "Sign up and you're in. No email to confirm."}
|
|
319
321
|
</Text>
|
|
320
322
|
</VStack>
|
|
321
323
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type ComponentProps } from "react";
|
|
2
2
|
import { openSettings } from "expo-linking";
|
|
3
3
|
import {
|
|
4
4
|
Host,
|
|
@@ -27,13 +27,14 @@ import { useSymbolSize } from "@/lib/dynamic-symbol-size";
|
|
|
27
27
|
import { Button as ButtonTokens } from "@/constants/layout";
|
|
28
28
|
|
|
29
29
|
import { haptics } from "@/lib/haptics";
|
|
30
|
+
import { useShareAnalytics } from "@/lib/preferences";
|
|
30
31
|
import { useColors } from "@/hooks/use-theme";
|
|
31
32
|
|
|
32
33
|
export default function PrivacyScreen() {
|
|
33
34
|
const dfont = useDynamicFont();
|
|
34
35
|
const symbolSize = useSymbolSize();
|
|
35
36
|
const colors = useColors();
|
|
36
|
-
const [analyticsEnabled, setAnalyticsEnabled] =
|
|
37
|
+
const [analyticsEnabled, setAnalyticsEnabled] = useShareAnalytics();
|
|
37
38
|
|
|
38
39
|
const handleOpenSettings = () => {
|
|
39
40
|
haptics.light();
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { router } from "expo-router";
|
|
21
|
-
import { useActionState, useState } from "react";
|
|
21
|
+
import { startTransition, useActionState, useState } from "react";
|
|
22
22
|
import { Image as ExpoImage } from "expo-image";
|
|
23
23
|
import { Button, Host, Spacer, Text, VStack } from "@expo/ui/swift-ui";
|
|
24
24
|
import {
|
|
@@ -158,7 +158,7 @@ export default function RestoreAccountScreen() {
|
|
|
158
158
|
<ProminentButton
|
|
159
159
|
testID="restore-account-restore"
|
|
160
160
|
label={restorePending ? "Restoring…" : "Restore Account"}
|
|
161
|
-
onPress={() => restore()}
|
|
161
|
+
onPress={() => startTransition(() => restore())}
|
|
162
162
|
disabled={restorePending || signingOut}
|
|
163
163
|
/>
|
|
164
164
|
<Button
|
|
@@ -69,7 +69,7 @@ export default function SessionsScreen() {
|
|
|
69
69
|
const { data: current } = authClient.useSession();
|
|
70
70
|
const currentToken = current?.session?.token ?? null;
|
|
71
71
|
const [sessions, setSessions] = useState<SessionRow[] | null>(null);
|
|
72
|
-
const [loadError, setLoadError] = useState(
|
|
72
|
+
const [loadError, setLoadError] = useState<"network" | "stale" | null>(null);
|
|
73
73
|
const [revoking, setRevoking] = useState<string | null>(null);
|
|
74
74
|
const [confirmToken, setConfirmToken] = useState<string | null>(null);
|
|
75
75
|
|
|
@@ -77,10 +77,13 @@ export default function SessionsScreen() {
|
|
|
77
77
|
try {
|
|
78
78
|
const res = await authClient.listSessions();
|
|
79
79
|
if (res.error) {
|
|
80
|
-
|
|
80
|
+
// Better Auth freshness-gates session management (freshAge in
|
|
81
|
+
// convex/auth.ts). A stale session gets 403 SESSION_NOT_FRESH, which
|
|
82
|
+
// no retry can fix, only a fresh sign-in can.
|
|
83
|
+
setLoadError(res.error.code === "SESSION_NOT_FRESH" ? "stale" : "network");
|
|
81
84
|
return;
|
|
82
85
|
}
|
|
83
|
-
setLoadError(
|
|
86
|
+
setLoadError(null);
|
|
84
87
|
const rows = (res.data ?? []).map((s) => ({
|
|
85
88
|
id: s.id,
|
|
86
89
|
token: s.token,
|
|
@@ -91,7 +94,7 @@ export default function SessionsScreen() {
|
|
|
91
94
|
}));
|
|
92
95
|
setSessions(rows);
|
|
93
96
|
} catch {
|
|
94
|
-
setLoadError(
|
|
97
|
+
setLoadError("network");
|
|
95
98
|
}
|
|
96
99
|
};
|
|
97
100
|
|
|
@@ -124,7 +127,14 @@ export default function SessionsScreen() {
|
|
|
124
127
|
return (
|
|
125
128
|
<Host testID="sessions-screen" style={{ flex: 1, backgroundColor: colors.background }}>
|
|
126
129
|
{sessions === null ? (
|
|
127
|
-
loadError ? (
|
|
130
|
+
loadError === "stale" ? (
|
|
131
|
+
<ContentUnavailable
|
|
132
|
+
testID="sessions-stale"
|
|
133
|
+
title="Sign in again to manage sessions"
|
|
134
|
+
systemImage="lock.shield"
|
|
135
|
+
description="For your security, managing sessions needs a recent sign-in. Sign out, sign back in, and come back here."
|
|
136
|
+
/>
|
|
137
|
+
) : loadError ? (
|
|
128
138
|
<ContentUnavailable
|
|
129
139
|
testID="sessions-error"
|
|
130
140
|
title="Couldn't load sessions"
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { ConvexError } from "convex/values";
|
|
2
2
|
|
|
3
|
-
import { ErrorText } from "./status-text";
|
|
4
|
-
|
|
5
3
|
export function formatError(err: unknown): string {
|
|
6
4
|
if (err instanceof ConvexError) {
|
|
7
5
|
const data = err.data as unknown;
|
|
@@ -14,8 +12,3 @@ export function formatError(err: unknown): string {
|
|
|
14
12
|
if (err instanceof Error) return err.message;
|
|
15
13
|
return "An unexpected error occurred";
|
|
16
14
|
}
|
|
17
|
-
|
|
18
|
-
export function ConvexErrorView({ error, testID }: { error: unknown; testID?: string }) {
|
|
19
|
-
if (error === undefined || error === null) return null;
|
|
20
|
-
return <ErrorText testID={testID}>{formatError(error)}</ErrorText>;
|
|
21
|
-
}
|
|
@@ -5,17 +5,6 @@ export const Opacity = {
|
|
|
5
5
|
muted: 0.6,
|
|
6
6
|
} as const;
|
|
7
7
|
|
|
8
|
-
export const Material = {
|
|
9
|
-
ultraThin: 20,
|
|
10
|
-
thin: 40,
|
|
11
|
-
regular: 60,
|
|
12
|
-
thick: 80,
|
|
13
|
-
ultraThick: 95,
|
|
14
|
-
bar: 50,
|
|
15
|
-
} as const;
|
|
16
|
-
|
|
17
|
-
export type MaterialLevel = keyof typeof Material;
|
|
18
|
-
|
|
19
8
|
export const Shadow = {
|
|
20
9
|
sm: "0 1px 2px",
|
|
21
10
|
md: "0 2px 4px",
|
|
@@ -9,6 +9,11 @@ import { checkForUpdate } from "@/lib/updates";
|
|
|
9
9
|
import { setTheme } from "@/hooks/use-theme";
|
|
10
10
|
import { reloadApp } from "./app";
|
|
11
11
|
|
|
12
|
+
// Mirror how `auth-client.ts` resolves the scheme so the secure-storage
|
|
13
|
+
// keys we clear match the ones `@better-auth/expo` actually wrote.
|
|
14
|
+
const rawScheme = Constants.expoConfig?.scheme;
|
|
15
|
+
const scheme = Array.isArray(rawScheme) ? rawScheme[0] : rawScheme;
|
|
16
|
+
|
|
12
17
|
type SessionResponse = { data?: { session?: { id?: string } } | null };
|
|
13
18
|
|
|
14
19
|
async function copyAuthSessionId() {
|
|
@@ -50,8 +55,12 @@ export function registerDevMenuItems() {
|
|
|
50
55
|
{
|
|
51
56
|
name: "Clear Secure Storage",
|
|
52
57
|
callback: () => {
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
// `@better-auth/expo` keys its SecureStore entries off `storagePrefix`,
|
|
59
|
+
// which `auth-client.ts` sets to the app scheme (falling back to
|
|
60
|
+
// "better-auth"). It writes `<prefix>_cookie` and `<prefix>_session_data`.
|
|
61
|
+
const prefix = scheme ?? "better-auth";
|
|
62
|
+
SecureStore.deleteItemAsync(`${prefix}_cookie`).catch(() => {});
|
|
63
|
+
SecureStore.deleteItemAsync(`${prefix}_session_data`).catch(() => {});
|
|
55
64
|
console.log("[DevMenu] Secure storage cleared");
|
|
56
65
|
},
|
|
57
66
|
},
|
|
@@ -7,6 +7,7 @@ export type ReduceMotionPref = "system" | "always" | "never";
|
|
|
7
7
|
const hapticsStore = createStorage<boolean>("pref.hapticsEnabled", true);
|
|
8
8
|
const reduceMotionStore = createStorage<ReduceMotionPref>("pref.reduceMotion", "system");
|
|
9
9
|
const debugEnabledStore = createStorage<boolean>("pref.debugEnabled", __DEV__);
|
|
10
|
+
const analyticsStore = createStorage<boolean>("pref.shareAnalytics", true);
|
|
10
11
|
|
|
11
12
|
export const preferences = {
|
|
12
13
|
hapticsEnabled: () => hapticsStore.get(),
|
|
@@ -17,6 +18,9 @@ export const preferences = {
|
|
|
17
18
|
|
|
18
19
|
debugEnabled: () => debugEnabledStore.get(),
|
|
19
20
|
setDebugEnabled: (v: boolean) => debugEnabledStore.set(v),
|
|
21
|
+
|
|
22
|
+
shareAnalytics: () => analyticsStore.get(),
|
|
23
|
+
setShareAnalytics: (v: boolean) => analyticsStore.set(v),
|
|
20
24
|
};
|
|
21
25
|
|
|
22
26
|
export function useHapticsEnabled(): [boolean, (v: boolean) => void] {
|
|
@@ -41,3 +45,8 @@ export function useDebugEnabled(): [boolean, (v: boolean) => void] {
|
|
|
41
45
|
);
|
|
42
46
|
return [v, debugEnabledStore.set];
|
|
43
47
|
}
|
|
48
|
+
|
|
49
|
+
export function useShareAnalytics(): [boolean, (v: boolean) => void] {
|
|
50
|
+
const v = useSyncExternalStore(analyticsStore.subscribe, analyticsStore.get, analyticsStore.get);
|
|
51
|
+
return [v, analyticsStore.set];
|
|
52
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ramonclaudio/create-vexpo",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
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",
|
|
@@ -48,7 +48,8 @@
|
|
|
48
48
|
"dev": "tsup --watch",
|
|
49
49
|
"prepublishOnly": "npm run build",
|
|
50
50
|
"typecheck": "tsc --noEmit",
|
|
51
|
-
"test": "echo '(create-vexpo: no
|
|
51
|
+
"test": "echo '(create-vexpo: no unit tests; see test:e2e)'",
|
|
52
|
+
"test:e2e": "npm run build && __tests__/e2e/run.sh"
|
|
52
53
|
},
|
|
53
54
|
"dependencies": {
|
|
54
55
|
"commander": "^15.0.0",
|