@ramonclaudio/create-vexpo 0.1.0
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 +50 -0
- package/dist/index.js +183 -0
- package/dist/templates/default/.eas/workflows/asc-events.yml +84 -0
- package/dist/templates/default/.eas/workflows/deploy-production.yml +129 -0
- package/dist/templates/default/.eas/workflows/development-builds.yml +19 -0
- package/dist/templates/default/.eas/workflows/e2e-tests.yml +42 -0
- package/dist/templates/default/.eas/workflows/pr-preview.yml +98 -0
- package/dist/templates/default/.eas/workflows/release.yml +44 -0
- package/dist/templates/default/.eas/workflows/rollback.yml +86 -0
- package/dist/templates/default/.eas/workflows/rollout.yml +84 -0
- package/dist/templates/default/.eas/workflows/rotate-apple-jwt.yml +42 -0
- package/dist/templates/default/.eas/workflows/testflight.yml +57 -0
- package/dist/templates/default/.github/workflows/check.yml +28 -0
- package/dist/templates/default/.maestro/launch.yaml +18 -0
- package/dist/templates/default/AGENTS.md +79 -0
- package/dist/templates/default/DESIGN.md +331 -0
- package/dist/templates/default/LICENSE +21 -0
- package/dist/templates/default/README.md +153 -0
- package/dist/templates/default/SETUP.md +618 -0
- package/dist/templates/default/__tests__/convex/constants.test.ts +49 -0
- package/dist/templates/default/__tests__/convex/validators.test.ts +23 -0
- package/dist/templates/default/__tests__/convex/webhook.test.ts +343 -0
- package/dist/templates/default/__tests__/lib/deep-link.test.ts +67 -0
- package/dist/templates/default/_easignore +22 -0
- package/dist/templates/default/_editorconfig +9 -0
- package/dist/templates/default/_env.example +34 -0
- package/dist/templates/default/_fingerprintignore +24 -0
- package/dist/templates/default/_gitattributes +7 -0
- package/dist/templates/default/_gitignore +69 -0
- package/dist/templates/default/_oxfmtrc.json +3 -0
- package/dist/templates/default/_oxlintrc.json +34 -0
- package/dist/templates/default/app/(app)/(tabs)/(home)/index.tsx +50 -0
- package/dist/templates/default/app/(app)/(tabs)/(home,search)/_layout.tsx +44 -0
- package/dist/templates/default/app/(app)/(tabs)/(search)/index.tsx +247 -0
- package/dist/templates/default/app/(app)/(tabs)/_layout.tsx +77 -0
- package/dist/templates/default/app/(app)/(tabs)/settings/_layout.tsx +37 -0
- package/dist/templates/default/app/(app)/(tabs)/settings/index.tsx +362 -0
- package/dist/templates/default/app/(app)/(tabs)/settings/preferences.tsx +184 -0
- package/dist/templates/default/app/(app)/_layout.tsx +73 -0
- package/dist/templates/default/app/(app)/debug.tsx +389 -0
- package/dist/templates/default/app/(app)/help.tsx +254 -0
- package/dist/templates/default/app/(app)/linked.tsx +116 -0
- package/dist/templates/default/app/(app)/privacy.tsx +159 -0
- package/dist/templates/default/app/(app)/profile.tsx +915 -0
- package/dist/templates/default/app/(app)/sessions.tsx +191 -0
- package/dist/templates/default/app/(app)/welcome.tsx +140 -0
- package/dist/templates/default/app/(auth)/_layout.tsx +31 -0
- package/dist/templates/default/app/(auth)/forgot-password.tsx +168 -0
- package/dist/templates/default/app/(auth)/reset-password.tsx +314 -0
- package/dist/templates/default/app/(auth)/sign-in.tsx +453 -0
- package/dist/templates/default/app/(auth)/sign-up.tsx +563 -0
- package/dist/templates/default/app/+native-intent.tsx +14 -0
- package/dist/templates/default/app/+not-found.tsx +51 -0
- package/dist/templates/default/app/_layout.tsx +102 -0
- package/dist/templates/default/app-store/screenshots/.gitkeep +0 -0
- package/dist/templates/default/app-store/screenshots/README.md +13 -0
- package/dist/templates/default/app.config.ts +201 -0
- package/dist/templates/default/app.json +11 -0
- package/dist/templates/default/assets/brand-icon-dark.png +0 -0
- package/dist/templates/default/assets/brand-icon-light.png +0 -0
- package/dist/templates/default/assets/fonts/Geist-Black.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-BlackItalic.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-Bold.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-BoldItalic.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-ExtraBold.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-ExtraBoldItalic.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-ExtraLight.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-ExtraLightItalic.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-Italic.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-Light.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-LightItalic.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-Medium.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-MediumItalic.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-Regular.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-SemiBold.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-SemiBoldItalic.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-Thin.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-ThinItalic.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-Variable-Italic.ttf +0 -0
- package/dist/templates/default/assets/fonts/Geist-Variable.ttf +0 -0
- package/dist/templates/default/assets/fonts/GeistMono-Bold.ttf +0 -0
- package/dist/templates/default/assets/fonts/GeistMono-BoldItalic.ttf +0 -0
- package/dist/templates/default/assets/fonts/GeistMono-Italic.ttf +0 -0
- package/dist/templates/default/assets/fonts/GeistMono-Medium.ttf +0 -0
- package/dist/templates/default/assets/fonts/GeistMono-MediumItalic.ttf +0 -0
- package/dist/templates/default/assets/fonts/GeistMono-Regular.ttf +0 -0
- package/dist/templates/default/assets/fonts/GeistPixel-Square.ttf +0 -0
- package/dist/templates/default/assets/icon.png +0 -0
- package/dist/templates/default/assets/sounds/notification.wav +0 -0
- package/dist/templates/default/assets/splash-image-dark.png +0 -0
- package/dist/templates/default/assets/splash-image-light.png +0 -0
- package/dist/templates/default/bun.lock +1860 -0
- package/dist/templates/default/components/auth/otp-verification.tsx +255 -0
- package/dist/templates/default/components/auth/password-field.tsx +121 -0
- package/dist/templates/default/components/auth/segmented-toggle.tsx +47 -0
- package/dist/templates/default/components/ui/convex-error.tsx +32 -0
- package/dist/templates/default/components/ui/error-boundary.tsx +57 -0
- package/dist/templates/default/components/ui/loading-screen.tsx +31 -0
- package/dist/templates/default/components/ui/material.tsx +94 -0
- package/dist/templates/default/components/ui/offline-banner.tsx +58 -0
- package/dist/templates/default/components/ui/prominent-button.tsx +71 -0
- package/dist/templates/default/components/ui/skeleton.tsx +107 -0
- package/dist/templates/default/components/ui/status-text.tsx +49 -0
- package/dist/templates/default/components/ui/update-banner.tsx +82 -0
- package/dist/templates/default/constants/layout.ts +102 -0
- package/dist/templates/default/constants/theme.ts +401 -0
- package/dist/templates/default/constants/ui.ts +77 -0
- package/dist/templates/default/convex/_generated/api.d.ts +77 -0
- package/dist/templates/default/convex/_generated/api.js +23 -0
- package/dist/templates/default/convex/_generated/dataModel.d.ts +60 -0
- package/dist/templates/default/convex/_generated/server.d.ts +143 -0
- package/dist/templates/default/convex/_generated/server.js +93 -0
- package/dist/templates/default/convex/admin.ts +102 -0
- package/dist/templates/default/convex/auth.config.ts +6 -0
- package/dist/templates/default/convex/auth.ts +335 -0
- package/dist/templates/default/convex/constants.ts +46 -0
- package/dist/templates/default/convex/convex.config.ts +11 -0
- package/dist/templates/default/convex/crons.ts +42 -0
- package/dist/templates/default/convex/email.ts +109 -0
- package/dist/templates/default/convex/env.ts +31 -0
- package/dist/templates/default/convex/errors.ts +33 -0
- package/dist/templates/default/convex/functions.ts +54 -0
- package/dist/templates/default/convex/http.ts +176 -0
- package/dist/templates/default/convex/log.ts +81 -0
- package/dist/templates/default/convex/pushTokens.ts +114 -0
- package/dist/templates/default/convex/rateLimit.ts +92 -0
- package/dist/templates/default/convex/schema.ts +28 -0
- package/dist/templates/default/convex/tsconfig.json +18 -0
- package/dist/templates/default/convex/users.ts +279 -0
- package/dist/templates/default/convex/validators.ts +74 -0
- package/dist/templates/default/convex/webhook.ts +193 -0
- package/dist/templates/default/convex.json +6 -0
- package/dist/templates/default/eas.json +56 -0
- package/dist/templates/default/fingerprint.config.js +9 -0
- package/dist/templates/default/hooks/use-debounce.ts +20 -0
- package/dist/templates/default/hooks/use-deep-link.ts +43 -0
- package/dist/templates/default/hooks/use-navigation-tracking.ts +15 -0
- package/dist/templates/default/hooks/use-network.ts +11 -0
- package/dist/templates/default/hooks/use-notifications.ts +107 -0
- package/dist/templates/default/hooks/use-onboarding.ts +15 -0
- package/dist/templates/default/hooks/use-reduced-motion.ts +11 -0
- package/dist/templates/default/hooks/use-theme.ts +53 -0
- package/dist/templates/default/hooks/use-updates.ts +86 -0
- package/dist/templates/default/lib/a11y.ts +5 -0
- package/dist/templates/default/lib/app.ts +14 -0
- package/dist/templates/default/lib/assets.ts +17 -0
- package/dist/templates/default/lib/auth-client.ts +21 -0
- package/dist/templates/default/lib/convex-auth.tsx +79 -0
- package/dist/templates/default/lib/deep-link.ts +71 -0
- package/dist/templates/default/lib/dev-menu.ts +119 -0
- package/dist/templates/default/lib/device.ts +40 -0
- package/dist/templates/default/lib/dynamic-font.ts +49 -0
- package/dist/templates/default/lib/env.ts +10 -0
- package/dist/templates/default/lib/haptics.ts +24 -0
- package/dist/templates/default/lib/notifications.ts +276 -0
- package/dist/templates/default/lib/preferences.ts +45 -0
- package/dist/templates/default/lib/schemas.ts +137 -0
- package/dist/templates/default/lib/storage.ts +47 -0
- package/dist/templates/default/lib/updates.ts +107 -0
- package/dist/templates/default/metro.config.js +14 -0
- package/dist/templates/default/package.json +129 -0
- package/dist/templates/default/patches/PR-368.patch +91 -0
- package/dist/templates/default/patches/convex-dev-better-auth-0.12.2.tgz +0 -0
- package/dist/templates/default/plugins/README.md +9 -0
- package/dist/templates/default/plugins/with-auto-signing.js +45 -0
- package/dist/templates/default/plugins/with-pod-deployment-target.js +35 -0
- package/dist/templates/default/scripts/README.md +36 -0
- package/dist/templates/default/scripts/_run.mjs +77 -0
- package/dist/templates/default/scripts/clean.ts +543 -0
- package/dist/templates/default/scripts/rotate-apple-jwt.mjs +80 -0
- package/dist/templates/default/store.config.json +58 -0
- package/dist/templates/default/tsconfig.json +13 -0
- package/dist/templates/default/vitest.config.ts +21 -0
- package/package.json +69 -0
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# create-vexpo
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@ramonclaudio/create-vexpo)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
Scaffold a new [vexpo](https://github.com/ramonclaudio/vexpo) project: Expo SDK 56 + Convex + Better Auth + Resend, wired end-to-end for iOS. Real auth (email + password, email OTP, Apple Sign In), APNs push, OTA updates, App Store submission. Strict TypeScript, native SwiftUI via `@expo/ui/swift-ui`, no NativeWind.
|
|
7
|
+
|
|
8
|
+
## Usage
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
npm create @ramonclaudio/vexpo@latest my-app
|
|
12
|
+
# or
|
|
13
|
+
bunx @ramonclaudio/create-vexpo@latest my-app
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
After scaffold:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
cd my-app
|
|
20
|
+
|
|
21
|
+
bunx vexpo lite # 60 seconds: Convex + Better Auth, simulator-ready
|
|
22
|
+
bunx vexpo lite --new # same + Convex signup walkthrough for first-time users
|
|
23
|
+
bunx vexpo full # full provisioning: TestFlight-ready (Convex, Better Auth, Resend, Apple Sign In, EAS, rebrand)
|
|
24
|
+
bunx vexpo full --new # same + Apple/Convex/Expo/Resend signup walkthrough
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`bunx vexpo lite` is the dev-mode shortcut. No Apple Developer account, no domain, no EAS, no Resend. Boots in the iOS Simulator in ~60 seconds. Add `--new` if you don't have a Convex account yet.
|
|
28
|
+
|
|
29
|
+
`bunx vexpo full` walks Apple Developer / Expo / Convex / Resend signups (with `--new`), validates each, provisions everything in order. ~30 minutes hands-on plus Apple-side wait times. Prints the canonical `eas build` command at the end. You run it when ready.
|
|
30
|
+
|
|
31
|
+
## Options
|
|
32
|
+
|
|
33
|
+
| Flag | Behavior |
|
|
34
|
+
| --------------- | --------------------------------------------------------------------------------------------- |
|
|
35
|
+
| `[directory]` | Project directory name (positional). Defaults to `my-vexpo-app` with `-y`, otherwise prompts. |
|
|
36
|
+
| `--no-install` | Skip running `<pm> install` after copying the template. |
|
|
37
|
+
| `--no-git` | Skip `git init` after install. |
|
|
38
|
+
| `--no-setup` | Skip the post-install `bunx vexpo lite` / `bunx vexpo full` prompt. |
|
|
39
|
+
| `-y, --yes` | Accept defaults, skip prompts. |
|
|
40
|
+
| `-v, --version` | Print version, exit. |
|
|
41
|
+
|
|
42
|
+
## What gets scaffolded
|
|
43
|
+
|
|
44
|
+
The CLI copies `templates/default/` from the published tarball, restores npm-stripped dotfiles (`.gitignore`, `.env.example`, etc.), rewrites `package.json` (project name, version, drops monorepo metadata, swaps `vexpo` workspace ref for the published version), runs `bun install`, and initializes a fresh git repo with `feat: initial commit`.
|
|
45
|
+
|
|
46
|
+
After that, the project is standalone. The operational CLI (`vexpo`) is installed as a devDependency, so `bunx vexpo <subcommand>` resolves to the local pinned version. Setup commands aren't in `package.json`. They're one-shot CLI invocations, not runtime scripts.
|
|
47
|
+
|
|
48
|
+
## Repo
|
|
49
|
+
|
|
50
|
+
[github.com/ramonclaudio/vexpo](https://github.com/ramonclaudio/vexpo)
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { cp, rename, readFile, writeFile } from 'fs/promises';
|
|
4
|
+
import { dirname, join, resolve, relative, basename } from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { Command } from 'commander';
|
|
7
|
+
import { execa } from 'execa';
|
|
8
|
+
import kleur from 'kleur';
|
|
9
|
+
import ora from 'ora';
|
|
10
|
+
import prompts from 'prompts';
|
|
11
|
+
|
|
12
|
+
// package.json
|
|
13
|
+
var package_default = {
|
|
14
|
+
version: "0.1.0"};
|
|
15
|
+
|
|
16
|
+
// src/index.ts
|
|
17
|
+
var here = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
var TEMPLATE_DIR = join(here, "templates", "default");
|
|
19
|
+
async function main() {
|
|
20
|
+
const program = new Command().name("create-vexpo").description(
|
|
21
|
+
"Scaffold a new vexpo project. Expo SDK 56 + Convex + Better Auth + Resend, wired for iOS."
|
|
22
|
+
).argument("[directory]", "project directory name").option("--no-install", "skip installing dependencies").option("--no-git", "skip git init").option("--no-setup", "skip the `bunx vexpo lite` / `bunx vexpo full` prompt after install").option("-y, --yes", "accept defaults, skip prompts").version(package_default.version, "-v, --version").parse();
|
|
23
|
+
const flags = program.opts();
|
|
24
|
+
const argDir = program.args[0];
|
|
25
|
+
intro();
|
|
26
|
+
const name = await resolveName(argDir, flags.yes);
|
|
27
|
+
const target = resolve(process.cwd(), name);
|
|
28
|
+
if (existsSync(target)) {
|
|
29
|
+
console.error(kleur.red(`
|
|
30
|
+
Target ${target} already exists. Pick a different name.`));
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
const pm = detectPackageManager();
|
|
34
|
+
const copySpin = ora(
|
|
35
|
+
`Copying template to ${kleur.cyan(relative(process.cwd(), target) || ".")}`
|
|
36
|
+
).start();
|
|
37
|
+
try {
|
|
38
|
+
await cp(TEMPLATE_DIR, target, { recursive: true });
|
|
39
|
+
await restoreStrippedDotfiles(target);
|
|
40
|
+
await rewritePackage(target, name);
|
|
41
|
+
copySpin.succeed("Template copied");
|
|
42
|
+
} catch (err) {
|
|
43
|
+
copySpin.fail("Template copy failed");
|
|
44
|
+
throw err;
|
|
45
|
+
}
|
|
46
|
+
if (flags.install) {
|
|
47
|
+
const installSpin = ora(`Installing dependencies with ${kleur.cyan(pm)}`).start();
|
|
48
|
+
try {
|
|
49
|
+
await execa(pm, ["install"], { cwd: target, stdio: "ignore" });
|
|
50
|
+
installSpin.succeed(`Installed with ${pm}`);
|
|
51
|
+
} catch {
|
|
52
|
+
installSpin.warn(`Install skipped. Run ${kleur.cyan(`${pm} install`)} manually.`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (flags.git) {
|
|
56
|
+
const gitSpin = ora("Initializing git").start();
|
|
57
|
+
try {
|
|
58
|
+
await execa("git", ["init", "--initial-branch=main"], { cwd: target, stdio: "ignore" });
|
|
59
|
+
await execa("git", ["add", "-A"], { cwd: target, stdio: "ignore" });
|
|
60
|
+
await execa("git", ["commit", "-m", "feat: initial commit", "--no-gpg-sign"], {
|
|
61
|
+
cwd: target,
|
|
62
|
+
stdio: "ignore"
|
|
63
|
+
});
|
|
64
|
+
gitSpin.succeed("Git repo initialized");
|
|
65
|
+
} catch {
|
|
66
|
+
gitSpin.warn("Git init skipped");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
nextSteps(target, flags, pm);
|
|
70
|
+
}
|
|
71
|
+
var NAME_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
72
|
+
var NAME_HINT = "lowercase letters, numbers, dashes; must start alphanumeric";
|
|
73
|
+
function validateNameSegment(target) {
|
|
74
|
+
const segment = basename(target);
|
|
75
|
+
if (!NAME_RE.test(segment)) return { ok: false, reason: NAME_HINT };
|
|
76
|
+
return { ok: true };
|
|
77
|
+
}
|
|
78
|
+
async function resolveName(argDir, yes) {
|
|
79
|
+
if (argDir) {
|
|
80
|
+
const check = validateNameSegment(argDir);
|
|
81
|
+
if (!check.ok) {
|
|
82
|
+
console.error(kleur.red(`
|
|
83
|
+
Invalid project directory '${argDir}'. ${check.reason}.`));
|
|
84
|
+
console.error(
|
|
85
|
+
kleur.gray(`Examples: my-app, my-cool-app, project1. Avoid spaces, unicode, npm scopes.`)
|
|
86
|
+
);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
return argDir;
|
|
90
|
+
}
|
|
91
|
+
if (yes) return "my-vexpo-app";
|
|
92
|
+
const res = await prompts(
|
|
93
|
+
{
|
|
94
|
+
type: "text",
|
|
95
|
+
name: "name",
|
|
96
|
+
message: "Project directory",
|
|
97
|
+
initial: "my-vexpo-app",
|
|
98
|
+
validate: (v) => {
|
|
99
|
+
const check = validateNameSegment(v);
|
|
100
|
+
return check.ok ? true : check.reason;
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
{ onCancel: () => process.exit(1) }
|
|
104
|
+
);
|
|
105
|
+
return res.name;
|
|
106
|
+
}
|
|
107
|
+
async function restoreStrippedDotfiles(target) {
|
|
108
|
+
const renames = [
|
|
109
|
+
["_gitignore", ".gitignore"],
|
|
110
|
+
["_env.example", ".env.example"],
|
|
111
|
+
["_oxfmtrc.json", ".oxfmtrc.json"],
|
|
112
|
+
["_oxlintrc.json", ".oxlintrc.json"],
|
|
113
|
+
["_editorconfig", ".editorconfig"],
|
|
114
|
+
["_gitattributes", ".gitattributes"],
|
|
115
|
+
["_easignore", ".easignore"],
|
|
116
|
+
["_fingerprintignore", ".fingerprintignore"],
|
|
117
|
+
["_env.convex.local", ".env.convex.local"]
|
|
118
|
+
];
|
|
119
|
+
for (const [from, to] of renames) {
|
|
120
|
+
const src = join(target, from);
|
|
121
|
+
if (existsSync(src)) await rename(src, join(target, to));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async function rewritePackage(target, requestedName) {
|
|
125
|
+
const pkgPath = join(target, "package.json");
|
|
126
|
+
const raw = await readFile(pkgPath, "utf8");
|
|
127
|
+
const parsed = JSON.parse(raw);
|
|
128
|
+
parsed.name = toPackageName(requestedName);
|
|
129
|
+
parsed.version = "0.0.0";
|
|
130
|
+
parsed.private = true;
|
|
131
|
+
delete parsed.author;
|
|
132
|
+
delete parsed.repository;
|
|
133
|
+
delete parsed.bugs;
|
|
134
|
+
delete parsed.homepage;
|
|
135
|
+
delete parsed.license;
|
|
136
|
+
delete parsed.publishConfig;
|
|
137
|
+
await writeFile(pkgPath, `${JSON.stringify(parsed, null, 2)}
|
|
138
|
+
`);
|
|
139
|
+
}
|
|
140
|
+
function toPackageName(raw) {
|
|
141
|
+
const name = basename(raw).toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/^-+|-+$/g, "");
|
|
142
|
+
return name || "my-vexpo-app";
|
|
143
|
+
}
|
|
144
|
+
function detectPackageManager() {
|
|
145
|
+
const ua = process.env.npm_config_user_agent ?? "";
|
|
146
|
+
if (ua.startsWith("bun")) return "bun";
|
|
147
|
+
if (ua.startsWith("pnpm")) return "pnpm";
|
|
148
|
+
if (ua.startsWith("yarn")) return "yarn";
|
|
149
|
+
if (ua.startsWith("npm")) return "npm";
|
|
150
|
+
return "bun";
|
|
151
|
+
}
|
|
152
|
+
function intro() {
|
|
153
|
+
console.log();
|
|
154
|
+
console.log(kleur.bold().cyan("create-vexpo") + kleur.gray(` v${package_default.version}`));
|
|
155
|
+
}
|
|
156
|
+
function nextSteps(target, flags, pm) {
|
|
157
|
+
const cdPath = relative(process.cwd(), target) || ".";
|
|
158
|
+
console.log();
|
|
159
|
+
console.log(kleur.bold("Next steps:"));
|
|
160
|
+
console.log(kleur.gray(" cd ") + kleur.cyan(cdPath));
|
|
161
|
+
if (!flags.install) console.log(kleur.gray(` ${pm} install`));
|
|
162
|
+
console.log(
|
|
163
|
+
kleur.gray(
|
|
164
|
+
` bunx vexpo lite ${kleur.dim("# dev mode: Convex + Better Auth, 60s to simulator")}`
|
|
165
|
+
)
|
|
166
|
+
);
|
|
167
|
+
console.log(
|
|
168
|
+
kleur.gray(
|
|
169
|
+
` bunx vexpo full ${kleur.dim("# real setup: TestFlight-ready (add --new if you're new)")}`
|
|
170
|
+
)
|
|
171
|
+
);
|
|
172
|
+
console.log();
|
|
173
|
+
console.log(kleur.bold("Then in two terminals:"));
|
|
174
|
+
console.log(kleur.gray(` ${pm} run convex:dev ${kleur.dim("# terminal 1")}`));
|
|
175
|
+
console.log(kleur.gray(` ${pm} run ios ${kleur.dim("# terminal 2")}`));
|
|
176
|
+
console.log();
|
|
177
|
+
console.log(kleur.gray("Docs: ") + kleur.cyan("https://github.com/ramonclaudio/vexpo"));
|
|
178
|
+
console.log();
|
|
179
|
+
}
|
|
180
|
+
main().catch((err) => {
|
|
181
|
+
console.error(err);
|
|
182
|
+
process.exit(1);
|
|
183
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
name: App Store Connect events
|
|
2
|
+
|
|
3
|
+
# Reacts to App Store Connect events using the `on.app_store_connect` trigger
|
|
4
|
+
# (https://docs.expo.dev/eas/workflows/syntax/#onapp_store_connect). EAS
|
|
5
|
+
# proxies ASC webhooks so we don't have to set up our own listener.
|
|
6
|
+
#
|
|
7
|
+
# Setup:
|
|
8
|
+
# 1. Open EAS dashboard → Project settings → General → Connections
|
|
9
|
+
# 2. Connect your App Store Connect app
|
|
10
|
+
# 3. Paste your Slack incoming-webhook URL into each `webhook_url:` field
|
|
11
|
+
# below (or strip the slack jobs entirely if you don't use Slack).
|
|
12
|
+
|
|
13
|
+
on:
|
|
14
|
+
app_store_connect:
|
|
15
|
+
app_version:
|
|
16
|
+
states:
|
|
17
|
+
- waiting_for_review
|
|
18
|
+
- in_review
|
|
19
|
+
- rejected
|
|
20
|
+
- accepted
|
|
21
|
+
- ready_for_distribution
|
|
22
|
+
build_upload:
|
|
23
|
+
states:
|
|
24
|
+
- complete
|
|
25
|
+
- failed
|
|
26
|
+
external_beta:
|
|
27
|
+
states:
|
|
28
|
+
- ready_for_beta_testing
|
|
29
|
+
- beta_approved
|
|
30
|
+
- beta_rejected
|
|
31
|
+
beta_feedback:
|
|
32
|
+
types:
|
|
33
|
+
- crash
|
|
34
|
+
|
|
35
|
+
jobs:
|
|
36
|
+
# NOTE: EAS's `app_store_connect.*` workflow context only documents
|
|
37
|
+
# `app.id` and `build_upload.id`. Other event-specific properties (state
|
|
38
|
+
# values, beta-feedback details) aren't in the public context shape today.
|
|
39
|
+
# The `if:` guards below check whether the corresponding event key exists,
|
|
40
|
+
# so each notify only fires on the matching trigger. The message bodies
|
|
41
|
+
# link straight to App Store Connect rather than interpolating fields that
|
|
42
|
+
# may not resolve.
|
|
43
|
+
|
|
44
|
+
notify_review_state:
|
|
45
|
+
name: Slack notify on review state change
|
|
46
|
+
if: ${{ app_store_connect.app_version }}
|
|
47
|
+
type: slack
|
|
48
|
+
params:
|
|
49
|
+
# `webhook_url` only accepts hardcoded strings today; EAS env-stored
|
|
50
|
+
# interpolation for this field is upcoming. Paste your URL below.
|
|
51
|
+
webhook_url: https://hooks.slack.com/services/REPLACE/ME/HERE
|
|
52
|
+
message: |
|
|
53
|
+
App Store version state changed. App Store Connect → My Apps → App Store tab.
|
|
54
|
+
Workflow: ${{ workflow.url }}
|
|
55
|
+
|
|
56
|
+
notify_upload_state:
|
|
57
|
+
name: Slack notify on build upload
|
|
58
|
+
if: ${{ app_store_connect.build_upload }}
|
|
59
|
+
type: slack
|
|
60
|
+
params:
|
|
61
|
+
webhook_url: https://hooks.slack.com/services/REPLACE/ME/HERE
|
|
62
|
+
message: |
|
|
63
|
+
Build upload completed: ${{ app_store_connect.build_upload.id }}.
|
|
64
|
+
Workflow: ${{ workflow.url }}
|
|
65
|
+
|
|
66
|
+
notify_beta_state:
|
|
67
|
+
name: Slack notify on external beta state
|
|
68
|
+
if: ${{ app_store_connect.external_beta }}
|
|
69
|
+
type: slack
|
|
70
|
+
params:
|
|
71
|
+
webhook_url: https://hooks.slack.com/services/REPLACE/ME/HERE
|
|
72
|
+
message: |
|
|
73
|
+
External beta state changed. App Store Connect → TestFlight.
|
|
74
|
+
Workflow: ${{ workflow.url }}
|
|
75
|
+
|
|
76
|
+
notify_crash:
|
|
77
|
+
name: Slack notify on TestFlight crash
|
|
78
|
+
if: ${{ app_store_connect.beta_feedback }}
|
|
79
|
+
type: slack
|
|
80
|
+
params:
|
|
81
|
+
webhook_url: https://hooks.slack.com/services/REPLACE/ME/HERE
|
|
82
|
+
message: |
|
|
83
|
+
🔴 TestFlight crash reported. App Store Connect → TestFlight → Feedback.
|
|
84
|
+
Workflow: ${{ workflow.url }}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
name: Deploy to production
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: ["main"]
|
|
6
|
+
workflow_dispatch: {}
|
|
7
|
+
|
|
8
|
+
concurrency:
|
|
9
|
+
cancel_in_progress: true
|
|
10
|
+
group: ${{ workflow.filename }}-${{ github.ref }}
|
|
11
|
+
|
|
12
|
+
defaults:
|
|
13
|
+
tools:
|
|
14
|
+
bun: latest
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
precheck:
|
|
18
|
+
name: Precheck
|
|
19
|
+
type: custom
|
|
20
|
+
runs_on: linux-medium
|
|
21
|
+
steps:
|
|
22
|
+
- uses: eas/checkout
|
|
23
|
+
- uses: eas/install_node_modules
|
|
24
|
+
- name: Typecheck
|
|
25
|
+
run: bun run typecheck
|
|
26
|
+
- name: Lint
|
|
27
|
+
run: bun run lint
|
|
28
|
+
- name: Test
|
|
29
|
+
run: bun run test
|
|
30
|
+
|
|
31
|
+
deploy_convex:
|
|
32
|
+
name: Deploy Convex
|
|
33
|
+
needs: [precheck]
|
|
34
|
+
type: custom
|
|
35
|
+
runs_on: linux-medium
|
|
36
|
+
# `production` env pulls CONVEX_DEPLOY_KEY (a production deploy key, set
|
|
37
|
+
# at secret visibility) into the job. `convex deploy` is non-interactive
|
|
38
|
+
# whenever CONVEX_DEPLOY_KEY is present. no --yes / --auto-accept flag.
|
|
39
|
+
# https://docs.convex.dev/cli#deploy-convex-functions-to-production
|
|
40
|
+
# https://docs.convex.dev/cli/deploy-key-types
|
|
41
|
+
environment: production
|
|
42
|
+
steps:
|
|
43
|
+
- uses: eas/checkout
|
|
44
|
+
- uses: eas/install_node_modules
|
|
45
|
+
- name: Deploy Convex backend
|
|
46
|
+
run: bunx convex deploy
|
|
47
|
+
|
|
48
|
+
fingerprint:
|
|
49
|
+
name: Fingerprint
|
|
50
|
+
needs: [precheck]
|
|
51
|
+
type: fingerprint
|
|
52
|
+
environment: production
|
|
53
|
+
|
|
54
|
+
get_ios_build:
|
|
55
|
+
name: Check existing iOS build
|
|
56
|
+
needs: [fingerprint]
|
|
57
|
+
type: get-build
|
|
58
|
+
params:
|
|
59
|
+
platform: ios
|
|
60
|
+
fingerprint_hash: ${{ needs.fingerprint.outputs.ios_fingerprint_hash }}
|
|
61
|
+
profile: production
|
|
62
|
+
|
|
63
|
+
build_ios:
|
|
64
|
+
name: Build iOS
|
|
65
|
+
needs: [get_ios_build, deploy_convex]
|
|
66
|
+
if: ${{ !needs.get_ios_build.outputs.build_id }}
|
|
67
|
+
type: build
|
|
68
|
+
params:
|
|
69
|
+
platform: ios
|
|
70
|
+
profile: production
|
|
71
|
+
|
|
72
|
+
submit_ios:
|
|
73
|
+
name: Submit to App Store
|
|
74
|
+
needs: [build_ios]
|
|
75
|
+
type: submit
|
|
76
|
+
params:
|
|
77
|
+
build_id: ${{ needs.build_ios.outputs.build_id }}
|
|
78
|
+
|
|
79
|
+
update_ios:
|
|
80
|
+
name: OTA update
|
|
81
|
+
needs: [get_ios_build, deploy_convex]
|
|
82
|
+
if: ${{ needs.get_ios_build.outputs.build_id }}
|
|
83
|
+
type: update
|
|
84
|
+
environment: production
|
|
85
|
+
params:
|
|
86
|
+
platform: ios
|
|
87
|
+
branch: production
|
|
88
|
+
|
|
89
|
+
# Post-deploy notification with EAS Insights link. `update_ios` exposes
|
|
90
|
+
# `first_update_group_id` (https://docs.expo.dev/eas/workflows/pre-packaged-jobs/#update-outputs);
|
|
91
|
+
# we feed it into a Slack message that links to:
|
|
92
|
+
# - the update group page (rollout %, runtime version, builds touched)
|
|
93
|
+
# - the deployments dashboard (adoption curves, crash-free %, unique users)
|
|
94
|
+
# - the channel insights page (filtered to production)
|
|
95
|
+
#
|
|
96
|
+
# `webhook_url` only accepts hardcoded strings today (env-stored webhooks
|
|
97
|
+
# are upcoming per https://docs.expo.dev/eas/workflows/pre-packaged-jobs/#slack);
|
|
98
|
+
# paste yours in `SLACK_WEBHOOK_URL` below and uncomment, or strip this job
|
|
99
|
+
# if you don't use Slack. The `print_insights` job prints `update:insights`
|
|
100
|
+
# JSON to workflow logs so you can audit adoption metrics directly.
|
|
101
|
+
print_insights:
|
|
102
|
+
name: Capture update:insights output
|
|
103
|
+
needs: [update_ios]
|
|
104
|
+
if: ${{ needs.update_ios.outputs.first_update_group_id }}
|
|
105
|
+
runs_on: linux-medium
|
|
106
|
+
steps:
|
|
107
|
+
- uses: eas/checkout
|
|
108
|
+
- uses: eas/install_node_modules
|
|
109
|
+
- name: Print insights JSON
|
|
110
|
+
run: |
|
|
111
|
+
GROUP="${{ needs.update_ios.outputs.first_update_group_id }}"
|
|
112
|
+
echo "Update group: $GROUP"
|
|
113
|
+
bunx eas-cli update:insights "$GROUP" --json --non-interactive || true
|
|
114
|
+
|
|
115
|
+
notify_update_published:
|
|
116
|
+
name: Notify Slack with insights link
|
|
117
|
+
needs: [update_ios]
|
|
118
|
+
if: ${{ needs.update_ios.outputs.first_update_group_id }}
|
|
119
|
+
type: slack
|
|
120
|
+
params:
|
|
121
|
+
# Replace with your Slack incoming-webhook URL. EAS does not yet
|
|
122
|
+
# support reading this from EAS env (see job comment above). interpolation
|
|
123
|
+
# of `${{ env.* }}` for `webhook_url` lands in a future EAS release.
|
|
124
|
+
webhook_url: https://hooks.slack.com/services/REPLACE/ME/HERE
|
|
125
|
+
message: |
|
|
126
|
+
:rocket: OTA update published to production.
|
|
127
|
+
Group: ${{ needs.update_ios.outputs.first_update_group_id }}
|
|
128
|
+
Workflow: ${{ workflow.url }}
|
|
129
|
+
Open the EAS dashboard for adoption %, crash-free %, and unique users.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: Development builds
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch: {}
|
|
5
|
+
|
|
6
|
+
jobs:
|
|
7
|
+
ios_device:
|
|
8
|
+
name: iOS device
|
|
9
|
+
type: build
|
|
10
|
+
params:
|
|
11
|
+
platform: ios
|
|
12
|
+
profile: development
|
|
13
|
+
|
|
14
|
+
ios_simulator:
|
|
15
|
+
name: iOS simulator
|
|
16
|
+
type: build
|
|
17
|
+
params:
|
|
18
|
+
platform: ios
|
|
19
|
+
profile: development:simulator
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: E2E tests
|
|
2
|
+
|
|
3
|
+
# Builds an iOS Simulator binary and runs Maestro flows against it. Mirrors
|
|
4
|
+
# the canonical EAS workflow at
|
|
5
|
+
# https://docs.expo.dev/eas/workflows/examples/e2e-tests.
|
|
6
|
+
#
|
|
7
|
+
# Maestro flows live under .maestro/ at the repo root. The build profile
|
|
8
|
+
# `development:simulator` already exists in eas.json. it produces a
|
|
9
|
+
# .app bundle the Maestro runner can install on an iOS Simulator.
|
|
10
|
+
#
|
|
11
|
+
# `MAESTRO_APP_ID` resolves to the project's bundle id from the EAS development
|
|
12
|
+
# environment (populated by `vexpo full` or `eas env:create`). The flow
|
|
13
|
+
# yaml at .maestro/launch.yaml reads it as `appId: ${MAESTRO_APP_ID}`.
|
|
14
|
+
|
|
15
|
+
on:
|
|
16
|
+
pull_request:
|
|
17
|
+
branches: ["main"]
|
|
18
|
+
workflow_dispatch: {}
|
|
19
|
+
|
|
20
|
+
concurrency:
|
|
21
|
+
cancel_in_progress: true
|
|
22
|
+
group: ${{ workflow.filename }}-${{ github.ref }}
|
|
23
|
+
|
|
24
|
+
jobs:
|
|
25
|
+
build_ios_simulator:
|
|
26
|
+
name: Build iOS Simulator
|
|
27
|
+
type: build
|
|
28
|
+
params:
|
|
29
|
+
platform: ios
|
|
30
|
+
profile: development:simulator
|
|
31
|
+
|
|
32
|
+
maestro:
|
|
33
|
+
name: Run Maestro flows
|
|
34
|
+
needs: [build_ios_simulator]
|
|
35
|
+
type: maestro
|
|
36
|
+
environment: development
|
|
37
|
+
env:
|
|
38
|
+
MAESTRO_APP_ID: ${{ env.EXPO_PUBLIC_APP_BUNDLE_ID }}
|
|
39
|
+
params:
|
|
40
|
+
build_id: ${{ needs.build_ios_simulator.outputs.build_id }}
|
|
41
|
+
flow_path: [".maestro"]
|
|
42
|
+
retries: 2
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
name: PR preview
|
|
2
|
+
|
|
3
|
+
# Builds + auto-posts a comment on the PR with build/update IDs and QR codes
|
|
4
|
+
# via the `github-comment` job. Mirrors the canonical EAS workflow pattern at
|
|
5
|
+
# https://docs.expo.dev/eas/workflows/pre-packaged-jobs/#github-comment.
|
|
6
|
+
#
|
|
7
|
+
# Flow (canonical fingerprint-gated branch model):
|
|
8
|
+
# 1. fingerprint → compute the iOS fingerprint for the PR's dev env.
|
|
9
|
+
# 2. get_existing_build → check if any development:simulator build with
|
|
10
|
+
# that fingerprint already exists (returns build_id if so).
|
|
11
|
+
# 3a. Path A (no compatible build): build a fresh dev-client simulator
|
|
12
|
+
# binary + publish a preview OTA to branch `pr-<n>` so the build
|
|
13
|
+
# picks it up on launch.
|
|
14
|
+
# 3b. Path B (compatible build exists): publish a preview OTA only.
|
|
15
|
+
# 4. github-comment → post the QR / build / update IDs.
|
|
16
|
+
#
|
|
17
|
+
# Either path publishes an update so a) the PR is always testable via a QR
|
|
18
|
+
# scan and b) `pr-<n>` is a real EAS branch from the first commit, not the
|
|
19
|
+
# second. Uses development:simulator (subscribed to the `development`
|
|
20
|
+
# channel). vexpo doesn't ship a separate `preview` build profile.
|
|
21
|
+
|
|
22
|
+
on:
|
|
23
|
+
pull_request:
|
|
24
|
+
branches: ["main"]
|
|
25
|
+
|
|
26
|
+
concurrency:
|
|
27
|
+
cancel_in_progress: true
|
|
28
|
+
group: ${{ workflow.filename }}-${{ github.ref }}
|
|
29
|
+
|
|
30
|
+
defaults:
|
|
31
|
+
tools:
|
|
32
|
+
bun: latest
|
|
33
|
+
|
|
34
|
+
jobs:
|
|
35
|
+
precheck:
|
|
36
|
+
name: Precheck
|
|
37
|
+
type: custom
|
|
38
|
+
runs_on: linux-medium
|
|
39
|
+
steps:
|
|
40
|
+
- uses: eas/checkout
|
|
41
|
+
- uses: eas/install_node_modules
|
|
42
|
+
- name: Typecheck
|
|
43
|
+
run: bun run typecheck
|
|
44
|
+
- name: Lint
|
|
45
|
+
run: bun run lint
|
|
46
|
+
- name: Test
|
|
47
|
+
run: bun run test
|
|
48
|
+
|
|
49
|
+
fingerprint:
|
|
50
|
+
name: Fingerprint
|
|
51
|
+
needs: [precheck]
|
|
52
|
+
type: fingerprint
|
|
53
|
+
environment: development
|
|
54
|
+
|
|
55
|
+
get_existing_build:
|
|
56
|
+
name: Find compatible build
|
|
57
|
+
needs: [fingerprint]
|
|
58
|
+
type: get-build
|
|
59
|
+
params:
|
|
60
|
+
platform: ios
|
|
61
|
+
fingerprint_hash: ${{ needs.fingerprint.outputs.ios_fingerprint_hash }}
|
|
62
|
+
profile: development:simulator
|
|
63
|
+
wait_for_in_progress: true
|
|
64
|
+
|
|
65
|
+
build_ios:
|
|
66
|
+
name: Build iOS Simulator (only if no compatible build exists)
|
|
67
|
+
needs: [get_existing_build]
|
|
68
|
+
if: ${{ !needs.get_existing_build.outputs.build_id }}
|
|
69
|
+
type: build
|
|
70
|
+
params:
|
|
71
|
+
platform: ios
|
|
72
|
+
profile: development:simulator
|
|
73
|
+
|
|
74
|
+
publish_update_new:
|
|
75
|
+
name: Publish preview update (new fingerprint path)
|
|
76
|
+
needs: [build_ios]
|
|
77
|
+
type: update
|
|
78
|
+
environment: development
|
|
79
|
+
params:
|
|
80
|
+
platform: ios
|
|
81
|
+
branch: pr-${{ github.event.pull_request.number }}
|
|
82
|
+
|
|
83
|
+
publish_update_existing:
|
|
84
|
+
name: Publish preview update (existing build path)
|
|
85
|
+
needs: [get_existing_build]
|
|
86
|
+
if: ${{ needs.get_existing_build.outputs.build_id }}
|
|
87
|
+
type: update
|
|
88
|
+
environment: development
|
|
89
|
+
params:
|
|
90
|
+
platform: ios
|
|
91
|
+
branch: pr-${{ github.event.pull_request.number }}
|
|
92
|
+
|
|
93
|
+
comment:
|
|
94
|
+
name: Post results to PR
|
|
95
|
+
after: [build_ios, publish_update_new, publish_update_existing]
|
|
96
|
+
type: github-comment
|
|
97
|
+
params:
|
|
98
|
+
message: "🚀 Preview ready. Scan the QR code or open the link to test."
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
|
|
7
|
+
concurrency:
|
|
8
|
+
cancel_in_progress: true
|
|
9
|
+
group: ${{ workflow.filename }}-${{ github.ref }}
|
|
10
|
+
|
|
11
|
+
defaults:
|
|
12
|
+
tools:
|
|
13
|
+
bun: latest
|
|
14
|
+
|
|
15
|
+
jobs:
|
|
16
|
+
precheck:
|
|
17
|
+
name: Precheck
|
|
18
|
+
type: custom
|
|
19
|
+
runs_on: linux-medium
|
|
20
|
+
steps:
|
|
21
|
+
- uses: eas/checkout
|
|
22
|
+
- uses: eas/install_node_modules
|
|
23
|
+
- name: Typecheck
|
|
24
|
+
run: bun run typecheck
|
|
25
|
+
- name: Lint
|
|
26
|
+
run: bun run lint
|
|
27
|
+
- name: Test
|
|
28
|
+
run: bun run test
|
|
29
|
+
|
|
30
|
+
build_ios:
|
|
31
|
+
name: Build iOS
|
|
32
|
+
needs: [precheck]
|
|
33
|
+
type: build
|
|
34
|
+
params:
|
|
35
|
+
platform: ios
|
|
36
|
+
profile: production
|
|
37
|
+
|
|
38
|
+
submit_ios:
|
|
39
|
+
name: Submit to App Store
|
|
40
|
+
needs: [build_ios]
|
|
41
|
+
type: submit
|
|
42
|
+
params:
|
|
43
|
+
build_id: ${{ needs.build_ios.outputs.build_id }}
|
|
44
|
+
profile: production
|