@aex.is/zero 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/prompts.js +163 -0
- package/dist/config/frameworks.js +2 -0
- package/dist/config/modules.js +56 -5
- package/dist/engine/scaffold.js +98 -96
- package/dist/engine/templates.js +603 -39
- package/dist/index.js +2 -9
- package/package.json +3 -8
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import * as p from '@clack/prompts';
|
|
2
|
+
import { frameworks } from '../config/frameworks.js';
|
|
3
|
+
import { modules } from '../config/modules.js';
|
|
4
|
+
const introArt = [
|
|
5
|
+
' _____',
|
|
6
|
+
' / ___ \\\\',
|
|
7
|
+
' / / _ \\\\ \\\\',
|
|
8
|
+
' | |/ /| |',
|
|
9
|
+
' \\\\ \\\\_/ / /',
|
|
10
|
+
' \\\\___/_/'
|
|
11
|
+
].join('\n');
|
|
12
|
+
export async function runWizard() {
|
|
13
|
+
p.intro(`${introArt}\nAexis Zero`);
|
|
14
|
+
let directory = '.';
|
|
15
|
+
let appName = '';
|
|
16
|
+
let domain = '';
|
|
17
|
+
let framework = 'nextjs';
|
|
18
|
+
let selectedModules = [];
|
|
19
|
+
let step = 'directory';
|
|
20
|
+
while (true) {
|
|
21
|
+
if (step === 'directory') {
|
|
22
|
+
const value = await p.text({
|
|
23
|
+
message: 'Project directory',
|
|
24
|
+
placeholder: '.',
|
|
25
|
+
validate: (input) => {
|
|
26
|
+
if (typeof input !== 'string') {
|
|
27
|
+
return 'Enter a directory.';
|
|
28
|
+
}
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
if (isCancelled(value))
|
|
33
|
+
return null;
|
|
34
|
+
const normalized = String(value).trim();
|
|
35
|
+
directory = normalized.length === 0 ? '.' : normalized;
|
|
36
|
+
step = 'name';
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (step === 'name') {
|
|
40
|
+
const value = await p.text({
|
|
41
|
+
message: 'App name',
|
|
42
|
+
placeholder: 'my-app',
|
|
43
|
+
validate: (input) => {
|
|
44
|
+
if (typeof input !== 'string' || input.trim().length === 0) {
|
|
45
|
+
return 'App name is required.';
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
if (isCancelled(value))
|
|
51
|
+
return null;
|
|
52
|
+
appName = String(value).trim();
|
|
53
|
+
step = 'domain';
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (step === 'domain') {
|
|
57
|
+
const value = await p.text({
|
|
58
|
+
message: 'Domain (optional)',
|
|
59
|
+
placeholder: 'example.com'
|
|
60
|
+
});
|
|
61
|
+
if (isCancelled(value))
|
|
62
|
+
return null;
|
|
63
|
+
domain = String(value).trim();
|
|
64
|
+
step = 'framework';
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (step === 'framework') {
|
|
68
|
+
const value = await p.select({
|
|
69
|
+
message: 'Framework',
|
|
70
|
+
options: frameworks.map((item) => ({
|
|
71
|
+
value: item.id,
|
|
72
|
+
label: item.label,
|
|
73
|
+
hint: item.description
|
|
74
|
+
}))
|
|
75
|
+
});
|
|
76
|
+
if (isCancelled(value))
|
|
77
|
+
return null;
|
|
78
|
+
framework = value;
|
|
79
|
+
step = 'modules';
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (step === 'modules') {
|
|
83
|
+
const value = await p.multiselect({
|
|
84
|
+
message: 'Modules',
|
|
85
|
+
options: modules.map((item) => ({
|
|
86
|
+
value: item.id,
|
|
87
|
+
label: item.label,
|
|
88
|
+
hint: item.description
|
|
89
|
+
})),
|
|
90
|
+
required: false
|
|
91
|
+
});
|
|
92
|
+
if (isCancelled(value))
|
|
93
|
+
return null;
|
|
94
|
+
selectedModules = value;
|
|
95
|
+
step = 'confirm';
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (step === 'confirm') {
|
|
99
|
+
const frameworkLabel = frameworks.find((item) => item.id === framework)?.label ?? framework;
|
|
100
|
+
const moduleLabels = selectedModules
|
|
101
|
+
.map((id) => modules.find((item) => item.id === id)?.label ?? id)
|
|
102
|
+
.join(', ');
|
|
103
|
+
p.note([
|
|
104
|
+
`Directory: ${directory}`,
|
|
105
|
+
`App name: ${appName}`,
|
|
106
|
+
`Domain: ${domain || 'None'}`,
|
|
107
|
+
`Framework: ${frameworkLabel}`,
|
|
108
|
+
`Modules: ${moduleLabels || 'None'}`
|
|
109
|
+
].join('\n'), 'Review');
|
|
110
|
+
const action = await p.select({
|
|
111
|
+
message: 'Next step',
|
|
112
|
+
options: [
|
|
113
|
+
{ value: 'continue', label: 'Continue' },
|
|
114
|
+
{ value: 'edit-directory', label: 'Edit directory' },
|
|
115
|
+
{ value: 'edit-name', label: 'Edit name' },
|
|
116
|
+
{ value: 'edit-domain', label: 'Edit domain' },
|
|
117
|
+
{ value: 'edit-framework', label: 'Edit framework' },
|
|
118
|
+
{ value: 'edit-modules', label: 'Edit modules' },
|
|
119
|
+
{ value: 'cancel', label: 'Cancel' }
|
|
120
|
+
]
|
|
121
|
+
});
|
|
122
|
+
if (isCancelled(action))
|
|
123
|
+
return null;
|
|
124
|
+
switch (action) {
|
|
125
|
+
case 'continue':
|
|
126
|
+
return {
|
|
127
|
+
directory,
|
|
128
|
+
appName,
|
|
129
|
+
domain,
|
|
130
|
+
framework,
|
|
131
|
+
modules: selectedModules
|
|
132
|
+
};
|
|
133
|
+
case 'edit-directory':
|
|
134
|
+
step = 'directory';
|
|
135
|
+
continue;
|
|
136
|
+
case 'edit-name':
|
|
137
|
+
step = 'name';
|
|
138
|
+
continue;
|
|
139
|
+
case 'edit-domain':
|
|
140
|
+
step = 'domain';
|
|
141
|
+
continue;
|
|
142
|
+
case 'edit-framework':
|
|
143
|
+
step = 'framework';
|
|
144
|
+
continue;
|
|
145
|
+
case 'edit-modules':
|
|
146
|
+
step = 'modules';
|
|
147
|
+
continue;
|
|
148
|
+
case 'cancel':
|
|
149
|
+
p.cancel('Cancelled.');
|
|
150
|
+
return null;
|
|
151
|
+
default:
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function isCancelled(value) {
|
|
158
|
+
if (p.isCancel(value)) {
|
|
159
|
+
p.cancel('Cancelled.');
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
return false;
|
|
163
|
+
}
|
package/dist/config/modules.js
CHANGED
|
@@ -3,7 +3,13 @@ export const modules = [
|
|
|
3
3
|
id: 'neon',
|
|
4
4
|
label: 'Database (Neon)',
|
|
5
5
|
description: 'Serverless Postgres with Neon.',
|
|
6
|
-
envVars: [
|
|
6
|
+
envVars: [
|
|
7
|
+
{
|
|
8
|
+
key: 'DATABASE_URL',
|
|
9
|
+
description: 'Neon connection string',
|
|
10
|
+
url: 'https://neon.com/docs/get-started/connect-neon'
|
|
11
|
+
}
|
|
12
|
+
],
|
|
7
13
|
packages: {
|
|
8
14
|
nextjs: ['@neondatabase/serverless'],
|
|
9
15
|
expo: ['@neondatabase/serverless']
|
|
@@ -13,7 +19,18 @@ export const modules = [
|
|
|
13
19
|
id: 'clerk',
|
|
14
20
|
label: 'Auth (Clerk)',
|
|
15
21
|
description: 'Authentication with Clerk.',
|
|
16
|
-
envVars: [
|
|
22
|
+
envVars: [
|
|
23
|
+
{
|
|
24
|
+
key: 'CLERK_PUBLISHABLE_KEY',
|
|
25
|
+
description: 'Clerk publishable key',
|
|
26
|
+
url: 'https://dashboard.clerk.com'
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
key: 'CLERK_SECRET_KEY',
|
|
30
|
+
description: 'Clerk secret key',
|
|
31
|
+
url: 'https://dashboard.clerk.com'
|
|
32
|
+
}
|
|
33
|
+
],
|
|
17
34
|
packages: {
|
|
18
35
|
nextjs: ['@clerk/nextjs'],
|
|
19
36
|
expo: ['@clerk/clerk-expo']
|
|
@@ -23,7 +40,18 @@ export const modules = [
|
|
|
23
40
|
id: 'payload',
|
|
24
41
|
label: 'CMS (Payload)',
|
|
25
42
|
description: 'Headless CMS using Payload.',
|
|
26
|
-
envVars: [
|
|
43
|
+
envVars: [
|
|
44
|
+
{
|
|
45
|
+
key: 'PAYLOAD_SECRET',
|
|
46
|
+
description: 'Payload secret (generate a long random string)',
|
|
47
|
+
url: 'https://payloadcms.com/docs'
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
key: 'DATABASE_URL',
|
|
51
|
+
description: 'Database connection string',
|
|
52
|
+
url: 'https://payloadcms.com/docs'
|
|
53
|
+
}
|
|
54
|
+
],
|
|
27
55
|
packages: {
|
|
28
56
|
nextjs: ['payload'],
|
|
29
57
|
expo: ['payload']
|
|
@@ -33,7 +61,18 @@ export const modules = [
|
|
|
33
61
|
id: 'stripe',
|
|
34
62
|
label: 'Payments (Stripe)',
|
|
35
63
|
description: 'Payments via Stripe SDK.',
|
|
36
|
-
envVars: [
|
|
64
|
+
envVars: [
|
|
65
|
+
{
|
|
66
|
+
key: 'STRIPE_SECRET_KEY',
|
|
67
|
+
description: 'Stripe secret key',
|
|
68
|
+
url: 'https://dashboard.stripe.com/apikeys'
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
key: 'STRIPE_WEBHOOK_SECRET',
|
|
72
|
+
description: 'Stripe webhook signing secret',
|
|
73
|
+
url: 'https://dashboard.stripe.com/webhooks'
|
|
74
|
+
}
|
|
75
|
+
],
|
|
37
76
|
packages: {
|
|
38
77
|
nextjs: ['stripe'],
|
|
39
78
|
expo: ['stripe']
|
|
@@ -62,8 +101,20 @@ export function getModuleEnvVars(moduleIds) {
|
|
|
62
101
|
for (const id of moduleIds) {
|
|
63
102
|
const module = getModuleDefinition(id);
|
|
64
103
|
for (const envVar of module.envVars) {
|
|
65
|
-
envVars.add(envVar);
|
|
104
|
+
envVars.add(envVar.key);
|
|
66
105
|
}
|
|
67
106
|
}
|
|
68
107
|
return Array.from(envVars).sort();
|
|
69
108
|
}
|
|
109
|
+
export function getModuleEnvHelp(moduleIds) {
|
|
110
|
+
const map = new Map();
|
|
111
|
+
for (const id of moduleIds) {
|
|
112
|
+
const module = getModuleDefinition(id);
|
|
113
|
+
for (const envVar of module.envVars) {
|
|
114
|
+
if (!map.has(envVar.key)) {
|
|
115
|
+
map.set(envVar.key, envVar);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return Array.from(map.values());
|
|
120
|
+
}
|
package/dist/engine/scaffold.js
CHANGED
|
@@ -2,25 +2,27 @@ import path from 'path';
|
|
|
2
2
|
import { promises as fs } from 'fs';
|
|
3
3
|
import { execa } from 'execa';
|
|
4
4
|
import { getFrameworkDefinition } from '../config/frameworks.js';
|
|
5
|
+
import { getModuleEnvHelp } from '../config/modules.js';
|
|
5
6
|
import { installBaseDependencies, installModulePackages } from './installers.js';
|
|
6
7
|
import { writeEnvExample } from './env.js';
|
|
7
|
-
import {
|
|
8
|
+
import { buildNextTemplateFiles, buildExpoTemplateFiles, componentsJsonTemplate } from './templates.js';
|
|
8
9
|
const baseEnv = {
|
|
9
10
|
...process.env,
|
|
10
11
|
CI: '1'
|
|
11
12
|
};
|
|
12
13
|
export async function scaffoldProject(config) {
|
|
13
|
-
const
|
|
14
|
+
const directoryInput = config.directory.trim().length === 0 ? '.' : config.directory.trim();
|
|
15
|
+
const targetDir = path.resolve(process.cwd(), directoryInput);
|
|
14
16
|
await ensureEmptyTargetDir(targetDir);
|
|
15
17
|
const framework = getFrameworkDefinition(config.framework);
|
|
16
18
|
console.log(`Scaffolding ${framework.label}...`);
|
|
17
|
-
await runScaffoldCommand(framework.scaffold.command, framework.scaffold.packageName,
|
|
19
|
+
await runScaffoldCommand(framework.scaffold.command, framework.scaffold.packageName, directoryInput, framework.scaffold.argSets);
|
|
18
20
|
console.log('Applying framework templates...');
|
|
19
21
|
if (config.framework === 'nextjs') {
|
|
20
|
-
await applyNextTemplates(targetDir);
|
|
22
|
+
await applyNextTemplates(config, targetDir);
|
|
21
23
|
}
|
|
22
24
|
else {
|
|
23
|
-
await applyExpoTemplates(targetDir);
|
|
25
|
+
await applyExpoTemplates(config, targetDir);
|
|
24
26
|
}
|
|
25
27
|
console.log('Installing base dependencies with Bun...');
|
|
26
28
|
await installBaseDependencies(targetDir, framework.packages);
|
|
@@ -29,7 +31,7 @@ export async function scaffoldProject(config) {
|
|
|
29
31
|
console.log('Generating .env.example...');
|
|
30
32
|
await writeEnvExample(config.modules, targetDir);
|
|
31
33
|
console.log('Scaffold complete.');
|
|
32
|
-
const cdTarget =
|
|
34
|
+
const cdTarget = directoryInput === '.' ? '.' : directoryInput.includes(' ') ? `"${directoryInput}"` : directoryInput;
|
|
33
35
|
console.log(`\nNext steps:\n 1) cd ${cdTarget}\n 2) bun run dev`);
|
|
34
36
|
}
|
|
35
37
|
async function ensureEmptyTargetDir(targetDir) {
|
|
@@ -50,11 +52,12 @@ async function ensureEmptyTargetDir(targetDir) {
|
|
|
50
52
|
throw error;
|
|
51
53
|
}
|
|
52
54
|
}
|
|
53
|
-
async function runScaffoldCommand(command, packageName,
|
|
55
|
+
async function runScaffoldCommand(command, packageName, directoryInput, argSets) {
|
|
54
56
|
const errors = [];
|
|
57
|
+
const targetArg = directoryInput === '.' ? '.' : directoryInput;
|
|
55
58
|
for (const args of argSets) {
|
|
56
59
|
try {
|
|
57
|
-
await execa(command, [packageName,
|
|
60
|
+
await execa(command, [packageName, targetArg, ...args], {
|
|
58
61
|
stdio: 'inherit',
|
|
59
62
|
env: baseEnv,
|
|
60
63
|
shell: false
|
|
@@ -70,32 +73,45 @@ async function runScaffoldCommand(command, packageName, appName, argSets) {
|
|
|
70
73
|
: 'Scaffold failed for unknown reasons.';
|
|
71
74
|
throw new Error(message);
|
|
72
75
|
}
|
|
73
|
-
async function applyNextTemplates(targetDir) {
|
|
74
|
-
const rootAppDir = path.join(targetDir, 'app');
|
|
76
|
+
async function applyNextTemplates(config, targetDir) {
|
|
75
77
|
const srcAppDir = path.join(targetDir, 'src', 'app');
|
|
76
78
|
const usesSrcDir = await pathExists(srcAppDir);
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
79
|
+
const basePath = usesSrcDir ? 'src' : '';
|
|
80
|
+
const envHelp = getModuleEnvHelp(config.modules);
|
|
81
|
+
const files = buildNextTemplateFiles({
|
|
82
|
+
appName: config.appName,
|
|
83
|
+
domain: config.domain,
|
|
84
|
+
envVars: envHelp,
|
|
85
|
+
basePath
|
|
86
|
+
});
|
|
87
|
+
await writeTemplateFiles(targetDir, files);
|
|
88
|
+
const globalsPath = usesSrcDir ? 'src/app/globals.css' : 'app/globals.css';
|
|
89
|
+
const tailwindConfig = await detectTailwindConfig(targetDir);
|
|
90
|
+
const componentsJson = componentsJsonTemplate(globalsPath, tailwindConfig ?? 'tailwind.config.ts');
|
|
91
|
+
await fs.writeFile(path.join(targetDir, 'components.json'), componentsJson, 'utf8');
|
|
83
92
|
await ensureNextTurbo(targetDir);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
93
|
+
await ensurePackageName(targetDir, config.appName);
|
|
94
|
+
}
|
|
95
|
+
async function applyExpoTemplates(config, targetDir) {
|
|
96
|
+
const envHelp = getModuleEnvHelp(config.modules);
|
|
97
|
+
const files = buildExpoTemplateFiles({
|
|
98
|
+
appName: config.appName,
|
|
99
|
+
domain: config.domain,
|
|
100
|
+
envVars: envHelp,
|
|
101
|
+
basePath: ''
|
|
102
|
+
});
|
|
103
|
+
await writeTemplateFiles(targetDir, files);
|
|
104
|
+
await ensureExpoConfig(targetDir, config.appName);
|
|
105
|
+
await ensurePackageName(targetDir, config.appName);
|
|
106
|
+
}
|
|
107
|
+
async function ensureExpoConfig(targetDir, appName) {
|
|
94
108
|
const appJsonPath = path.join(targetDir, 'app.json');
|
|
95
109
|
const appJson = await readJson(appJsonPath, { expo: {} });
|
|
96
110
|
if (!appJson.expo || typeof appJson.expo !== 'object') {
|
|
97
111
|
appJson.expo = {};
|
|
98
112
|
}
|
|
113
|
+
appJson.expo.name = appName;
|
|
114
|
+
appJson.expo.slug = toSlug(appName);
|
|
99
115
|
if (!Array.isArray(appJson.expo.platforms)) {
|
|
100
116
|
appJson.expo.platforms = ['ios', 'android', 'macos', 'windows'];
|
|
101
117
|
}
|
|
@@ -134,6 +150,31 @@ async function ensureExpoConfig(targetDir) {
|
|
|
134
150
|
};
|
|
135
151
|
await writeJson(easPath, easConfig);
|
|
136
152
|
}
|
|
153
|
+
async function ensureNextTurbo(targetDir) {
|
|
154
|
+
const packageJsonPath = path.join(targetDir, 'package.json');
|
|
155
|
+
const packageJson = await readJson(packageJsonPath, {});
|
|
156
|
+
if (!packageJson.scripts || typeof packageJson.scripts !== 'object') {
|
|
157
|
+
packageJson.scripts = {};
|
|
158
|
+
}
|
|
159
|
+
const currentDev = typeof packageJson.scripts.dev === 'string' ? packageJson.scripts.dev : 'next dev';
|
|
160
|
+
if (!currentDev.includes('--turbo')) {
|
|
161
|
+
packageJson.scripts.dev = `${currentDev} --turbo`;
|
|
162
|
+
}
|
|
163
|
+
await writeJson(packageJsonPath, packageJson);
|
|
164
|
+
}
|
|
165
|
+
async function ensurePackageName(targetDir, appName) {
|
|
166
|
+
const packageJsonPath = path.join(targetDir, 'package.json');
|
|
167
|
+
const packageJson = await readJson(packageJsonPath, {});
|
|
168
|
+
packageJson.name = toPackageName(appName);
|
|
169
|
+
await writeJson(packageJsonPath, packageJson);
|
|
170
|
+
}
|
|
171
|
+
async function writeTemplateFiles(targetDir, files) {
|
|
172
|
+
for (const file of files) {
|
|
173
|
+
const fullPath = path.join(targetDir, ...file.path.split('/'));
|
|
174
|
+
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
175
|
+
await fs.writeFile(fullPath, file.content, 'utf8');
|
|
176
|
+
}
|
|
177
|
+
}
|
|
137
178
|
function mergeUnique(values, additions) {
|
|
138
179
|
const set = new Set(values);
|
|
139
180
|
for (const value of additions) {
|
|
@@ -141,6 +182,21 @@ function mergeUnique(values, additions) {
|
|
|
141
182
|
}
|
|
142
183
|
return Array.from(set);
|
|
143
184
|
}
|
|
185
|
+
async function detectTailwindConfig(targetDir) {
|
|
186
|
+
const candidates = [
|
|
187
|
+
'tailwind.config.ts',
|
|
188
|
+
'tailwind.config.js',
|
|
189
|
+
'tailwind.config.cjs',
|
|
190
|
+
'tailwind.config.mjs'
|
|
191
|
+
];
|
|
192
|
+
for (const filename of candidates) {
|
|
193
|
+
const fullPath = path.join(targetDir, filename);
|
|
194
|
+
if (await pathExists(fullPath)) {
|
|
195
|
+
return filename;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
144
200
|
async function readJson(filePath, fallback) {
|
|
145
201
|
try {
|
|
146
202
|
const data = await fs.readFile(filePath, 'utf8');
|
|
@@ -178,74 +234,20 @@ async function pathExists(targetPath) {
|
|
|
178
234
|
throw error;
|
|
179
235
|
}
|
|
180
236
|
}
|
|
181
|
-
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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');
|
|
237
|
+
function toPackageName(name) {
|
|
238
|
+
const cleaned = name
|
|
239
|
+
.trim()
|
|
240
|
+
.toLowerCase()
|
|
241
|
+
.replace(/[^a-z0-9-._]/g, '-')
|
|
242
|
+
.replace(/-+/g, '-')
|
|
243
|
+
.replace(/^[-_.]+|[-_.]+$/g, '');
|
|
244
|
+
return cleaned || 'aexis-zero-app';
|
|
245
|
+
}
|
|
246
|
+
function toSlug(name) {
|
|
247
|
+
return name
|
|
248
|
+
.trim()
|
|
249
|
+
.toLowerCase()
|
|
250
|
+
.replace(/[^a-z0-9-]/g, '-')
|
|
251
|
+
.replace(/-+/g, '-')
|
|
252
|
+
.replace(/^-|-$/g, '') || 'aexis-zero-app';
|
|
251
253
|
}
|
package/dist/engine/templates.js
CHANGED
|
@@ -1,7 +1,125 @@
|
|
|
1
|
-
export
|
|
1
|
+
export function componentsJsonTemplate(globalsPath, tailwindConfigPath) {
|
|
2
|
+
return `{
|
|
3
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
4
|
+
"style": "default",
|
|
5
|
+
"rsc": true,
|
|
6
|
+
"tsx": true,
|
|
7
|
+
"tailwind": {
|
|
8
|
+
"config": "${tailwindConfigPath}",
|
|
9
|
+
"css": "${globalsPath}",
|
|
10
|
+
"baseColor": "slate",
|
|
11
|
+
"cssVariables": true
|
|
12
|
+
},
|
|
13
|
+
"aliases": {
|
|
14
|
+
"components": "@/components",
|
|
15
|
+
"utils": "@/lib/utils"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
`;
|
|
19
|
+
}
|
|
20
|
+
export function buildNextTemplateFiles(data) {
|
|
21
|
+
const base = data.basePath ? `${data.basePath}/` : '';
|
|
22
|
+
const envList = renderNextEnvList(data.envVars);
|
|
23
|
+
return [
|
|
24
|
+
{
|
|
25
|
+
path: `${base}app/layout.tsx`,
|
|
26
|
+
content: nextLayoutTemplate(data.appName, data.domain)
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
path: `${base}app/page.tsx`,
|
|
30
|
+
content: nextHomeTemplate(data.appName, data.domain, envList)
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
path: `${base}app/about/page.tsx`,
|
|
34
|
+
content: nextRouteTemplate('About', 'A concise overview of your project.')
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
path: `${base}app/guide/page.tsx`,
|
|
38
|
+
content: nextRouteTemplate('Guide', 'Three routes are ready. Customize and ship.')
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
path: `${base}app/globals.css`,
|
|
42
|
+
content: nextGlobalsCss()
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
path: `${base}components/site-header.tsx`,
|
|
46
|
+
content: nextHeaderTemplate(data.appName)
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
path: `${base}components/site-footer.tsx`,
|
|
50
|
+
content: nextFooterTemplate(data.domain)
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
path: `${base}components/env-list.tsx`,
|
|
54
|
+
content: nextEnvListTemplate(envList)
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
path: `${base}lib/utils.ts`,
|
|
58
|
+
content: nextUtilsTemplate()
|
|
59
|
+
}
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
export function buildExpoTemplateFiles(data) {
|
|
63
|
+
const envItems = renderExpoEnvItems(data.envVars);
|
|
64
|
+
return [
|
|
65
|
+
{
|
|
66
|
+
path: 'app/_layout.tsx',
|
|
67
|
+
content: expoLayoutTemplate()
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
path: 'app/index.tsx',
|
|
71
|
+
content: expoHomeTemplate(data.appName, data.domain, envItems)
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
path: 'app/about.tsx',
|
|
75
|
+
content: expoRouteTemplate('About', 'A concise overview of your project.')
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
path: 'app/guide.tsx',
|
|
79
|
+
content: expoRouteTemplate('Guide', 'Three routes are ready. Customize and ship.')
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
path: 'components/theme.ts',
|
|
83
|
+
content: expoThemeTemplate()
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
path: 'components/site-header.tsx',
|
|
87
|
+
content: expoHeaderTemplate(data.appName)
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
path: 'components/site-footer.tsx',
|
|
91
|
+
content: expoFooterTemplate(data.domain)
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
path: 'components/env-list.tsx',
|
|
95
|
+
content: expoEnvListTemplate(envItems)
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
path: 'components/page-shell.tsx',
|
|
99
|
+
content: expoPageShellTemplate()
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
path: 'tamagui.config.ts',
|
|
103
|
+
content: tamaguiConfigTemplate()
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
path: 'metro.config.js',
|
|
107
|
+
content: metroConfigTemplate()
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
path: 'babel.config.js',
|
|
111
|
+
content: babelConfigTemplate()
|
|
112
|
+
}
|
|
113
|
+
];
|
|
114
|
+
}
|
|
115
|
+
function nextLayoutTemplate(appName, domain) {
|
|
116
|
+
return `import type { ReactNode } from 'react';
|
|
117
|
+
import './globals.css';
|
|
118
|
+
import { SiteHeader } from '@/components/site-header';
|
|
119
|
+
import { SiteFooter } from '@/components/site-footer';
|
|
2
120
|
|
|
3
121
|
export const metadata = {
|
|
4
|
-
title: '
|
|
122
|
+
title: '${escapeTemplate(appName)}',
|
|
5
123
|
description: 'Scaffolded by Aexis Zero.'
|
|
6
124
|
};
|
|
7
125
|
|
|
@@ -12,74 +130,520 @@ export default function RootLayout({
|
|
|
12
130
|
}) {
|
|
13
131
|
return (
|
|
14
132
|
<html lang="en">
|
|
15
|
-
<body>
|
|
133
|
+
<body className="min-h-screen bg-[var(--bg)] text-[var(--fg)]">
|
|
134
|
+
<div className="flex min-h-screen flex-col">
|
|
135
|
+
<SiteHeader appName="${escapeTemplate(appName)}" />
|
|
136
|
+
<main className="flex-1">{children}</main>
|
|
137
|
+
<SiteFooter domain="${escapeTemplate(domain)}" />
|
|
138
|
+
</div>
|
|
139
|
+
</body>
|
|
16
140
|
</html>
|
|
17
141
|
);
|
|
18
142
|
}
|
|
19
143
|
`;
|
|
20
|
-
|
|
21
|
-
|
|
144
|
+
}
|
|
145
|
+
function nextHomeTemplate(appName, domain, envList) {
|
|
146
|
+
return `import { EnvList } from '@/components/env-list';
|
|
147
|
+
|
|
148
|
+
export default function Home() {
|
|
149
|
+
return (
|
|
150
|
+
<section className="mx-auto flex w-full max-w-3xl flex-col gap-8 px-6 py-12">
|
|
151
|
+
<div className="flex flex-col gap-3">
|
|
152
|
+
<p className="text-xs uppercase tracking-[0.4em]">Hello World</p>
|
|
153
|
+
<h1 className="text-3xl font-thin">${escapeTemplate(appName)}</h1>
|
|
154
|
+
<p className="text-sm">
|
|
155
|
+
${escapeTemplate(domain) ? `Domain: ${escapeTemplate(domain)}` : 'No domain configured yet.'}
|
|
156
|
+
</p>
|
|
157
|
+
</div>
|
|
158
|
+
<div className="rounded-xl border border-[var(--fg)] p-6">
|
|
159
|
+
<h2 className="text-lg font-medium">Environment variables</h2>
|
|
160
|
+
<p className="text-sm">Set these in your <code className="rounded bg-[var(--fg)] px-2 py-1 text-[var(--bg)]">.env</code>.</p>
|
|
161
|
+
<EnvList />
|
|
162
|
+
</div>
|
|
163
|
+
<div className="rounded-xl border border-[var(--fg)] p-6">
|
|
164
|
+
<h2 className="text-lg font-medium">Routes</h2>
|
|
165
|
+
<p className="text-sm">Explore <code className="rounded bg-[var(--fg)] px-2 py-1 text-[var(--bg)]">/about</code> and <code className="rounded bg-[var(--fg)] px-2 py-1 text-[var(--bg)]">/guide</code>.</p>
|
|
166
|
+
</div>
|
|
167
|
+
</section>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
`;
|
|
171
|
+
}
|
|
172
|
+
function nextRouteTemplate(title, body) {
|
|
173
|
+
return `export default function Page() {
|
|
174
|
+
return (
|
|
175
|
+
<section className="mx-auto flex w-full max-w-3xl flex-col gap-4 px-6 py-12">
|
|
176
|
+
<h1 className="text-3xl font-thin">${title}</h1>
|
|
177
|
+
<p className="text-sm">${body}</p>
|
|
178
|
+
</section>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
`;
|
|
182
|
+
}
|
|
183
|
+
function nextHeaderTemplate(appName) {
|
|
184
|
+
return `import Link from 'next/link';
|
|
185
|
+
|
|
186
|
+
const links = [
|
|
187
|
+
{ href: '/', label: 'Home' },
|
|
188
|
+
{ href: '/about', label: 'About' },
|
|
189
|
+
{ href: '/guide', label: 'Guide' }
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
export function SiteHeader({ appName }: { appName: string }) {
|
|
193
|
+
return (
|
|
194
|
+
<header className="border-b border-[var(--fg)]">
|
|
195
|
+
<div className="mx-auto flex w-full max-w-4xl items-center justify-between px-6 py-4">
|
|
196
|
+
<div className="flex items-baseline gap-3">
|
|
197
|
+
<span className="text-xl font-thin tracking-[0.3em]">ZER0</span>
|
|
198
|
+
<span className="text-xs uppercase tracking-[0.2em]">{appName}</span>
|
|
199
|
+
</div>
|
|
200
|
+
<nav className="hidden items-center gap-6 text-sm sm:flex">
|
|
201
|
+
{links.map((link) => (
|
|
202
|
+
<Link key={link.href} href={link.href} className="underline-offset-4 hover:underline">
|
|
203
|
+
{link.label}
|
|
204
|
+
</Link>
|
|
205
|
+
))}
|
|
206
|
+
</nav>
|
|
207
|
+
<details className="sm:hidden">
|
|
208
|
+
<summary className="cursor-pointer text-sm">Menu</summary>
|
|
209
|
+
<div className="mt-3 flex flex-col gap-3 text-sm">
|
|
210
|
+
{links.map((link) => (
|
|
211
|
+
<Link key={link.href} href={link.href} className="underline-offset-4 hover:underline">
|
|
212
|
+
{link.label}
|
|
213
|
+
</Link>
|
|
214
|
+
))}
|
|
215
|
+
</div>
|
|
216
|
+
</details>
|
|
217
|
+
</div>
|
|
218
|
+
</header>
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
`;
|
|
222
|
+
}
|
|
223
|
+
function nextFooterTemplate(domain) {
|
|
224
|
+
const domainLabel = escapeTemplate(domain).trim().length > 0
|
|
225
|
+
? `Domain: ${escapeTemplate(domain)}`
|
|
226
|
+
: 'Domain: not set';
|
|
227
|
+
return `export function SiteFooter({ domain }: { domain?: string }) {
|
|
228
|
+
return (
|
|
229
|
+
<footer className=\"border-t border-[var(--fg)]\">
|
|
230
|
+
<div className=\"mx-auto flex w-full max-w-4xl flex-col gap-2 px-6 py-4 text-xs\">
|
|
231
|
+
<span>${domainLabel}</span>
|
|
232
|
+
<span>Generated by Aexis Zero.</span>
|
|
233
|
+
</div>
|
|
234
|
+
</footer>
|
|
235
|
+
);
|
|
22
236
|
}
|
|
23
237
|
`;
|
|
24
|
-
|
|
238
|
+
}
|
|
239
|
+
function nextEnvListTemplate(envList) {
|
|
240
|
+
return `export function EnvList() {
|
|
241
|
+
return (
|
|
242
|
+
<div className="mt-4 flex flex-col gap-4">
|
|
243
|
+
${envList}
|
|
244
|
+
</div>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
`;
|
|
248
|
+
}
|
|
249
|
+
function nextUtilsTemplate() {
|
|
250
|
+
return `import { clsx, type ClassValue } from 'clsx';
|
|
25
251
|
import { twMerge } from 'tailwind-merge';
|
|
26
252
|
|
|
27
253
|
export function cn(...inputs: ClassValue[]) {
|
|
28
254
|
return twMerge(clsx(inputs));
|
|
29
255
|
}
|
|
30
256
|
`;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
257
|
+
}
|
|
258
|
+
function nextGlobalsCss() {
|
|
259
|
+
return `@import url('https://fonts.googleapis.com/css2?family=Geist+Mono:wght@100;200;300;400;500;600;700;800;900&display=swap');
|
|
260
|
+
|
|
261
|
+
@tailwind base;
|
|
262
|
+
@tailwind components;
|
|
263
|
+
@tailwind utilities;
|
|
264
|
+
|
|
265
|
+
:root {
|
|
266
|
+
--bg: #E7E5E4;
|
|
267
|
+
--fg: #1C1917;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
@media (prefers-color-scheme: dark) {
|
|
271
|
+
:root {
|
|
272
|
+
--bg: #1C1917;
|
|
273
|
+
--fg: #E7E5E4;
|
|
45
274
|
}
|
|
46
275
|
}
|
|
47
|
-
`;
|
|
48
|
-
export const tamaguiConfigTemplate = `import { config } from '@tamagui/config/v3';
|
|
49
276
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const { withTamagui } = require('@tamagui/metro-plugin');
|
|
277
|
+
* {
|
|
278
|
+
border-color: var(--fg);
|
|
279
|
+
}
|
|
54
280
|
|
|
55
|
-
|
|
281
|
+
html,
|
|
282
|
+
body {
|
|
283
|
+
min-height: 100%;
|
|
284
|
+
background: var(--bg);
|
|
285
|
+
color: var(--fg);
|
|
286
|
+
font-family: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
|
287
|
+
}
|
|
56
288
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
289
|
+
a {
|
|
290
|
+
color: inherit;
|
|
291
|
+
text-decoration: underline;
|
|
292
|
+
text-underline-offset: 4px;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
code {
|
|
296
|
+
font-family: 'Geist Mono', ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace;
|
|
297
|
+
}
|
|
62
298
|
`;
|
|
63
|
-
|
|
64
|
-
|
|
299
|
+
}
|
|
300
|
+
function renderNextEnvList(envVars) {
|
|
301
|
+
if (envVars.length === 0) {
|
|
302
|
+
return '<p className="text-sm">No environment variables required.</p>';
|
|
303
|
+
}
|
|
304
|
+
return envVars
|
|
305
|
+
.map((item) => {
|
|
306
|
+
return `
|
|
307
|
+
<div className="rounded-lg border border-[var(--fg)] p-4">
|
|
308
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
309
|
+
<code className="rounded bg-[var(--fg)] px-2 py-1 text-[var(--bg)]">${escapeTemplate(item.key)}</code>
|
|
310
|
+
<span className="text-sm">${escapeTemplate(item.description)}</span>
|
|
311
|
+
</div>
|
|
312
|
+
<a
|
|
313
|
+
className="mt-2 inline-flex text-sm underline underline-offset-4"
|
|
314
|
+
href="${escapeAttribute(item.url)}"
|
|
315
|
+
target="_blank"
|
|
316
|
+
rel="noreferrer"
|
|
317
|
+
>
|
|
318
|
+
Get keys →
|
|
319
|
+
</a>
|
|
320
|
+
</div>`;
|
|
321
|
+
})
|
|
322
|
+
.join('\n');
|
|
323
|
+
}
|
|
324
|
+
function expoLayoutTemplate() {
|
|
325
|
+
return `import { Stack } from 'expo-router';
|
|
326
|
+
import { TamaguiProvider, Theme } from 'tamagui';
|
|
327
|
+
import { useColorScheme } from 'react-native';
|
|
328
|
+
import { useFonts, GeistMono_100Thin } from '@expo-google-fonts/geist-mono';
|
|
65
329
|
import config from '../tamagui.config';
|
|
66
330
|
|
|
67
331
|
export default function RootLayout() {
|
|
332
|
+
const scheme = useColorScheme();
|
|
333
|
+
const [loaded] = useFonts({ GeistMono_100Thin });
|
|
334
|
+
|
|
335
|
+
if (!loaded) {
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
|
|
68
339
|
return (
|
|
69
340
|
<TamaguiProvider config={config}>
|
|
70
|
-
<
|
|
341
|
+
<Theme name={scheme === 'dark' ? 'dark' : 'light'}>
|
|
342
|
+
<Stack screenOptions={{ headerShown: false }} />
|
|
343
|
+
</Theme>
|
|
71
344
|
</TamaguiProvider>
|
|
72
345
|
);
|
|
73
346
|
}
|
|
74
347
|
`;
|
|
75
|
-
|
|
348
|
+
}
|
|
349
|
+
function expoHomeTemplate(appName, domain, envItems) {
|
|
350
|
+
return `import { Text, YStack } from 'tamagui';
|
|
351
|
+
import { PageShell } from '../components/page-shell';
|
|
352
|
+
import { EnvList } from '../components/env-list';
|
|
353
|
+
import { FONT_FAMILY, useThemeColors } from '../components/theme';
|
|
76
354
|
|
|
77
355
|
export default function Home() {
|
|
356
|
+
const { fg } = useThemeColors();
|
|
357
|
+
|
|
78
358
|
return (
|
|
79
|
-
<
|
|
80
|
-
|
|
81
|
-
|
|
359
|
+
<PageShell
|
|
360
|
+
title="${escapeTemplate(appName)}"
|
|
361
|
+
subtitle="${escapeTemplate(domain) ? `Domain: ${escapeTemplate(domain)}` : 'No domain configured yet.'}"
|
|
362
|
+
badge="Hello World"
|
|
363
|
+
>
|
|
364
|
+
<YStack borderWidth={1} borderColor={fg} padding="$4" borderRadius="$4" gap="$3">
|
|
365
|
+
<Text fontFamily={FONT_FAMILY} fontSize="$4" color={fg}>
|
|
366
|
+
Environment variables
|
|
367
|
+
</Text>
|
|
368
|
+
<EnvList />
|
|
369
|
+
</YStack>
|
|
370
|
+
<YStack borderWidth={1} borderColor={fg} padding="$4" borderRadius="$4" gap="$3">
|
|
371
|
+
<Text fontFamily={FONT_FAMILY} fontSize="$4" color={fg}>
|
|
372
|
+
Routes
|
|
373
|
+
</Text>
|
|
374
|
+
<Text fontFamily={FONT_FAMILY} fontSize="$2" color={fg}>
|
|
375
|
+
Visit /about and /guide.
|
|
376
|
+
</Text>
|
|
377
|
+
</YStack>
|
|
378
|
+
</PageShell>
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
`;
|
|
382
|
+
}
|
|
383
|
+
function expoRouteTemplate(title, body) {
|
|
384
|
+
return `import { PageShell } from '../components/page-shell';
|
|
385
|
+
|
|
386
|
+
export default function Page() {
|
|
387
|
+
return (
|
|
388
|
+
<PageShell title="${title}" subtitle="${body}">
|
|
389
|
+
<></>
|
|
390
|
+
</PageShell>
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
`;
|
|
394
|
+
}
|
|
395
|
+
function expoThemeTemplate() {
|
|
396
|
+
return `import { useColorScheme } from 'react-native';
|
|
397
|
+
|
|
398
|
+
export const COLORS = {
|
|
399
|
+
light: {
|
|
400
|
+
bg: '#E7E5E4',
|
|
401
|
+
fg: '#1C1917'
|
|
402
|
+
},
|
|
403
|
+
dark: {
|
|
404
|
+
bg: '#1C1917',
|
|
405
|
+
fg: '#E7E5E4'
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
export const FONT_FAMILY = 'GeistMono_100Thin';
|
|
410
|
+
|
|
411
|
+
export function useThemeColors() {
|
|
412
|
+
const scheme = useColorScheme();
|
|
413
|
+
const mode = scheme === 'dark' ? 'dark' : 'light';
|
|
414
|
+
return {
|
|
415
|
+
mode,
|
|
416
|
+
bg: COLORS[mode].bg,
|
|
417
|
+
fg: COLORS[mode].fg
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
`;
|
|
421
|
+
}
|
|
422
|
+
function expoHeaderTemplate(appName) {
|
|
423
|
+
return `import { useState } from 'react';
|
|
424
|
+
import { Link } from 'expo-router';
|
|
425
|
+
import { Button, Text, XStack, YStack } from 'tamagui';
|
|
426
|
+
import { FONT_FAMILY, useThemeColors } from './theme';
|
|
427
|
+
|
|
428
|
+
const links = [
|
|
429
|
+
{ href: '/', label: 'Home' },
|
|
430
|
+
{ href: '/about', label: 'About' },
|
|
431
|
+
{ href: '/guide', label: 'Guide' }
|
|
432
|
+
];
|
|
433
|
+
|
|
434
|
+
export function SiteHeader() {
|
|
435
|
+
const [open, setOpen] = useState(false);
|
|
436
|
+
const { bg, fg } = useThemeColors();
|
|
437
|
+
|
|
438
|
+
return (
|
|
439
|
+
<YStack backgroundColor={bg} paddingHorizontal="$5" paddingVertical="$4" borderBottomWidth={1} borderColor={fg}>
|
|
440
|
+
<XStack alignItems="center" justifyContent="space-between">
|
|
441
|
+
<XStack alignItems="center" gap="$3">
|
|
442
|
+
<Text fontFamily={FONT_FAMILY} fontWeight="100" fontSize="$6" color={fg}>
|
|
443
|
+
ZER0
|
|
444
|
+
</Text>
|
|
445
|
+
<Text fontFamily={FONT_FAMILY} fontSize="$2" textTransform="uppercase" color={fg}>
|
|
446
|
+
${escapeTemplate(appName)}
|
|
447
|
+
</Text>
|
|
448
|
+
</XStack>
|
|
449
|
+
<Button
|
|
450
|
+
backgroundColor={fg}
|
|
451
|
+
color={bg}
|
|
452
|
+
fontFamily={FONT_FAMILY}
|
|
453
|
+
size="$2"
|
|
454
|
+
onPress={() => setOpen((prev) => !prev)}
|
|
455
|
+
>
|
|
456
|
+
Menu
|
|
457
|
+
</Button>
|
|
458
|
+
</XStack>
|
|
459
|
+
{open ? (
|
|
460
|
+
<YStack marginTop="$3" gap="$2">
|
|
461
|
+
{links.map((link) => (
|
|
462
|
+
<Link key={link.href} href={link.href} asChild>
|
|
463
|
+
<Text fontFamily={FONT_FAMILY} textDecorationLine="underline" color={fg}>
|
|
464
|
+
{link.label}
|
|
465
|
+
</Text>
|
|
466
|
+
</Link>
|
|
467
|
+
))}
|
|
468
|
+
</YStack>
|
|
469
|
+
) : null}
|
|
470
|
+
</YStack>
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
`;
|
|
474
|
+
}
|
|
475
|
+
function expoFooterTemplate(domain) {
|
|
476
|
+
return `import { Text, YStack } from 'tamagui';
|
|
477
|
+
import { FONT_FAMILY, useThemeColors } from './theme';
|
|
478
|
+
|
|
479
|
+
export function SiteFooter() {
|
|
480
|
+
const { bg, fg } = useThemeColors();
|
|
481
|
+
|
|
482
|
+
return (
|
|
483
|
+
<YStack backgroundColor={bg} paddingHorizontal="$5" paddingVertical="$4" borderTopWidth={1} borderColor={fg}>
|
|
484
|
+
<Text fontFamily={FONT_FAMILY} fontSize="$2" color={fg}>
|
|
485
|
+
${escapeTemplate(domain) ? `Domain: ${escapeTemplate(domain)}` : 'Domain: not set'}
|
|
486
|
+
</Text>
|
|
487
|
+
<Text fontFamily={FONT_FAMILY} fontSize="$2" color={fg}>
|
|
488
|
+
Generated by Aexis Zero.
|
|
489
|
+
</Text>
|
|
82
490
|
</YStack>
|
|
83
491
|
);
|
|
84
492
|
}
|
|
85
493
|
`;
|
|
494
|
+
}
|
|
495
|
+
function expoEnvListTemplate(envItems) {
|
|
496
|
+
return `import { Linking } from 'react-native';
|
|
497
|
+
import { Text, YStack } from 'tamagui';
|
|
498
|
+
import { FONT_FAMILY, useThemeColors } from './theme';
|
|
499
|
+
|
|
500
|
+
const envItems = ${envItems};
|
|
501
|
+
|
|
502
|
+
export function EnvList() {
|
|
503
|
+
const { bg, fg } = useThemeColors();
|
|
504
|
+
|
|
505
|
+
if (envItems.length === 0) {
|
|
506
|
+
return (
|
|
507
|
+
<Text fontFamily={FONT_FAMILY} fontSize="$2" color={fg}>
|
|
508
|
+
No environment variables required.
|
|
509
|
+
</Text>
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return (
|
|
514
|
+
<YStack gap="$3">
|
|
515
|
+
{envItems.map((item) => (
|
|
516
|
+
<YStack key={item.key} borderWidth={1} borderColor={fg} padding="$3" borderRadius="$4">
|
|
517
|
+
<Text fontFamily={FONT_FAMILY} color={fg}>{item.description}</Text>
|
|
518
|
+
<Text
|
|
519
|
+
fontFamily={FONT_FAMILY}
|
|
520
|
+
backgroundColor={fg}
|
|
521
|
+
color={bg}
|
|
522
|
+
paddingHorizontal="$2"
|
|
523
|
+
paddingVertical="$1"
|
|
524
|
+
borderRadius="$2"
|
|
525
|
+
marginTop="$2"
|
|
526
|
+
>
|
|
527
|
+
{item.key}
|
|
528
|
+
</Text>
|
|
529
|
+
<Text
|
|
530
|
+
fontFamily={FONT_FAMILY}
|
|
531
|
+
textDecorationLine="underline"
|
|
532
|
+
color={fg}
|
|
533
|
+
marginTop="$2"
|
|
534
|
+
onPress={() => Linking.openURL(item.url)}
|
|
535
|
+
>
|
|
536
|
+
Get keys →
|
|
537
|
+
</Text>
|
|
538
|
+
</YStack>
|
|
539
|
+
))}
|
|
540
|
+
</YStack>
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
`;
|
|
544
|
+
}
|
|
545
|
+
function expoPageShellTemplate() {
|
|
546
|
+
return `import type { ReactNode } from 'react';
|
|
547
|
+
import { ScrollView, Text, YStack } from 'tamagui';
|
|
548
|
+
import { SiteHeader } from './site-header';
|
|
549
|
+
import { SiteFooter } from './site-footer';
|
|
550
|
+
import { FONT_FAMILY, useThemeColors } from './theme';
|
|
551
|
+
|
|
552
|
+
interface PageShellProps {
|
|
553
|
+
title: string;
|
|
554
|
+
subtitle: string;
|
|
555
|
+
badge?: string;
|
|
556
|
+
children: ReactNode;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
export function PageShell({ title, subtitle, badge, children }: PageShellProps) {
|
|
560
|
+
const { bg, fg } = useThemeColors();
|
|
561
|
+
|
|
562
|
+
return (
|
|
563
|
+
<YStack flex={1} backgroundColor={bg}>
|
|
564
|
+
<SiteHeader />
|
|
565
|
+
<ScrollView contentContainerStyle={{ padding: 24 }}>
|
|
566
|
+
<YStack gap="$4">
|
|
567
|
+
{badge ? (
|
|
568
|
+
<Text fontFamily={FONT_FAMILY} fontSize="$2" textTransform="uppercase" letterSpacing={2} color={fg}>
|
|
569
|
+
{badge}
|
|
570
|
+
</Text>
|
|
571
|
+
) : null}
|
|
572
|
+
<Text fontFamily={FONT_FAMILY} fontSize="$7" color={fg}>
|
|
573
|
+
{title}
|
|
574
|
+
</Text>
|
|
575
|
+
<Text fontFamily={FONT_FAMILY} fontSize="$3" color={fg}>
|
|
576
|
+
{subtitle}
|
|
577
|
+
</Text>
|
|
578
|
+
{children}
|
|
579
|
+
</YStack>
|
|
580
|
+
</ScrollView>
|
|
581
|
+
<SiteFooter />
|
|
582
|
+
</YStack>
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
`;
|
|
586
|
+
}
|
|
587
|
+
function tamaguiConfigTemplate() {
|
|
588
|
+
return `import { config } from '@tamagui/config/v3';
|
|
589
|
+
|
|
590
|
+
export default config;
|
|
591
|
+
`;
|
|
592
|
+
}
|
|
593
|
+
function metroConfigTemplate() {
|
|
594
|
+
return `const { getDefaultConfig } = require('expo/metro-config');
|
|
595
|
+
const { withTamagui } = require('@tamagui/metro-plugin');
|
|
596
|
+
|
|
597
|
+
const config = getDefaultConfig(__dirname);
|
|
598
|
+
|
|
599
|
+
module.exports = withTamagui(config, {
|
|
600
|
+
components: ['tamagui'],
|
|
601
|
+
config: './tamagui.config.ts',
|
|
602
|
+
outputCSS: './tamagui-web.css'
|
|
603
|
+
});
|
|
604
|
+
`;
|
|
605
|
+
}
|
|
606
|
+
function babelConfigTemplate() {
|
|
607
|
+
return `module.exports = function (api) {
|
|
608
|
+
api.cache(true);
|
|
609
|
+
return {
|
|
610
|
+
presets: ['babel-preset-expo'],
|
|
611
|
+
plugins: [
|
|
612
|
+
'expo-router/babel',
|
|
613
|
+
[
|
|
614
|
+
'@tamagui/babel-plugin',
|
|
615
|
+
{
|
|
616
|
+
config: './tamagui.config.ts',
|
|
617
|
+
components: ['tamagui']
|
|
618
|
+
}
|
|
619
|
+
]
|
|
620
|
+
]
|
|
621
|
+
};
|
|
622
|
+
};
|
|
623
|
+
`;
|
|
624
|
+
}
|
|
625
|
+
function renderExpoEnvItems(envVars) {
|
|
626
|
+
if (envVars.length === 0) {
|
|
627
|
+
return '[]';
|
|
628
|
+
}
|
|
629
|
+
const items = envVars.map((item) => {
|
|
630
|
+
return `{
|
|
631
|
+
key: '${escapeTemplate(item.key)}',
|
|
632
|
+
description: '${escapeTemplate(item.description)}',
|
|
633
|
+
url: '${escapeTemplate(item.url)}'
|
|
634
|
+
}`;
|
|
635
|
+
});
|
|
636
|
+
return `[
|
|
637
|
+
${items.join(',\n ')}
|
|
638
|
+
]`;
|
|
639
|
+
}
|
|
640
|
+
function escapeTemplate(value) {
|
|
641
|
+
return value
|
|
642
|
+
.replace(/`/g, '\\`')
|
|
643
|
+
.replace(/\$/g, '\\$')
|
|
644
|
+
.replace(/"/g, '\\"')
|
|
645
|
+
.replace(/'/g, "\\'");
|
|
646
|
+
}
|
|
647
|
+
function escapeAttribute(value) {
|
|
648
|
+
return value.replace(/"/g, '"');
|
|
649
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { render } from 'ink';
|
|
4
|
-
import { App } from './ui/App.js';
|
|
2
|
+
import { runWizard } from './cli/prompts.js';
|
|
5
3
|
import { assertBunAvailable } from './env/detect.js';
|
|
6
4
|
import { scaffoldProject } from './engine/scaffold.js';
|
|
7
5
|
async function main() {
|
|
@@ -14,13 +12,8 @@ async function main() {
|
|
|
14
12
|
process.exitCode = 1;
|
|
15
13
|
return;
|
|
16
14
|
}
|
|
17
|
-
|
|
18
|
-
const { waitUntilExit } = render(_jsx(App, { onComplete: (result) => {
|
|
19
|
-
config = result;
|
|
20
|
-
} }));
|
|
21
|
-
await waitUntilExit();
|
|
15
|
+
const config = await runWizard();
|
|
22
16
|
if (!config) {
|
|
23
|
-
console.log('Cancelled.');
|
|
24
17
|
return;
|
|
25
18
|
}
|
|
26
19
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aex.is/zero",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "Aexis Zero scaffolding CLI",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -18,22 +18,17 @@
|
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
20
|
"build": "tsc -p tsconfig.json",
|
|
21
|
-
"dev": "bun run src/index.
|
|
21
|
+
"dev": "bun run src/index.ts",
|
|
22
22
|
"start": "node dist/index.js",
|
|
23
23
|
"prepublishOnly": "npm run build"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
+
"@clack/prompts": "^0.7.0",
|
|
26
27
|
"execa": "^8.0.1",
|
|
27
|
-
"ink": "^4.4.1",
|
|
28
|
-
"ink-text-input": "^5.0.1",
|
|
29
|
-
"react": "^18.3.1",
|
|
30
|
-
"react-dom": "^18.3.1",
|
|
31
28
|
"which": "^4.0.0"
|
|
32
29
|
},
|
|
33
30
|
"devDependencies": {
|
|
34
31
|
"@types/node": "^20.11.30",
|
|
35
|
-
"@types/react": "^18.3.3",
|
|
36
|
-
"@types/react-dom": "^18.3.0",
|
|
37
32
|
"@types/which": "^3.0.3",
|
|
38
33
|
"typescript": "^5.5.4"
|
|
39
34
|
}
|