@aex.is/zero 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -0
- package/dist/config/frameworks.js +95 -0
- package/dist/config/modules.js +69 -0
- package/dist/engine/env.js +10 -0
- package/dist/engine/installers.js +36 -0
- package/dist/engine/scaffold.js +251 -0
- package/{src/engine/templates.ts → dist/engine/templates.js} +1 -8
- package/dist/env/detect.js +36 -0
- package/dist/index.js +39 -0
- package/dist/types.js +1 -0
- package/dist/ui/App.js +75 -0
- package/dist/ui/components/SelectList.js +67 -0
- package/dist/ui/screens/Confirm.js +22 -0
- package/dist/ui/screens/DomainPrompt.js +11 -0
- package/dist/ui/screens/FrameworkSelect.js +12 -0
- package/dist/ui/screens/Intro.js +18 -0
- package/dist/ui/screens/ModuleSelect.js +12 -0
- package/dist/ui/screens/NamePrompt.js +18 -0
- package/package.json +6 -5
- package/src/config/frameworks.ts +0 -110
- package/src/config/modules.ts +0 -82
- package/src/engine/env.ts +0 -12
- package/src/engine/installers.ts +0 -44
- package/src/engine/scaffold.ts +0 -296
- package/src/env/detect.ts +0 -40
- package/src/index.ts +0 -48
- package/src/types.ts +0 -12
- package/src/ui/App.tsx +0 -139
- package/src/ui/components/SelectList.tsx +0 -114
- package/src/ui/screens/Confirm.tsx +0 -62
- package/src/ui/screens/DomainPrompt.tsx +0 -29
- package/src/ui/screens/FrameworkSelect.tsx +0 -34
- package/src/ui/screens/Intro.tsx +0 -31
- package/src/ui/screens/ModuleSelect.tsx +0 -34
- package/src/ui/screens/NamePrompt.tsx +0 -37
package/README.md
CHANGED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export const frameworks = [
|
|
2
|
+
{
|
|
3
|
+
id: 'nextjs',
|
|
4
|
+
label: 'Next.js',
|
|
5
|
+
description: 'React framework with App Router and Tailwind.',
|
|
6
|
+
packages: [
|
|
7
|
+
'class-variance-authority',
|
|
8
|
+
'clsx',
|
|
9
|
+
'lucide-react',
|
|
10
|
+
'tailwind-merge',
|
|
11
|
+
'tailwindcss-animate',
|
|
12
|
+
'@radix-ui/react-slot'
|
|
13
|
+
],
|
|
14
|
+
scaffold: {
|
|
15
|
+
command: 'bunx',
|
|
16
|
+
packageName: 'create-next-app@latest',
|
|
17
|
+
argSets: [
|
|
18
|
+
[
|
|
19
|
+
'--ts',
|
|
20
|
+
'--eslint',
|
|
21
|
+
'--tailwind',
|
|
22
|
+
'--turbo',
|
|
23
|
+
'--app',
|
|
24
|
+
'--no-src-dir',
|
|
25
|
+
'--import-alias',
|
|
26
|
+
'@/*',
|
|
27
|
+
'--use-bun',
|
|
28
|
+
'--skip-install'
|
|
29
|
+
],
|
|
30
|
+
[
|
|
31
|
+
'--ts',
|
|
32
|
+
'--eslint',
|
|
33
|
+
'--tailwind',
|
|
34
|
+
'--turbo',
|
|
35
|
+
'--app',
|
|
36
|
+
'--no-src-dir',
|
|
37
|
+
'--import-alias',
|
|
38
|
+
'@/*',
|
|
39
|
+
'--use-bun'
|
|
40
|
+
],
|
|
41
|
+
[
|
|
42
|
+
'--ts',
|
|
43
|
+
'--eslint',
|
|
44
|
+
'--tailwind',
|
|
45
|
+
'--turbo',
|
|
46
|
+
'--app',
|
|
47
|
+
'--no-src-dir',
|
|
48
|
+
'--import-alias',
|
|
49
|
+
'@/*'
|
|
50
|
+
],
|
|
51
|
+
[
|
|
52
|
+
'--ts',
|
|
53
|
+
'--eslint',
|
|
54
|
+
'--tailwind',
|
|
55
|
+
'--app',
|
|
56
|
+
'--no-src-dir',
|
|
57
|
+
'--import-alias',
|
|
58
|
+
'@/*'
|
|
59
|
+
]
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'expo',
|
|
65
|
+
label: 'Expo (React Native)',
|
|
66
|
+
description: 'Expo app with Router and EAS configuration.',
|
|
67
|
+
packages: [
|
|
68
|
+
'expo-router',
|
|
69
|
+
'tamagui',
|
|
70
|
+
'@tamagui/config',
|
|
71
|
+
'@tamagui/animations-react-native',
|
|
72
|
+
'@tamagui/metro-plugin',
|
|
73
|
+
'@tamagui/babel-plugin',
|
|
74
|
+
'react-native-svg'
|
|
75
|
+
],
|
|
76
|
+
scaffold: {
|
|
77
|
+
command: 'bunx',
|
|
78
|
+
packageName: 'create-expo-app',
|
|
79
|
+
argSets: [
|
|
80
|
+
['--template', 'expo-router', '--yes', '--no-install'],
|
|
81
|
+
['--template', 'expo-router', '--yes'],
|
|
82
|
+
['--yes', '--no-install'],
|
|
83
|
+
['--yes'],
|
|
84
|
+
[]
|
|
85
|
+
]
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
];
|
|
89
|
+
export function getFrameworkDefinition(id) {
|
|
90
|
+
const framework = frameworks.find((item) => item.id === id);
|
|
91
|
+
if (!framework) {
|
|
92
|
+
throw new Error(`Unknown framework: ${id}`);
|
|
93
|
+
}
|
|
94
|
+
return framework;
|
|
95
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export const modules = [
|
|
2
|
+
{
|
|
3
|
+
id: 'neon',
|
|
4
|
+
label: 'Database (Neon)',
|
|
5
|
+
description: 'Serverless Postgres with Neon.',
|
|
6
|
+
envVars: ['DATABASE_URL'],
|
|
7
|
+
packages: {
|
|
8
|
+
nextjs: ['@neondatabase/serverless'],
|
|
9
|
+
expo: ['@neondatabase/serverless']
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: 'clerk',
|
|
14
|
+
label: 'Auth (Clerk)',
|
|
15
|
+
description: 'Authentication with Clerk.',
|
|
16
|
+
envVars: ['CLERK_PUBLISHABLE_KEY', 'CLERK_SECRET_KEY'],
|
|
17
|
+
packages: {
|
|
18
|
+
nextjs: ['@clerk/nextjs'],
|
|
19
|
+
expo: ['@clerk/clerk-expo']
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: 'payload',
|
|
24
|
+
label: 'CMS (Payload)',
|
|
25
|
+
description: 'Headless CMS using Payload.',
|
|
26
|
+
envVars: ['PAYLOAD_SECRET', 'DATABASE_URL'],
|
|
27
|
+
packages: {
|
|
28
|
+
nextjs: ['payload'],
|
|
29
|
+
expo: ['payload']
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'stripe',
|
|
34
|
+
label: 'Payments (Stripe)',
|
|
35
|
+
description: 'Payments via Stripe SDK.',
|
|
36
|
+
envVars: ['STRIPE_SECRET_KEY', 'STRIPE_WEBHOOK_SECRET'],
|
|
37
|
+
packages: {
|
|
38
|
+
nextjs: ['stripe'],
|
|
39
|
+
expo: ['stripe']
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
];
|
|
43
|
+
export function getModuleDefinition(id) {
|
|
44
|
+
const module = modules.find((item) => item.id === id);
|
|
45
|
+
if (!module) {
|
|
46
|
+
throw new Error(`Unknown module: ${id}`);
|
|
47
|
+
}
|
|
48
|
+
return module;
|
|
49
|
+
}
|
|
50
|
+
export function getModulePackages(moduleIds, framework) {
|
|
51
|
+
const packages = new Set();
|
|
52
|
+
for (const id of moduleIds) {
|
|
53
|
+
const module = getModuleDefinition(id);
|
|
54
|
+
for (const pkg of module.packages[framework]) {
|
|
55
|
+
packages.add(pkg);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return Array.from(packages).sort();
|
|
59
|
+
}
|
|
60
|
+
export function getModuleEnvVars(moduleIds) {
|
|
61
|
+
const envVars = new Set();
|
|
62
|
+
for (const id of moduleIds) {
|
|
63
|
+
const module = getModuleDefinition(id);
|
|
64
|
+
for (const envVar of module.envVars) {
|
|
65
|
+
envVars.add(envVar);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return Array.from(envVars).sort();
|
|
69
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import { getModuleEnvVars } from '../config/modules.js';
|
|
4
|
+
export async function writeEnvExample(moduleIds, targetDir) {
|
|
5
|
+
const envVars = getModuleEnvVars(moduleIds);
|
|
6
|
+
const lines = envVars.map((key) => `${key}=`);
|
|
7
|
+
const content = lines.length > 0 ? `${lines.join('\n')}\n` : '';
|
|
8
|
+
const envPath = path.join(targetDir, '.env.example');
|
|
9
|
+
await fs.writeFile(envPath, content, 'utf8');
|
|
10
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { execa } from 'execa';
|
|
2
|
+
import { getModulePackages } from '../config/modules.js';
|
|
3
|
+
const baseEnv = {
|
|
4
|
+
...process.env,
|
|
5
|
+
CI: '1'
|
|
6
|
+
};
|
|
7
|
+
export async function installBaseDependencies(targetDir, extraPackages = []) {
|
|
8
|
+
if (extraPackages.length > 0) {
|
|
9
|
+
const packages = Array.from(new Set(extraPackages)).sort();
|
|
10
|
+
await execa('bun', ['add', ...packages], {
|
|
11
|
+
cwd: targetDir,
|
|
12
|
+
stdio: 'inherit',
|
|
13
|
+
env: baseEnv,
|
|
14
|
+
shell: false
|
|
15
|
+
});
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
await execa('bun', ['install'], {
|
|
19
|
+
cwd: targetDir,
|
|
20
|
+
stdio: 'inherit',
|
|
21
|
+
env: baseEnv,
|
|
22
|
+
shell: false
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
export async function installModulePackages(framework, moduleIds, targetDir) {
|
|
26
|
+
const packages = getModulePackages(moduleIds, framework);
|
|
27
|
+
if (packages.length === 0) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
await execa('bun', ['add', ...packages], {
|
|
31
|
+
cwd: targetDir,
|
|
32
|
+
stdio: 'inherit',
|
|
33
|
+
env: baseEnv,
|
|
34
|
+
shell: false
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { promises as fs } from 'fs';
|
|
3
|
+
import { execa } from 'execa';
|
|
4
|
+
import { getFrameworkDefinition } from '../config/frameworks.js';
|
|
5
|
+
import { installBaseDependencies, installModulePackages } from './installers.js';
|
|
6
|
+
import { writeEnvExample } from './env.js';
|
|
7
|
+
import { nextLayoutTemplate, nextPageTemplate, shadcnUtilsTemplate, componentsJsonTemplate, tamaguiConfigTemplate, metroConfigTemplate, expoLayoutTemplate, expoIndexTemplate } from './templates.js';
|
|
8
|
+
const baseEnv = {
|
|
9
|
+
...process.env,
|
|
10
|
+
CI: '1'
|
|
11
|
+
};
|
|
12
|
+
export async function scaffoldProject(config) {
|
|
13
|
+
const targetDir = path.resolve(process.cwd(), config.appName);
|
|
14
|
+
await ensureEmptyTargetDir(targetDir);
|
|
15
|
+
const framework = getFrameworkDefinition(config.framework);
|
|
16
|
+
console.log(`Scaffolding ${framework.label}...`);
|
|
17
|
+
await runScaffoldCommand(framework.scaffold.command, framework.scaffold.packageName, config.appName, framework.scaffold.argSets);
|
|
18
|
+
console.log('Applying framework templates...');
|
|
19
|
+
if (config.framework === 'nextjs') {
|
|
20
|
+
await applyNextTemplates(targetDir);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
await applyExpoTemplates(targetDir);
|
|
24
|
+
}
|
|
25
|
+
console.log('Installing base dependencies with Bun...');
|
|
26
|
+
await installBaseDependencies(targetDir, framework.packages);
|
|
27
|
+
console.log('Installing module packages...');
|
|
28
|
+
await installModulePackages(config.framework, config.modules, targetDir);
|
|
29
|
+
console.log('Generating .env.example...');
|
|
30
|
+
await writeEnvExample(config.modules, targetDir);
|
|
31
|
+
console.log('Scaffold complete.');
|
|
32
|
+
const cdTarget = config.appName.includes(' ') ? `"${config.appName}"` : config.appName;
|
|
33
|
+
console.log(`\nNext steps:\n 1) cd ${cdTarget}\n 2) bun run dev`);
|
|
34
|
+
}
|
|
35
|
+
async function ensureEmptyTargetDir(targetDir) {
|
|
36
|
+
try {
|
|
37
|
+
const stat = await fs.stat(targetDir);
|
|
38
|
+
if (!stat.isDirectory()) {
|
|
39
|
+
throw new Error('Target path exists and is not a directory.');
|
|
40
|
+
}
|
|
41
|
+
const entries = await fs.readdir(targetDir);
|
|
42
|
+
if (entries.length > 0) {
|
|
43
|
+
throw new Error('Target directory is not empty.');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (isErrnoException(error) && error.code === 'ENOENT') {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function runScaffoldCommand(command, packageName, appName, argSets) {
|
|
54
|
+
const errors = [];
|
|
55
|
+
for (const args of argSets) {
|
|
56
|
+
try {
|
|
57
|
+
await execa(command, [packageName, appName, ...args], {
|
|
58
|
+
stdio: 'inherit',
|
|
59
|
+
env: baseEnv,
|
|
60
|
+
shell: false
|
|
61
|
+
});
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
errors.push(formatError(error));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const message = errors.length > 0
|
|
69
|
+
? `Scaffold failed after ${errors.length} attempts:\n${errors.join('\n')}`
|
|
70
|
+
: 'Scaffold failed for unknown reasons.';
|
|
71
|
+
throw new Error(message);
|
|
72
|
+
}
|
|
73
|
+
async function applyNextTemplates(targetDir) {
|
|
74
|
+
const rootAppDir = path.join(targetDir, 'app');
|
|
75
|
+
const srcAppDir = path.join(targetDir, 'src', 'app');
|
|
76
|
+
const usesSrcDir = await pathExists(srcAppDir);
|
|
77
|
+
const appDir = usesSrcDir ? srcAppDir : rootAppDir;
|
|
78
|
+
const projectSrcBase = usesSrcDir ? path.join(targetDir, 'src') : targetDir;
|
|
79
|
+
await fs.mkdir(appDir, { recursive: true });
|
|
80
|
+
await fs.writeFile(path.join(appDir, 'layout.tsx'), nextLayoutTemplate, 'utf8');
|
|
81
|
+
await fs.writeFile(path.join(appDir, 'page.tsx'), nextPageTemplate, 'utf8');
|
|
82
|
+
await ensureShadcnSetup(targetDir, projectSrcBase, usesSrcDir);
|
|
83
|
+
await ensureNextTurbo(targetDir);
|
|
84
|
+
}
|
|
85
|
+
async function applyExpoTemplates(targetDir) {
|
|
86
|
+
const appDir = path.join(targetDir, 'app');
|
|
87
|
+
await fs.mkdir(appDir, { recursive: true });
|
|
88
|
+
await fs.writeFile(path.join(appDir, '_layout.tsx'), expoLayoutTemplate, 'utf8');
|
|
89
|
+
await fs.writeFile(path.join(appDir, 'index.tsx'), expoIndexTemplate, 'utf8');
|
|
90
|
+
await ensureExpoConfig(targetDir);
|
|
91
|
+
await ensureExpoTamagui(targetDir);
|
|
92
|
+
}
|
|
93
|
+
async function ensureExpoConfig(targetDir) {
|
|
94
|
+
const appJsonPath = path.join(targetDir, 'app.json');
|
|
95
|
+
const appJson = await readJson(appJsonPath, { expo: {} });
|
|
96
|
+
if (!appJson.expo || typeof appJson.expo !== 'object') {
|
|
97
|
+
appJson.expo = {};
|
|
98
|
+
}
|
|
99
|
+
if (!Array.isArray(appJson.expo.platforms)) {
|
|
100
|
+
appJson.expo.platforms = ['ios', 'android', 'macos', 'windows'];
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
const current = appJson.expo.platforms.filter((value) => typeof value === 'string');
|
|
104
|
+
appJson.expo.platforms = mergeUnique(current, ['ios', 'android', 'macos', 'windows']);
|
|
105
|
+
}
|
|
106
|
+
if (!Array.isArray(appJson.expo.plugins)) {
|
|
107
|
+
appJson.expo.plugins = ['expo-router'];
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
const current = appJson.expo.plugins.filter((value) => typeof value === 'string');
|
|
111
|
+
appJson.expo.plugins = mergeUnique(current, ['expo-router']);
|
|
112
|
+
}
|
|
113
|
+
await writeJson(appJsonPath, appJson);
|
|
114
|
+
const packageJsonPath = path.join(targetDir, 'package.json');
|
|
115
|
+
const packageJson = await readJson(packageJsonPath, {});
|
|
116
|
+
packageJson.main = 'expo-router/entry';
|
|
117
|
+
await writeJson(packageJsonPath, packageJson);
|
|
118
|
+
const easPath = path.join(targetDir, 'eas.json');
|
|
119
|
+
const easConfig = {
|
|
120
|
+
cli: {
|
|
121
|
+
version: '>= 8.0.0'
|
|
122
|
+
},
|
|
123
|
+
build: {
|
|
124
|
+
development: {
|
|
125
|
+
developmentClient: true,
|
|
126
|
+
distribution: 'internal'
|
|
127
|
+
},
|
|
128
|
+
preview: {
|
|
129
|
+
distribution: 'internal'
|
|
130
|
+
},
|
|
131
|
+
production: {}
|
|
132
|
+
},
|
|
133
|
+
submit: {}
|
|
134
|
+
};
|
|
135
|
+
await writeJson(easPath, easConfig);
|
|
136
|
+
}
|
|
137
|
+
function mergeUnique(values, additions) {
|
|
138
|
+
const set = new Set(values);
|
|
139
|
+
for (const value of additions) {
|
|
140
|
+
set.add(value);
|
|
141
|
+
}
|
|
142
|
+
return Array.from(set);
|
|
143
|
+
}
|
|
144
|
+
async function readJson(filePath, fallback) {
|
|
145
|
+
try {
|
|
146
|
+
const data = await fs.readFile(filePath, 'utf8');
|
|
147
|
+
return JSON.parse(data);
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
if (isErrnoException(error) && error.code === 'ENOENT') {
|
|
151
|
+
return fallback;
|
|
152
|
+
}
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async function writeJson(filePath, value) {
|
|
157
|
+
const data = JSON.stringify(value, null, 2);
|
|
158
|
+
await fs.writeFile(filePath, `${data}\n`, 'utf8');
|
|
159
|
+
}
|
|
160
|
+
function formatError(error) {
|
|
161
|
+
if (error instanceof Error) {
|
|
162
|
+
return error.message;
|
|
163
|
+
}
|
|
164
|
+
return String(error);
|
|
165
|
+
}
|
|
166
|
+
function isErrnoException(error) {
|
|
167
|
+
return error instanceof Error && 'code' in error;
|
|
168
|
+
}
|
|
169
|
+
async function pathExists(targetPath) {
|
|
170
|
+
try {
|
|
171
|
+
await fs.stat(targetPath);
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
if (isErrnoException(error) && error.code === 'ENOENT') {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
async function ensureNextTurbo(targetDir) {
|
|
182
|
+
const packageJsonPath = path.join(targetDir, 'package.json');
|
|
183
|
+
const packageJson = await readJson(packageJsonPath, {});
|
|
184
|
+
if (!packageJson.scripts || typeof packageJson.scripts !== 'object') {
|
|
185
|
+
packageJson.scripts = {};
|
|
186
|
+
}
|
|
187
|
+
const currentDev = typeof packageJson.scripts.dev === 'string' ? packageJson.scripts.dev : 'next dev';
|
|
188
|
+
if (!currentDev.includes('--turbo')) {
|
|
189
|
+
packageJson.scripts.dev = `${currentDev} --turbo`;
|
|
190
|
+
}
|
|
191
|
+
await writeJson(packageJsonPath, packageJson);
|
|
192
|
+
}
|
|
193
|
+
async function ensureShadcnSetup(targetDir, projectSrcBase, usesSrcDir) {
|
|
194
|
+
const libDir = path.join(projectSrcBase, 'lib');
|
|
195
|
+
await fs.mkdir(libDir, { recursive: true });
|
|
196
|
+
await fs.writeFile(path.join(libDir, 'utils.ts'), shadcnUtilsTemplate, 'utf8');
|
|
197
|
+
const globalsPath = usesSrcDir ? 'src/app/globals.css' : 'app/globals.css';
|
|
198
|
+
const tailwindConfigPath = await detectTailwindConfig(targetDir);
|
|
199
|
+
const componentsJson = componentsJsonTemplate(globalsPath, tailwindConfigPath ?? 'tailwind.config.ts');
|
|
200
|
+
await fs.writeFile(path.join(targetDir, 'components.json'), componentsJson, 'utf8');
|
|
201
|
+
}
|
|
202
|
+
async function detectTailwindConfig(targetDir) {
|
|
203
|
+
const candidates = [
|
|
204
|
+
'tailwind.config.ts',
|
|
205
|
+
'tailwind.config.js',
|
|
206
|
+
'tailwind.config.cjs',
|
|
207
|
+
'tailwind.config.mjs'
|
|
208
|
+
];
|
|
209
|
+
for (const filename of candidates) {
|
|
210
|
+
const fullPath = path.join(targetDir, filename);
|
|
211
|
+
if (await pathExists(fullPath)) {
|
|
212
|
+
return filename;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
async function ensureExpoTamagui(targetDir) {
|
|
218
|
+
const configPath = path.join(targetDir, 'tamagui.config.ts');
|
|
219
|
+
await fs.writeFile(configPath, tamaguiConfigTemplate, 'utf8');
|
|
220
|
+
const metroPath = path.join(targetDir, 'metro.config.js');
|
|
221
|
+
await fs.writeFile(metroPath, metroConfigTemplate, 'utf8');
|
|
222
|
+
await ensureBabelTamagui(targetDir);
|
|
223
|
+
}
|
|
224
|
+
async function ensureBabelTamagui(targetDir) {
|
|
225
|
+
const babelPath = path.join(targetDir, 'babel.config.js');
|
|
226
|
+
let content = '';
|
|
227
|
+
if (await pathExists(babelPath)) {
|
|
228
|
+
content = await fs.readFile(babelPath, 'utf8');
|
|
229
|
+
}
|
|
230
|
+
if (!content) {
|
|
231
|
+
const defaultConfig = `module.exports = function (api) {\n api.cache(true);\n return {\n presets: ['babel-preset-expo'],\n plugins: [\n 'expo-router/babel',\n [\n '@tamagui/babel-plugin',\n {\n config: './tamagui.config.ts',\n components: ['tamagui']\n }\n ]\n ]\n };\n};\n`;
|
|
232
|
+
await fs.writeFile(babelPath, defaultConfig, 'utf8');
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (content.includes('@tamagui/babel-plugin')) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const pluginSnippet = `[\n '@tamagui/babel-plugin',\n {\n config: './tamagui.config.ts',\n components: ['tamagui']\n }\n ]`;
|
|
239
|
+
const pluginsRegex = /plugins:\\s*\\[(.*)\\]/s;
|
|
240
|
+
if (pluginsRegex.test(content)) {
|
|
241
|
+
content = content.replace(pluginsRegex, (match, inner) => {
|
|
242
|
+
const trimmed = inner.trim();
|
|
243
|
+
const updatedInner = trimmed.length > 0 ? `${trimmed},\n ${pluginSnippet}` : pluginSnippet;
|
|
244
|
+
return `plugins: [${updatedInner}]`;
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
content = content.replace(/return\\s*\\{/, (match) => `${match}\n plugins: [${pluginSnippet}],`);
|
|
249
|
+
}
|
|
250
|
+
await fs.writeFile(babelPath, content, 'utf8');
|
|
251
|
+
}
|
|
@@ -17,12 +17,10 @@ export default function RootLayout({
|
|
|
17
17
|
);
|
|
18
18
|
}
|
|
19
19
|
`;
|
|
20
|
-
|
|
21
20
|
export const nextPageTemplate = `export default function Home() {
|
|
22
21
|
return <main />;
|
|
23
22
|
}
|
|
24
23
|
`;
|
|
25
|
-
|
|
26
24
|
export const shadcnUtilsTemplate = `import { clsx, type ClassValue } from 'clsx';
|
|
27
25
|
import { twMerge } from 'tailwind-merge';
|
|
28
26
|
|
|
@@ -30,8 +28,7 @@ export function cn(...inputs: ClassValue[]) {
|
|
|
30
28
|
return twMerge(clsx(inputs));
|
|
31
29
|
}
|
|
32
30
|
`;
|
|
33
|
-
|
|
34
|
-
export const componentsJsonTemplate = (globalsPath: string, tailwindConfigPath: string) => `{
|
|
31
|
+
export const componentsJsonTemplate = (globalsPath, tailwindConfigPath) => `{
|
|
35
32
|
"$schema": "https://ui.shadcn.com/schema.json",
|
|
36
33
|
"style": "default",
|
|
37
34
|
"rsc": true,
|
|
@@ -48,12 +45,10 @@ export const componentsJsonTemplate = (globalsPath: string, tailwindConfigPath:
|
|
|
48
45
|
}
|
|
49
46
|
}
|
|
50
47
|
`;
|
|
51
|
-
|
|
52
48
|
export const tamaguiConfigTemplate = `import { config } from '@tamagui/config/v3';
|
|
53
49
|
|
|
54
50
|
export default config;
|
|
55
51
|
`;
|
|
56
|
-
|
|
57
52
|
export const metroConfigTemplate = `const { getDefaultConfig } = require('expo/metro-config');
|
|
58
53
|
const { withTamagui } = require('@tamagui/metro-plugin');
|
|
59
54
|
|
|
@@ -65,7 +60,6 @@ module.exports = withTamagui(config, {
|
|
|
65
60
|
outputCSS: './tamagui-web.css'
|
|
66
61
|
});
|
|
67
62
|
`;
|
|
68
|
-
|
|
69
63
|
export const expoLayoutTemplate = `import { Stack } from 'expo-router';
|
|
70
64
|
import { TamaguiProvider } from 'tamagui';
|
|
71
65
|
import config from '../tamagui.config';
|
|
@@ -78,7 +72,6 @@ export default function RootLayout() {
|
|
|
78
72
|
);
|
|
79
73
|
}
|
|
80
74
|
`;
|
|
81
|
-
|
|
82
75
|
export const expoIndexTemplate = `import { Button, H1, YStack } from 'tamagui';
|
|
83
76
|
|
|
84
77
|
export default function Home() {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import which from 'which';
|
|
2
|
+
export function detectPlatform() {
|
|
3
|
+
switch (process.platform) {
|
|
4
|
+
case 'darwin':
|
|
5
|
+
return 'macos';
|
|
6
|
+
case 'win32':
|
|
7
|
+
return 'windows';
|
|
8
|
+
case 'linux':
|
|
9
|
+
return 'linux';
|
|
10
|
+
default:
|
|
11
|
+
return 'linux';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function detectShell(platform = detectPlatform()) {
|
|
15
|
+
return platform === 'windows' ? 'powershell' : 'posix';
|
|
16
|
+
}
|
|
17
|
+
export async function isBunAvailable() {
|
|
18
|
+
try {
|
|
19
|
+
await which('bun');
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function assertBunAvailable() {
|
|
27
|
+
const available = await isBunAvailable();
|
|
28
|
+
if (!available) {
|
|
29
|
+
const platform = detectPlatform();
|
|
30
|
+
const shell = detectShell(platform);
|
|
31
|
+
const hint = platform === 'windows'
|
|
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}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { render } from 'ink';
|
|
4
|
+
import { App } from './ui/App.js';
|
|
5
|
+
import { assertBunAvailable } from './env/detect.js';
|
|
6
|
+
import { scaffoldProject } from './engine/scaffold.js';
|
|
7
|
+
async function main() {
|
|
8
|
+
try {
|
|
9
|
+
await assertBunAvailable();
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
13
|
+
console.error(message);
|
|
14
|
+
process.exitCode = 1;
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
let config = null;
|
|
18
|
+
const { waitUntilExit } = render(_jsx(App, { onComplete: (result) => {
|
|
19
|
+
config = result;
|
|
20
|
+
} }));
|
|
21
|
+
await waitUntilExit();
|
|
22
|
+
if (!config) {
|
|
23
|
+
console.log('Cancelled.');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
await scaffoldProject(config);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
31
|
+
console.error(`Error: ${message}`);
|
|
32
|
+
process.exitCode = 1;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
main().catch((error) => {
|
|
36
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
37
|
+
console.error(`Error: ${message}`);
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
});
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/ui/App.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useRef, useState } from 'react';
|
|
3
|
+
import { Box, Text, useApp } from 'ink';
|
|
4
|
+
import { Intro } from './screens/Intro.js';
|
|
5
|
+
import { NamePrompt } from './screens/NamePrompt.js';
|
|
6
|
+
import { DomainPrompt } from './screens/DomainPrompt.js';
|
|
7
|
+
import { FrameworkSelect } from './screens/FrameworkSelect.js';
|
|
8
|
+
import { ModuleSelect } from './screens/ModuleSelect.js';
|
|
9
|
+
import { Confirm } from './screens/Confirm.js';
|
|
10
|
+
export function App({ onComplete }) {
|
|
11
|
+
const { exit } = useApp();
|
|
12
|
+
const [step, setStep] = useState('intro');
|
|
13
|
+
const [resumeStep, setResumeStep] = useState(null);
|
|
14
|
+
const [name, setName] = useState('');
|
|
15
|
+
const [domain, setDomain] = useState('');
|
|
16
|
+
const [framework, setFramework] = useState('nextjs');
|
|
17
|
+
const [modules, setModules] = useState([]);
|
|
18
|
+
const completedRef = useRef(false);
|
|
19
|
+
const finish = useCallback((config) => {
|
|
20
|
+
if (completedRef.current) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
completedRef.current = true;
|
|
24
|
+
onComplete(config);
|
|
25
|
+
exit();
|
|
26
|
+
}, [exit, onComplete]);
|
|
27
|
+
const goNext = useCallback((next) => {
|
|
28
|
+
if (resumeStep) {
|
|
29
|
+
setStep(resumeStep);
|
|
30
|
+
setResumeStep(null);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
setStep(next);
|
|
34
|
+
}, [resumeStep]);
|
|
35
|
+
const handleConfirmAction = useCallback((action) => {
|
|
36
|
+
switch (action) {
|
|
37
|
+
case 'continue':
|
|
38
|
+
finish({
|
|
39
|
+
appName: name,
|
|
40
|
+
domain,
|
|
41
|
+
framework,
|
|
42
|
+
modules
|
|
43
|
+
});
|
|
44
|
+
return;
|
|
45
|
+
case 'cancel':
|
|
46
|
+
finish(null);
|
|
47
|
+
return;
|
|
48
|
+
case 'edit-name':
|
|
49
|
+
setResumeStep('confirm');
|
|
50
|
+
setStep('name');
|
|
51
|
+
return;
|
|
52
|
+
case 'edit-domain':
|
|
53
|
+
setResumeStep('confirm');
|
|
54
|
+
setStep('domain');
|
|
55
|
+
return;
|
|
56
|
+
case 'edit-framework':
|
|
57
|
+
setResumeStep('confirm');
|
|
58
|
+
setStep('framework');
|
|
59
|
+
return;
|
|
60
|
+
case 'edit-modules':
|
|
61
|
+
setResumeStep('confirm');
|
|
62
|
+
setStep('modules');
|
|
63
|
+
return;
|
|
64
|
+
default:
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
}, [domain, finish, framework, modules, name]);
|
|
68
|
+
return (_jsxs(Box, { flexDirection: "column", children: [step === 'intro' ? null : (_jsx(Text, { color: "cyan", children: "Aexis Zero" })), step === 'intro' ? (_jsx(Intro, { onContinue: () => setStep('name') })) : null, step === 'name' ? (_jsx(NamePrompt, { initialValue: name, onSubmit: (value) => {
|
|
69
|
+
setName(value);
|
|
70
|
+
goNext('domain');
|
|
71
|
+
} })) : null, step === 'domain' ? (_jsx(DomainPrompt, { initialValue: domain, onSubmit: (value) => {
|
|
72
|
+
setDomain(value);
|
|
73
|
+
goNext('framework');
|
|
74
|
+
} })) : null, step === 'framework' ? (_jsx(FrameworkSelect, { value: framework, onChange: setFramework, onConfirm: () => goNext('modules') })) : null, step === 'modules' ? (_jsx(ModuleSelect, { value: modules, onChange: setModules, onConfirm: () => goNext('confirm') })) : null, step === 'confirm' ? (_jsx(Confirm, { name: name, domain: domain, framework: framework, modules: modules, onAction: handleConfirmAction })) : null] }));
|
|
75
|
+
}
|