@aex.is/zero 0.1.8 → 0.1.10
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 +5 -5
- package/bin/zero-darwin-amd64 +0 -0
- package/bin/zero-darwin-arm64 +0 -0
- package/bin/zero-linux-amd64 +0 -0
- package/bin/zero-linux-arm64 +0 -0
- package/bin/zero-windows-amd64.exe +0 -0
- package/dist/config/base-env.js +3 -7
- package/dist/config/frameworks.js +0 -4
- package/dist/config/modules.js +34 -0
- package/dist/config/package-managers.js +41 -0
- package/dist/engine/installers.js +8 -5
- package/dist/engine/scaffold.js +41 -8
- package/dist/engine/templates.js +201 -44
- package/dist/env/detect.js +8 -8
- package/dist/index.js +6 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,20 +22,20 @@ zero
|
|
|
22
22
|
|
|
23
23
|
## Requirements
|
|
24
24
|
|
|
25
|
-
-
|
|
25
|
+
- One package manager installed: npm, pnpm, yarn, or bun.
|
|
26
26
|
|
|
27
27
|
## Generated starters
|
|
28
28
|
|
|
29
29
|
- Next.js App Router + Tailwind + shadcn-ready config.
|
|
30
30
|
- Expo Router + Tamagui.
|
|
31
|
-
- Minimal layout,
|
|
32
|
-
- Contact form wired to `/api/contact` (Next) and
|
|
31
|
+
- Minimal layout, four routes, metadata, and icon generation.
|
|
32
|
+
- Contact form wired to `/api/contact` (Next) and a device POST to your backend (Expo).
|
|
33
33
|
|
|
34
34
|
## Environment variables
|
|
35
35
|
|
|
36
36
|
The CLI generates `.env.example` with:
|
|
37
|
-
- Next.js: `RESEND_API_KEY`, `
|
|
38
|
-
- Expo: `
|
|
37
|
+
- Next.js: `RESEND_API_KEY`, `CONTACT_FROM_EMAIL`, `CONTACT_TO_EMAIL`.
|
|
38
|
+
- Expo: `EXPO_PUBLIC_CONTACT_ENDPOINT`.
|
|
39
39
|
- Any selected module keys (Neon, Clerk, Payload, Stripe).
|
|
40
40
|
|
|
41
41
|
## Development
|
package/bin/zero-darwin-amd64
CHANGED
|
Binary file
|
package/bin/zero-darwin-arm64
CHANGED
|
Binary file
|
package/bin/zero-linux-amd64
CHANGED
|
Binary file
|
package/bin/zero-linux-arm64
CHANGED
|
Binary file
|
|
Binary file
|
package/dist/config/base-env.js
CHANGED
|
@@ -5,22 +5,18 @@ const nextBaseEnv = [
|
|
|
5
5
|
url: 'https://resend.com'
|
|
6
6
|
},
|
|
7
7
|
{
|
|
8
|
-
key: '
|
|
8
|
+
key: 'CONTACT_FROM_EMAIL',
|
|
9
9
|
description: 'Verified sender email address'
|
|
10
10
|
},
|
|
11
11
|
{
|
|
12
|
-
key: '
|
|
12
|
+
key: 'CONTACT_TO_EMAIL',
|
|
13
13
|
description: 'Destination email address'
|
|
14
14
|
}
|
|
15
15
|
];
|
|
16
16
|
const expoBaseEnv = [
|
|
17
|
-
{
|
|
18
|
-
key: 'EXPO_PUBLIC_CONTACT_EMAIL',
|
|
19
|
-
description: 'Email address used for contact form'
|
|
20
|
-
},
|
|
21
17
|
{
|
|
22
18
|
key: 'EXPO_PUBLIC_CONTACT_ENDPOINT',
|
|
23
|
-
description: '
|
|
19
|
+
description: 'Contact API endpoint (e.g. https://yourdomain.com/api/contact)'
|
|
24
20
|
}
|
|
25
21
|
];
|
|
26
22
|
export function getBaseEnvHelp(framework) {
|
|
@@ -13,7 +13,6 @@ export const frameworks = [
|
|
|
13
13
|
'@radix-ui/react-slot'
|
|
14
14
|
],
|
|
15
15
|
scaffold: {
|
|
16
|
-
command: 'bunx',
|
|
17
16
|
packageName: 'create-next-app@latest',
|
|
18
17
|
argSets: [
|
|
19
18
|
[
|
|
@@ -25,7 +24,6 @@ export const frameworks = [
|
|
|
25
24
|
'--no-src-dir',
|
|
26
25
|
'--import-alias',
|
|
27
26
|
'@/*',
|
|
28
|
-
'--use-bun',
|
|
29
27
|
'--skip-install'
|
|
30
28
|
],
|
|
31
29
|
[
|
|
@@ -37,7 +35,6 @@ export const frameworks = [
|
|
|
37
35
|
'--no-src-dir',
|
|
38
36
|
'--import-alias',
|
|
39
37
|
'@/*',
|
|
40
|
-
'--use-bun'
|
|
41
38
|
],
|
|
42
39
|
[
|
|
43
40
|
'--ts',
|
|
@@ -77,7 +74,6 @@ export const frameworks = [
|
|
|
77
74
|
'react-native-svg'
|
|
78
75
|
],
|
|
79
76
|
scaffold: {
|
|
80
|
-
command: 'bunx',
|
|
81
77
|
packageName: 'create-expo-app',
|
|
82
78
|
argSets: [
|
|
83
79
|
['--template', 'expo-router', '--yes', '--no-install'],
|
package/dist/config/modules.js
CHANGED
|
@@ -3,6 +3,10 @@ export const modules = [
|
|
|
3
3
|
id: 'neon',
|
|
4
4
|
label: 'Database (Neon)',
|
|
5
5
|
description: 'Serverless Postgres with Neon.',
|
|
6
|
+
connect: {
|
|
7
|
+
label: 'Connect to Neon',
|
|
8
|
+
url: 'https://console.neon.tech/'
|
|
9
|
+
},
|
|
6
10
|
envVars: [
|
|
7
11
|
{
|
|
8
12
|
key: 'DATABASE_URL',
|
|
@@ -19,6 +23,10 @@ export const modules = [
|
|
|
19
23
|
id: 'clerk',
|
|
20
24
|
label: 'Auth (Clerk)',
|
|
21
25
|
description: 'Authentication with Clerk.',
|
|
26
|
+
connect: {
|
|
27
|
+
label: 'Connect to Clerk',
|
|
28
|
+
url: 'https://dashboard.clerk.com'
|
|
29
|
+
},
|
|
22
30
|
envVars: [
|
|
23
31
|
{
|
|
24
32
|
key: 'CLERK_PUBLISHABLE_KEY',
|
|
@@ -40,6 +48,10 @@ export const modules = [
|
|
|
40
48
|
id: 'payload',
|
|
41
49
|
label: 'CMS (Payload)',
|
|
42
50
|
description: 'Headless CMS using Payload.',
|
|
51
|
+
connect: {
|
|
52
|
+
label: 'Generate Payload Secret',
|
|
53
|
+
url: 'https://payloadcms.com/docs'
|
|
54
|
+
},
|
|
43
55
|
envVars: [
|
|
44
56
|
{
|
|
45
57
|
key: 'PAYLOAD_SECRET',
|
|
@@ -61,6 +73,10 @@ export const modules = [
|
|
|
61
73
|
id: 'stripe',
|
|
62
74
|
label: 'Payments (Stripe)',
|
|
63
75
|
description: 'Payments via Stripe SDK.',
|
|
76
|
+
connect: {
|
|
77
|
+
label: 'Connect to Stripe',
|
|
78
|
+
url: 'https://dashboard.stripe.com/apikeys'
|
|
79
|
+
},
|
|
64
80
|
envVars: [
|
|
65
81
|
{
|
|
66
82
|
key: 'STRIPE_SECRET_KEY',
|
|
@@ -118,3 +134,21 @@ export function getModuleEnvHelp(moduleIds) {
|
|
|
118
134
|
}
|
|
119
135
|
return Array.from(map.values());
|
|
120
136
|
}
|
|
137
|
+
export function getModuleConnections(moduleIds) {
|
|
138
|
+
const connections = [];
|
|
139
|
+
for (const id of moduleIds) {
|
|
140
|
+
const module = getModuleDefinition(id);
|
|
141
|
+
if (module.connect) {
|
|
142
|
+
connections.push(module.connect);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const fallback = module.envVars.find((item) => item.url);
|
|
146
|
+
if (fallback?.url) {
|
|
147
|
+
connections.push({
|
|
148
|
+
label: `Connect to ${module.label}`,
|
|
149
|
+
url: fallback.url
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return connections;
|
|
154
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export const packageManagers = [
|
|
2
|
+
{
|
|
3
|
+
id: 'npm',
|
|
4
|
+
label: 'npm',
|
|
5
|
+
runner: { command: 'npx', args: [] },
|
|
6
|
+
install: ['npm', 'install'],
|
|
7
|
+
add: ['npm', 'install'],
|
|
8
|
+
dev: ['npm', 'run', 'dev']
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
id: 'pnpm',
|
|
12
|
+
label: 'pnpm',
|
|
13
|
+
runner: { command: 'pnpm', args: ['dlx'] },
|
|
14
|
+
install: ['pnpm', 'install'],
|
|
15
|
+
add: ['pnpm', 'add'],
|
|
16
|
+
dev: ['pnpm', 'dev']
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: 'yarn',
|
|
20
|
+
label: 'yarn',
|
|
21
|
+
runner: { command: 'yarn', args: ['dlx'] },
|
|
22
|
+
install: ['yarn', 'install'],
|
|
23
|
+
add: ['yarn', 'add'],
|
|
24
|
+
dev: ['yarn', 'dev']
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: 'bun',
|
|
28
|
+
label: 'bun',
|
|
29
|
+
runner: { command: 'bunx', args: [] },
|
|
30
|
+
install: ['bun', 'install'],
|
|
31
|
+
add: ['bun', 'add'],
|
|
32
|
+
dev: ['bun', 'run', 'dev']
|
|
33
|
+
}
|
|
34
|
+
];
|
|
35
|
+
export function getPackageManagerDefinition(id) {
|
|
36
|
+
const manager = packageManagers.find((item) => item.id === id);
|
|
37
|
+
if (!manager) {
|
|
38
|
+
throw new Error(`Unknown package manager: ${id}`);
|
|
39
|
+
}
|
|
40
|
+
return manager;
|
|
41
|
+
}
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { execa } from 'execa';
|
|
2
2
|
import { getModulePackages } from '../config/modules.js';
|
|
3
|
+
import { getPackageManagerDefinition } from '../config/package-managers.js';
|
|
3
4
|
const baseEnv = {
|
|
4
5
|
...process.env,
|
|
5
6
|
CI: '1'
|
|
6
7
|
};
|
|
7
|
-
export async function installBaseDependencies(targetDir, extraPackages = []) {
|
|
8
|
+
export async function installBaseDependencies(targetDir, packageManager, extraPackages = []) {
|
|
9
|
+
const manager = getPackageManagerDefinition(packageManager);
|
|
8
10
|
if (extraPackages.length > 0) {
|
|
9
11
|
const packages = Array.from(new Set(extraPackages)).sort();
|
|
10
|
-
await execa(
|
|
12
|
+
await execa(manager.add[0], [...manager.add.slice(1), ...packages], {
|
|
11
13
|
cwd: targetDir,
|
|
12
14
|
stdio: 'inherit',
|
|
13
15
|
env: baseEnv,
|
|
@@ -15,19 +17,20 @@ export async function installBaseDependencies(targetDir, extraPackages = []) {
|
|
|
15
17
|
});
|
|
16
18
|
return;
|
|
17
19
|
}
|
|
18
|
-
await execa(
|
|
20
|
+
await execa(manager.install[0], manager.install.slice(1), {
|
|
19
21
|
cwd: targetDir,
|
|
20
22
|
stdio: 'inherit',
|
|
21
23
|
env: baseEnv,
|
|
22
24
|
shell: false
|
|
23
25
|
});
|
|
24
26
|
}
|
|
25
|
-
export async function installModulePackages(framework, moduleIds, targetDir) {
|
|
27
|
+
export async function installModulePackages(framework, moduleIds, targetDir, packageManager) {
|
|
26
28
|
const packages = getModulePackages(moduleIds, framework);
|
|
27
29
|
if (packages.length === 0) {
|
|
28
30
|
return;
|
|
29
31
|
}
|
|
30
|
-
|
|
32
|
+
const manager = getPackageManagerDefinition(packageManager);
|
|
33
|
+
await execa(manager.add[0], [...manager.add.slice(1), ...packages], {
|
|
31
34
|
cwd: targetDir,
|
|
32
35
|
stdio: 'inherit',
|
|
33
36
|
env: baseEnv,
|
package/dist/engine/scaffold.js
CHANGED
|
@@ -3,11 +3,12 @@ import { promises as fs } from 'fs';
|
|
|
3
3
|
import { execa } from 'execa';
|
|
4
4
|
import { getFrameworkDefinition } from '../config/frameworks.js';
|
|
5
5
|
import { getBaseEnvHelp } from '../config/base-env.js';
|
|
6
|
-
import { getModuleEnvHelp } from '../config/modules.js';
|
|
6
|
+
import { getModuleConnections, getModuleEnvHelp } from '../config/modules.js';
|
|
7
7
|
import { installBaseDependencies, installModulePackages } from './installers.js';
|
|
8
8
|
import { writeEnvExample } from './env.js';
|
|
9
9
|
import { assertAssetSources, generateExpoAssets, generateNextAssets, resolveAssetSources } from './assets.js';
|
|
10
10
|
import { buildNextTemplateFiles, buildExpoTemplateFiles, componentsJsonTemplate } from './templates.js';
|
|
11
|
+
import { getPackageManagerDefinition } from '../config/package-managers.js';
|
|
11
12
|
const baseEnv = {
|
|
12
13
|
...process.env,
|
|
13
14
|
CI: '1'
|
|
@@ -17,10 +18,12 @@ export async function scaffoldProject(config) {
|
|
|
17
18
|
const targetDir = path.resolve(process.cwd(), directoryInput);
|
|
18
19
|
await ensureEmptyTargetDir(targetDir);
|
|
19
20
|
const framework = getFrameworkDefinition(config.framework);
|
|
21
|
+
const packageManager = getPackageManagerDefinition(config.packageManager);
|
|
20
22
|
const sources = resolveAssetSources();
|
|
21
23
|
await assertAssetSources(sources);
|
|
22
24
|
console.log(`Scaffolding ${framework.label}...`);
|
|
23
|
-
|
|
25
|
+
const argSets = buildScaffoldArgs(framework, config.packageManager);
|
|
26
|
+
await runScaffoldCommand(packageManager.runner, framework.scaffold.packageName, directoryInput, argSets);
|
|
24
27
|
console.log('Applying framework templates...');
|
|
25
28
|
if (config.framework === 'nextjs') {
|
|
26
29
|
await applyNextTemplates(config, targetDir);
|
|
@@ -45,15 +48,16 @@ export async function scaffoldProject(config) {
|
|
|
45
48
|
assetsDir: path.join(targetDir, 'assets')
|
|
46
49
|
});
|
|
47
50
|
}
|
|
48
|
-
console.log(
|
|
49
|
-
await installBaseDependencies(targetDir, framework.packages);
|
|
51
|
+
console.log(`Installing base dependencies with ${packageManager.label}...`);
|
|
52
|
+
await installBaseDependencies(targetDir, config.packageManager, framework.packages);
|
|
50
53
|
console.log('Installing module packages...');
|
|
51
|
-
await installModulePackages(config.framework, config.modules, targetDir);
|
|
54
|
+
await installModulePackages(config.framework, config.modules, targetDir, config.packageManager);
|
|
52
55
|
console.log('Generating .env.example...');
|
|
53
56
|
await writeEnvExample(config.modules, config.framework, targetDir);
|
|
54
57
|
console.log('Scaffold complete.');
|
|
55
58
|
const cdTarget = directoryInput === '.' ? '.' : directoryInput.includes(' ') ? `"${directoryInput}"` : directoryInput;
|
|
56
|
-
|
|
59
|
+
const devCommand = packageManager.dev.join(' ');
|
|
60
|
+
console.log(`\nNext steps:\n 1) cd ${cdTarget}\n 2) ${devCommand}`);
|
|
57
61
|
}
|
|
58
62
|
async function ensureEmptyTargetDir(targetDir) {
|
|
59
63
|
try {
|
|
@@ -75,12 +79,12 @@ async function ensureEmptyTargetDir(targetDir) {
|
|
|
75
79
|
throw error;
|
|
76
80
|
}
|
|
77
81
|
}
|
|
78
|
-
async function runScaffoldCommand(
|
|
82
|
+
async function runScaffoldCommand(runner, packageName, directoryInput, argSets) {
|
|
79
83
|
const errors = [];
|
|
80
84
|
const targetArg = directoryInput === '.' ? '.' : directoryInput;
|
|
81
85
|
for (const args of argSets) {
|
|
82
86
|
try {
|
|
83
|
-
await execa(command, [packageName, targetArg, ...args], {
|
|
87
|
+
await execa(runner.command, [...runner.args, packageName, targetArg, ...args], {
|
|
84
88
|
stdio: 'inherit',
|
|
85
89
|
env: baseEnv,
|
|
86
90
|
shell: false
|
|
@@ -96,15 +100,42 @@ async function runScaffoldCommand(command, packageName, directoryInput, argSets)
|
|
|
96
100
|
: 'Scaffold failed for unknown reasons.';
|
|
97
101
|
throw new Error(message);
|
|
98
102
|
}
|
|
103
|
+
function buildScaffoldArgs(framework, packageManager) {
|
|
104
|
+
if (framework.id !== 'nextjs') {
|
|
105
|
+
return framework.scaffold.argSets;
|
|
106
|
+
}
|
|
107
|
+
const flag = resolveNextPackageManagerFlag(packageManager);
|
|
108
|
+
if (!flag) {
|
|
109
|
+
return framework.scaffold.argSets;
|
|
110
|
+
}
|
|
111
|
+
const withFlag = framework.scaffold.argSets.map((args) => [...args, flag]);
|
|
112
|
+
return [...withFlag, ...framework.scaffold.argSets];
|
|
113
|
+
}
|
|
114
|
+
function resolveNextPackageManagerFlag(packageManager) {
|
|
115
|
+
switch (packageManager) {
|
|
116
|
+
case 'npm':
|
|
117
|
+
return '--use-npm';
|
|
118
|
+
case 'pnpm':
|
|
119
|
+
return '--use-pnpm';
|
|
120
|
+
case 'yarn':
|
|
121
|
+
return '--use-yarn';
|
|
122
|
+
case 'bun':
|
|
123
|
+
return '--use-bun';
|
|
124
|
+
default:
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
99
128
|
async function applyNextTemplates(config, targetDir) {
|
|
100
129
|
const srcAppDir = path.join(targetDir, 'src', 'app');
|
|
101
130
|
const usesSrcDir = await pathExists(srcAppDir);
|
|
102
131
|
const basePath = usesSrcDir ? 'src' : '';
|
|
103
132
|
const envHelp = mergeEnvHelp(getBaseEnvHelp(config.framework), getModuleEnvHelp(config.modules));
|
|
133
|
+
const connections = getModuleConnections(config.modules);
|
|
104
134
|
const files = buildNextTemplateFiles({
|
|
105
135
|
appName: config.appName,
|
|
106
136
|
domain: config.domain,
|
|
107
137
|
envVars: envHelp,
|
|
138
|
+
connections,
|
|
108
139
|
basePath
|
|
109
140
|
});
|
|
110
141
|
await writeTemplateFiles(targetDir, files);
|
|
@@ -117,10 +148,12 @@ async function applyNextTemplates(config, targetDir) {
|
|
|
117
148
|
}
|
|
118
149
|
async function applyExpoTemplates(config, targetDir) {
|
|
119
150
|
const envHelp = mergeEnvHelp(getBaseEnvHelp(config.framework), getModuleEnvHelp(config.modules));
|
|
151
|
+
const connections = getModuleConnections(config.modules);
|
|
120
152
|
const files = buildExpoTemplateFiles({
|
|
121
153
|
appName: config.appName,
|
|
122
154
|
domain: config.domain,
|
|
123
155
|
envVars: envHelp,
|
|
156
|
+
connections,
|
|
124
157
|
basePath: ''
|
|
125
158
|
});
|
|
126
159
|
await writeTemplateFiles(targetDir, files);
|
package/dist/engine/templates.js
CHANGED
|
@@ -20,6 +20,7 @@ export function componentsJsonTemplate(globalsPath, tailwindConfigPath) {
|
|
|
20
20
|
export function buildNextTemplateFiles(data) {
|
|
21
21
|
const base = data.basePath ? `${data.basePath}/` : '';
|
|
22
22
|
const envList = renderNextEnvList(data.envVars);
|
|
23
|
+
const connectionSection = renderNextConnectionSection(data.connections);
|
|
23
24
|
return [
|
|
24
25
|
{
|
|
25
26
|
path: `${base}app/layout.tsx`,
|
|
@@ -27,7 +28,7 @@ export function buildNextTemplateFiles(data) {
|
|
|
27
28
|
},
|
|
28
29
|
{
|
|
29
30
|
path: `${base}app/page.tsx`,
|
|
30
|
-
content: nextHomeTemplate(data.appName, data.domain, envList)
|
|
31
|
+
content: nextHomeTemplate(data.appName, data.domain, envList, connectionSection)
|
|
31
32
|
},
|
|
32
33
|
{
|
|
33
34
|
path: `${base}app/about/page.tsx`,
|
|
@@ -37,6 +38,10 @@ export function buildNextTemplateFiles(data) {
|
|
|
37
38
|
path: `${base}app/guide/page.tsx`,
|
|
38
39
|
content: nextRouteTemplate('Guide', 'Three routes are ready. Customize and ship.')
|
|
39
40
|
},
|
|
41
|
+
{
|
|
42
|
+
path: `${base}app/contact/page.tsx`,
|
|
43
|
+
content: nextContactPageTemplate()
|
|
44
|
+
},
|
|
40
45
|
{
|
|
41
46
|
path: `${base}app/api/contact/route.ts`,
|
|
42
47
|
content: nextContactRouteTemplate(data.appName)
|
|
@@ -57,6 +62,10 @@ export function buildNextTemplateFiles(data) {
|
|
|
57
62
|
path: `${base}components/contact-form.tsx`,
|
|
58
63
|
content: nextContactFormTemplate()
|
|
59
64
|
},
|
|
65
|
+
{
|
|
66
|
+
path: `${base}components/connection-guide.tsx`,
|
|
67
|
+
content: nextConnectionGuideTemplate(data.connections)
|
|
68
|
+
},
|
|
60
69
|
{
|
|
61
70
|
path: `${base}components/env-list.tsx`,
|
|
62
71
|
content: nextEnvListTemplate(envList)
|
|
@@ -69,6 +78,7 @@ export function buildNextTemplateFiles(data) {
|
|
|
69
78
|
}
|
|
70
79
|
export function buildExpoTemplateFiles(data) {
|
|
71
80
|
const envItems = renderExpoEnvItems(data.envVars);
|
|
81
|
+
const connectionItems = renderConnectionItems(data.connections);
|
|
72
82
|
return [
|
|
73
83
|
{
|
|
74
84
|
path: 'app/_layout.tsx',
|
|
@@ -78,6 +88,10 @@ export function buildExpoTemplateFiles(data) {
|
|
|
78
88
|
path: 'app/index.tsx',
|
|
79
89
|
content: expoHomeTemplate(data.appName, data.domain, envItems)
|
|
80
90
|
},
|
|
91
|
+
{
|
|
92
|
+
path: 'app/contact.tsx',
|
|
93
|
+
content: expoContactTemplate()
|
|
94
|
+
},
|
|
81
95
|
{
|
|
82
96
|
path: 'app/about.tsx',
|
|
83
97
|
content: expoRouteTemplate('About', 'A concise overview of your project.')
|
|
@@ -102,6 +116,10 @@ export function buildExpoTemplateFiles(data) {
|
|
|
102
116
|
path: 'components/env-list.tsx',
|
|
103
117
|
content: expoEnvListTemplate(envItems)
|
|
104
118
|
},
|
|
119
|
+
{
|
|
120
|
+
path: 'components/connection-guide.tsx',
|
|
121
|
+
content: expoConnectionGuideTemplate(connectionItems)
|
|
122
|
+
},
|
|
105
123
|
{
|
|
106
124
|
path: 'components/contact-form.tsx',
|
|
107
125
|
content: expoContactFormTemplate()
|
|
@@ -171,9 +189,10 @@ export default function RootLayout({
|
|
|
171
189
|
}
|
|
172
190
|
`;
|
|
173
191
|
}
|
|
174
|
-
function nextHomeTemplate(appName, domain, envList) {
|
|
192
|
+
function nextHomeTemplate(appName, domain, envList, connectionSection) {
|
|
175
193
|
return `import { EnvList } from '@/components/env-list';
|
|
176
194
|
import { ContactForm } from '@/components/contact-form';
|
|
195
|
+
import { ConnectionGuide } from '@/components/connection-guide';
|
|
177
196
|
|
|
178
197
|
export const metadata = {
|
|
179
198
|
title: 'Home',
|
|
@@ -197,11 +216,13 @@ export default function Home() {
|
|
|
197
216
|
</p>
|
|
198
217
|
<EnvList />
|
|
199
218
|
</div>
|
|
219
|
+
${connectionSection}
|
|
200
220
|
<div className="flex flex-col gap-2">
|
|
201
221
|
<h2 className="text-base font-bold">Routes</h2>
|
|
202
222
|
<p className="text-base">
|
|
203
|
-
Explore <code className="bg-[var(--fg)] px-2 py-1 text-[var(--bg)]">/about</code
|
|
204
|
-
<code className="bg-[var(--fg)] px-2 py-1 text-[var(--bg)]">/guide</code
|
|
223
|
+
Explore <code className="bg-[var(--fg)] px-2 py-1 text-[var(--bg)]">/about</code>,{' '}
|
|
224
|
+
<code className="bg-[var(--fg)] px-2 py-1 text-[var(--bg)]">/guide</code>, and{' '}
|
|
225
|
+
<code className="bg-[var(--fg)] px-2 py-1 text-[var(--bg)]">/contact</code>.
|
|
205
226
|
</p>
|
|
206
227
|
</div>
|
|
207
228
|
<ContactForm />
|
|
@@ -232,7 +253,8 @@ function nextHeaderTemplate(appName) {
|
|
|
232
253
|
const links = [
|
|
233
254
|
{ href: '/', label: 'Home' },
|
|
234
255
|
{ href: '/about', label: 'About' },
|
|
235
|
-
{ href: '/guide', label: 'Guide' }
|
|
256
|
+
{ href: '/guide', label: 'Guide' },
|
|
257
|
+
{ href: '/contact', label: 'Contact' }
|
|
236
258
|
];
|
|
237
259
|
|
|
238
260
|
export function SiteHeader({ appName }: { appName: string }) {
|
|
@@ -437,12 +459,12 @@ function isNonEmpty(value: unknown): value is string {
|
|
|
437
459
|
|
|
438
460
|
export async function POST(request: Request) {
|
|
439
461
|
const apiKey = process.env.RESEND_API_KEY?.trim();
|
|
440
|
-
const from = process.env.
|
|
441
|
-
const to = process.env.
|
|
462
|
+
const from = process.env.CONTACT_FROM_EMAIL?.trim();
|
|
463
|
+
const to = process.env.CONTACT_TO_EMAIL?.trim();
|
|
442
464
|
|
|
443
465
|
if (!apiKey || !from || !to) {
|
|
444
466
|
return Response.json(
|
|
445
|
-
{ error: 'Set RESEND_API_KEY,
|
|
467
|
+
{ error: 'Set RESEND_API_KEY, CONTACT_FROM_EMAIL, and CONTACT_TO_EMAIL.' },
|
|
446
468
|
{ status: 500 }
|
|
447
469
|
);
|
|
448
470
|
}
|
|
@@ -472,6 +494,25 @@ export async function POST(request: Request) {
|
|
|
472
494
|
}
|
|
473
495
|
`;
|
|
474
496
|
}
|
|
497
|
+
function nextContactPageTemplate() {
|
|
498
|
+
return `import { ContactForm } from '@/components/contact-form';
|
|
499
|
+
|
|
500
|
+
export const metadata = {
|
|
501
|
+
title: 'Contact',
|
|
502
|
+
description: 'Send a message to your inbox.'
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
export default function ContactPage() {
|
|
506
|
+
return (
|
|
507
|
+
<section className="mx-auto flex w-full max-w-3xl flex-col gap-6 px-6 py-12">
|
|
508
|
+
<h1 className="text-base font-bold">Contact</h1>
|
|
509
|
+
<p className="text-base">Send a message to your inbox.</p>
|
|
510
|
+
<ContactForm />
|
|
511
|
+
</section>
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
`;
|
|
515
|
+
}
|
|
475
516
|
function nextGlobalsCss() {
|
|
476
517
|
return `@import url('https://fonts.googleapis.com/css2?family=Geist+Mono:wght@400;700&display=swap');
|
|
477
518
|
|
|
@@ -547,6 +588,47 @@ function renderNextEnvList(envVars) {
|
|
|
547
588
|
})
|
|
548
589
|
.join('\n');
|
|
549
590
|
}
|
|
591
|
+
function renderNextConnectionSection(_connections) {
|
|
592
|
+
return '<ConnectionGuide />';
|
|
593
|
+
}
|
|
594
|
+
function nextConnectionGuideTemplate(connections) {
|
|
595
|
+
const items = renderConnectionItems(connections);
|
|
596
|
+
return `import Link from 'next/link';
|
|
597
|
+
|
|
598
|
+
const connections = ${items};
|
|
599
|
+
|
|
600
|
+
export function ConnectionGuide() {
|
|
601
|
+
if (connections.length === 0) {
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return (
|
|
606
|
+
<div className="flex flex-col gap-3">
|
|
607
|
+
<h2 className="text-base font-bold">Connection guide</h2>
|
|
608
|
+
{connections.map((item) => (
|
|
609
|
+
<div key={item.label} className="flex flex-col gap-1">
|
|
610
|
+
<Link
|
|
611
|
+
href={item.url}
|
|
612
|
+
target="_blank"
|
|
613
|
+
rel="noreferrer"
|
|
614
|
+
className="text-base font-bold underline underline-offset-4"
|
|
615
|
+
>
|
|
616
|
+
{item.label}
|
|
617
|
+
</Link>
|
|
618
|
+
<p className="text-base">
|
|
619
|
+
or go to{' '}
|
|
620
|
+
<Link href={item.url} target="_blank" rel="noreferrer" className="underline underline-offset-4">
|
|
621
|
+
{item.url}
|
|
622
|
+
</Link>{' '}
|
|
623
|
+
directly.
|
|
624
|
+
</p>
|
|
625
|
+
</div>
|
|
626
|
+
))}
|
|
627
|
+
</div>
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
`;
|
|
631
|
+
}
|
|
550
632
|
function expoLayoutTemplate() {
|
|
551
633
|
return `import { Stack } from 'expo-router';
|
|
552
634
|
import { TamaguiProvider, Theme } from 'tamagui';
|
|
@@ -577,6 +659,7 @@ function expoHomeTemplate(appName, domain, envItems) {
|
|
|
577
659
|
import { Text, YStack } from 'tamagui';
|
|
578
660
|
import { PageShell } from '../components/page-shell';
|
|
579
661
|
import { EnvList } from '../components/env-list';
|
|
662
|
+
import { ConnectionGuide } from '../components/connection-guide';
|
|
580
663
|
import { ContactForm } from '../components/contact-form';
|
|
581
664
|
import { FONT_BOLD, FONT_REGULAR, FONT_SIZE, useThemeColors } from '../components/theme';
|
|
582
665
|
|
|
@@ -602,12 +685,13 @@ export default function Home() {
|
|
|
602
685
|
</Text>
|
|
603
686
|
<EnvList />
|
|
604
687
|
</YStack>
|
|
688
|
+
<ConnectionGuide />
|
|
605
689
|
<YStack gap="$2">
|
|
606
690
|
<Text fontFamily={FONT_BOLD} fontSize={FONT_SIZE} color={fg}>
|
|
607
691
|
Routes
|
|
608
692
|
</Text>
|
|
609
693
|
<Text fontFamily={FONT_REGULAR} fontSize={FONT_SIZE} color={fg}>
|
|
610
|
-
Visit /about and /
|
|
694
|
+
Visit /about, /guide, and /contact.
|
|
611
695
|
</Text>
|
|
612
696
|
</YStack>
|
|
613
697
|
<ContactForm />
|
|
@@ -616,6 +700,27 @@ export default function Home() {
|
|
|
616
700
|
}
|
|
617
701
|
`;
|
|
618
702
|
}
|
|
703
|
+
function expoContactTemplate() {
|
|
704
|
+
return `import { Head } from 'expo-router/head';
|
|
705
|
+
import { PageShell } from '../components/page-shell';
|
|
706
|
+
import { ContactForm } from '../components/contact-form';
|
|
707
|
+
|
|
708
|
+
export default function Contact() {
|
|
709
|
+
return (
|
|
710
|
+
<PageShell title="Contact" subtitle="Send a message to your inbox.">
|
|
711
|
+
<Head>
|
|
712
|
+
<title>Contact</title>
|
|
713
|
+
<meta name="description" content="Send a message to your inbox." />
|
|
714
|
+
<meta property="og:title" content="Contact" />
|
|
715
|
+
<meta property="og:description" content="Send a message to your inbox." />
|
|
716
|
+
<meta property="twitter:card" content="summary_large_image" />
|
|
717
|
+
</Head>
|
|
718
|
+
<ContactForm />
|
|
719
|
+
</PageShell>
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
`;
|
|
723
|
+
}
|
|
619
724
|
function expoRouteTemplate(title, body) {
|
|
620
725
|
return `import { Head } from 'expo-router/head';
|
|
621
726
|
import { PageShell } from '../components/page-shell';
|
|
@@ -674,7 +779,8 @@ import { FONT_BOLD, FONT_REGULAR, FONT_SIZE, useThemeColors } from './theme';
|
|
|
674
779
|
const links = [
|
|
675
780
|
{ href: '/', label: 'Home' },
|
|
676
781
|
{ href: '/about', label: 'About' },
|
|
677
|
-
{ href: '/guide', label: 'Guide' }
|
|
782
|
+
{ href: '/guide', label: 'Guide' },
|
|
783
|
+
{ href: '/contact', label: 'Contact' }
|
|
678
784
|
];
|
|
679
785
|
|
|
680
786
|
export function SiteHeader() {
|
|
@@ -793,13 +899,62 @@ export function EnvList() {
|
|
|
793
899
|
}
|
|
794
900
|
`;
|
|
795
901
|
}
|
|
902
|
+
function expoConnectionGuideTemplate(connectionItems) {
|
|
903
|
+
return `import { Linking } from 'react-native';
|
|
904
|
+
import { Text, YStack } from 'tamagui';
|
|
905
|
+
import { FONT_BOLD, FONT_REGULAR, FONT_SIZE, useThemeColors } from './theme';
|
|
906
|
+
|
|
907
|
+
const connections = ${connectionItems};
|
|
908
|
+
|
|
909
|
+
export function ConnectionGuide() {
|
|
910
|
+
const { fg } = useThemeColors();
|
|
911
|
+
|
|
912
|
+
if (connections.length === 0) {
|
|
913
|
+
return null;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
return (
|
|
917
|
+
<YStack gap="$3">
|
|
918
|
+
<Text fontFamily={FONT_BOLD} fontSize={FONT_SIZE} color={fg}>
|
|
919
|
+
Connection guide
|
|
920
|
+
</Text>
|
|
921
|
+
{connections.map((item) => (
|
|
922
|
+
<YStack key={item.label} gap="$1">
|
|
923
|
+
<Text
|
|
924
|
+
fontFamily={FONT_BOLD}
|
|
925
|
+
fontSize={FONT_SIZE}
|
|
926
|
+
color={fg}
|
|
927
|
+
textDecorationLine="underline"
|
|
928
|
+
onPress={() => Linking.openURL(item.url)}
|
|
929
|
+
>
|
|
930
|
+
{item.label}
|
|
931
|
+
</Text>
|
|
932
|
+
<Text fontFamily={FONT_REGULAR} fontSize={FONT_SIZE} color={fg}>
|
|
933
|
+
or go to{' '}
|
|
934
|
+
<Text
|
|
935
|
+
fontFamily={FONT_REGULAR}
|
|
936
|
+
fontSize={FONT_SIZE}
|
|
937
|
+
color={fg}
|
|
938
|
+
textDecorationLine="underline"
|
|
939
|
+
onPress={() => Linking.openURL(item.url)}
|
|
940
|
+
>
|
|
941
|
+
{item.url}
|
|
942
|
+
</Text>{' '}
|
|
943
|
+
directly.
|
|
944
|
+
</Text>
|
|
945
|
+
</YStack>
|
|
946
|
+
))}
|
|
947
|
+
</YStack>
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
`;
|
|
951
|
+
}
|
|
796
952
|
function expoContactFormTemplate() {
|
|
797
953
|
return `import { useState } from 'react';
|
|
798
|
-
import {
|
|
954
|
+
import { TextInput } from 'react-native';
|
|
799
955
|
import { Button, Text, YStack } from 'tamagui';
|
|
800
956
|
import { FONT_BOLD, FONT_REGULAR, FONT_SIZE, useThemeColors } from './theme';
|
|
801
957
|
|
|
802
|
-
const CONTACT_EMAIL = process.env.EXPO_PUBLIC_CONTACT_EMAIL?.trim() ?? '';
|
|
803
958
|
const CONTACT_ENDPOINT = process.env.EXPO_PUBLIC_CONTACT_ENDPOINT?.trim() ?? '';
|
|
804
959
|
|
|
805
960
|
type Status = 'idle' | 'sending' | 'sent' | 'error';
|
|
@@ -828,46 +983,34 @@ export function ContactForm() {
|
|
|
828
983
|
return;
|
|
829
984
|
}
|
|
830
985
|
|
|
986
|
+
if (!CONTACT_ENDPOINT) {
|
|
987
|
+
setStatus('error');
|
|
988
|
+
setError('Set EXPO_PUBLIC_CONTACT_ENDPOINT.');
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
|
|
831
992
|
setStatus('sending');
|
|
832
993
|
setError('');
|
|
833
994
|
|
|
834
995
|
let sent = false;
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
sent = false;
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
if (!sent && CONTACT_EMAIL) {
|
|
853
|
-
const subject = encodeURIComponent(\`New message from \${name.trim()}\`);
|
|
854
|
-
const body = encodeURIComponent(\`Name: \${name.trim()}\\nEmail: \${email.trim()}\\n\\n\${message.trim()}\`);
|
|
855
|
-
const url = \`mailto:\${CONTACT_EMAIL}?subject=\${subject}&body=\${body}\`;
|
|
856
|
-
try {
|
|
857
|
-
await Linking.openURL(url);
|
|
858
|
-
sent = true;
|
|
859
|
-
} catch {
|
|
860
|
-
sent = false;
|
|
861
|
-
}
|
|
996
|
+
try {
|
|
997
|
+
const response = await fetch(CONTACT_ENDPOINT, {
|
|
998
|
+
method: 'POST',
|
|
999
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1000
|
+
body: JSON.stringify({
|
|
1001
|
+
name: name.trim(),
|
|
1002
|
+
email: email.trim(),
|
|
1003
|
+
message: message.trim()
|
|
1004
|
+
})
|
|
1005
|
+
});
|
|
1006
|
+
sent = response.ok;
|
|
1007
|
+
} catch {
|
|
1008
|
+
sent = false;
|
|
862
1009
|
}
|
|
863
1010
|
|
|
864
1011
|
if (!sent) {
|
|
865
1012
|
setStatus('error');
|
|
866
|
-
|
|
867
|
-
setError('Unable to send message.');
|
|
868
|
-
} else {
|
|
869
|
-
setError('Set EXPO_PUBLIC_CONTACT_ENDPOINT or EXPO_PUBLIC_CONTACT_EMAIL.');
|
|
870
|
-
}
|
|
1013
|
+
setError('Unable to send message.');
|
|
871
1014
|
return;
|
|
872
1015
|
}
|
|
873
1016
|
|
|
@@ -1047,6 +1190,20 @@ function renderExpoEnvItems(envVars) {
|
|
|
1047
1190
|
${items.join(',\n ')}
|
|
1048
1191
|
]`;
|
|
1049
1192
|
}
|
|
1193
|
+
function renderConnectionItems(connections) {
|
|
1194
|
+
if (connections.length === 0) {
|
|
1195
|
+
return '[]';
|
|
1196
|
+
}
|
|
1197
|
+
const items = connections.map((item) => {
|
|
1198
|
+
return `{
|
|
1199
|
+
label: '${escapeTemplate(item.label)}',
|
|
1200
|
+
url: '${escapeTemplate(item.url)}'
|
|
1201
|
+
}`;
|
|
1202
|
+
});
|
|
1203
|
+
return `[
|
|
1204
|
+
${items.join(',\n ')}
|
|
1205
|
+
]`;
|
|
1206
|
+
}
|
|
1050
1207
|
function escapeTemplate(value) {
|
|
1051
1208
|
return value
|
|
1052
1209
|
.replace(/`/g, '\\`')
|
package/dist/env/detect.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import which from 'which';
|
|
2
|
+
import { getPackageManagerDefinition } from '../config/package-managers.js';
|
|
2
3
|
export function detectPlatform() {
|
|
3
4
|
switch (process.platform) {
|
|
4
5
|
case 'darwin':
|
|
@@ -14,23 +15,22 @@ export function detectPlatform() {
|
|
|
14
15
|
export function detectShell(platform = detectPlatform()) {
|
|
15
16
|
return platform === 'windows' ? 'powershell' : 'posix';
|
|
16
17
|
}
|
|
17
|
-
export async function
|
|
18
|
+
export async function isCommandAvailable(command) {
|
|
18
19
|
try {
|
|
19
|
-
await which(
|
|
20
|
+
await which(command);
|
|
20
21
|
return true;
|
|
21
22
|
}
|
|
22
23
|
catch {
|
|
23
24
|
return false;
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
|
-
export async function
|
|
27
|
-
const
|
|
27
|
+
export async function assertPackageManagerAvailable(packageManager) {
|
|
28
|
+
const manager = getPackageManagerDefinition(packageManager);
|
|
29
|
+
const command = manager.install[0];
|
|
30
|
+
const available = await isCommandAvailable(command);
|
|
28
31
|
if (!available) {
|
|
29
32
|
const platform = detectPlatform();
|
|
30
33
|
const shell = detectShell(platform);
|
|
31
|
-
|
|
32
|
-
? 'Install Bun for Windows and reopen PowerShell.'
|
|
33
|
-
: 'Install Bun and ensure it is on your PATH.';
|
|
34
|
-
throw new Error(`Bun is required but was not found in PATH. (${platform}/${shell}) ${hint}`);
|
|
34
|
+
throw new Error(`${manager.label} is required but was not found in PATH. (${platform}/${shell}) Install ${manager.label} and retry.`);
|
|
35
35
|
}
|
|
36
36
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { runWizard } from './cli/bubbletea.js';
|
|
3
|
-
import {
|
|
3
|
+
import { assertPackageManagerAvailable } from './env/detect.js';
|
|
4
4
|
import { scaffoldProject } from './engine/scaffold.js';
|
|
5
5
|
async function main() {
|
|
6
|
+
const config = await runWizard();
|
|
7
|
+
if (!config) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
6
10
|
try {
|
|
7
|
-
await
|
|
11
|
+
await assertPackageManagerAvailable(config.packageManager);
|
|
8
12
|
}
|
|
9
13
|
catch (error) {
|
|
10
14
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -12,10 +16,6 @@ async function main() {
|
|
|
12
16
|
process.exitCode = 1;
|
|
13
17
|
return;
|
|
14
18
|
}
|
|
15
|
-
const config = await runWizard();
|
|
16
|
-
if (!config) {
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
19
|
try {
|
|
20
20
|
await scaffoldProject(config);
|
|
21
21
|
}
|