orizon 0.2.0 → 0.2.1
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/bin/init.mjs +537 -156
- package/package.json +1 -1
package/bin/init.mjs
CHANGED
|
@@ -1,32 +1,80 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
4
|
-
import {
|
|
4
|
+
import { join, relative, basename } from "node:path";
|
|
5
5
|
import { execSync } from "node:child_process";
|
|
6
|
+
import { createInterface } from "node:readline";
|
|
6
7
|
import pc from "picocolors";
|
|
7
8
|
|
|
8
9
|
// ---------------------------------------------------------------------------
|
|
9
10
|
// Logging helpers
|
|
10
11
|
// ---------------------------------------------------------------------------
|
|
11
12
|
const log = {
|
|
12
|
-
step: (msg) =>
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
step: (num, total, msg) =>
|
|
14
|
+
console.log(`\n ${pc.cyan(`[${num}/${total}]`)} ${pc.bold(msg)}`),
|
|
15
|
+
success: (msg) => console.log(` ${pc.green("✔")} ${msg}`),
|
|
16
|
+
skip: (msg) => console.log(` ${pc.yellow("⊘")} ${pc.dim(msg)}`),
|
|
17
|
+
error: (msg) => console.error(` ${pc.red("✖")} ${msg}`),
|
|
18
|
+
info: (msg) => console.log(` ${pc.dim(msg)}`),
|
|
19
|
+
detail: (msg) => console.log(` ${msg}`),
|
|
20
|
+
divider: () => console.log(pc.dim(" " + "─".repeat(50))),
|
|
17
21
|
};
|
|
18
22
|
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Prompt helper — ask y/n question
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
function ask(question, defaultYes = true) {
|
|
27
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
28
|
+
const hint = defaultYes ? "[Y/n]" : "[y/N]";
|
|
29
|
+
|
|
30
|
+
return new Promise((resolve) => {
|
|
31
|
+
rl.question(
|
|
32
|
+
`\n ${pc.yellow("?")} ${question} ${pc.dim(hint)} `,
|
|
33
|
+
(answer) => {
|
|
34
|
+
rl.close();
|
|
35
|
+
const trimmed = answer.trim().toLowerCase();
|
|
36
|
+
if (trimmed === "") resolve(defaultYes);
|
|
37
|
+
else resolve(trimmed === "y" || trimmed === "yes");
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Prompt helper — ask for text input
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
function askText(question, defaultValue = "") {
|
|
47
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
48
|
+
const hint = defaultValue ? pc.dim(` (${defaultValue})`) : "";
|
|
49
|
+
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
rl.question(
|
|
52
|
+
`\n ${pc.yellow("?")} ${question}${hint} `,
|
|
53
|
+
(answer) => {
|
|
54
|
+
rl.close();
|
|
55
|
+
const trimmed = answer.trim();
|
|
56
|
+
resolve(trimmed || defaultValue);
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Sleep helper for pacing output
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
66
|
+
|
|
19
67
|
// ---------------------------------------------------------------------------
|
|
20
68
|
// Detect package manager
|
|
21
69
|
// ---------------------------------------------------------------------------
|
|
22
70
|
function detectPM(cwd) {
|
|
23
71
|
if (existsSync(join(cwd, "bun.lock")) || existsSync(join(cwd, "bun.lockb")))
|
|
24
|
-
return { name: "bun", install: "bun add -D" };
|
|
72
|
+
return { name: "bun", install: "bun add -D", installProd: "bun add", create: "bunx", exec: "bunx" };
|
|
25
73
|
if (existsSync(join(cwd, "pnpm-lock.yaml")))
|
|
26
|
-
return { name: "pnpm", install: "pnpm add -D" };
|
|
74
|
+
return { name: "pnpm", install: "pnpm add -D", installProd: "pnpm add", create: "pnpm create", exec: "pnpm dlx" };
|
|
27
75
|
if (existsSync(join(cwd, "yarn.lock")))
|
|
28
|
-
return { name: "yarn", install: "yarn add -D" };
|
|
29
|
-
return { name: "npm", install: "npm install -D" };
|
|
76
|
+
return { name: "yarn", install: "yarn add -D", installProd: "yarn add", create: "yarn create", exec: "yarn dlx" };
|
|
77
|
+
return { name: "npm", install: "npm install -D", installProd: "npm install", create: "npm create", exec: "npx" };
|
|
30
78
|
}
|
|
31
79
|
|
|
32
80
|
// ---------------------------------------------------------------------------
|
|
@@ -39,180 +87,389 @@ function isInstalled(pkg, pkgJson) {
|
|
|
39
87
|
}
|
|
40
88
|
|
|
41
89
|
// ---------------------------------------------------------------------------
|
|
42
|
-
//
|
|
90
|
+
// Sanitize project name for npm
|
|
43
91
|
// ---------------------------------------------------------------------------
|
|
44
|
-
function
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const importRegex = /^import\s.+$/gm;
|
|
52
|
-
let lastImportMatch = null;
|
|
53
|
-
let match;
|
|
54
|
-
while ((match = importRegex.exec(content)) !== null) {
|
|
55
|
-
lastImportMatch = match;
|
|
56
|
-
}
|
|
92
|
+
function sanitizeName(name) {
|
|
93
|
+
return name
|
|
94
|
+
.toLowerCase()
|
|
95
|
+
.replace(/\s+/g, "-")
|
|
96
|
+
.replace(/[^a-z0-9\-_.]/g, "")
|
|
97
|
+
.replace(/^[.\-_]+/, "");
|
|
98
|
+
}
|
|
57
99
|
|
|
58
|
-
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Scaffold a new React + Vite + TS project
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
async function scaffoldProject(cwd) {
|
|
104
|
+
const dirName = basename(cwd);
|
|
105
|
+
const defaultName = sanitizeName(dirName) || "my-orizon-app";
|
|
59
106
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
68
|
-
modified = true;
|
|
69
|
-
log.success("Added tailwindcss import to " + filePath.split(/[\\/]/).pop());
|
|
70
|
-
} else {
|
|
71
|
-
log.skip("tailwindcss import already in vite config");
|
|
107
|
+
// Ask project name
|
|
108
|
+
const rawName = await askText("What is your project name?", defaultName);
|
|
109
|
+
const projectName = sanitizeName(rawName);
|
|
110
|
+
|
|
111
|
+
if (!projectName) {
|
|
112
|
+
log.error("Invalid project name.");
|
|
113
|
+
process.exit(1);
|
|
72
114
|
}
|
|
73
115
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
content.slice(insertPos);
|
|
84
|
-
modified = true;
|
|
85
|
-
log.success("Added tailwindcss() plugin to vite config");
|
|
86
|
-
} else {
|
|
87
|
-
log.error(
|
|
88
|
-
"Could not find plugins array in vite config — please add tailwindcss() manually",
|
|
89
|
-
);
|
|
116
|
+
const projectDir = join(cwd, projectName);
|
|
117
|
+
|
|
118
|
+
if (existsSync(projectDir)) {
|
|
119
|
+
// Check if directory is non-empty
|
|
120
|
+
const { readdirSync } = await import("node:fs");
|
|
121
|
+
const contents = readdirSync(projectDir);
|
|
122
|
+
if (contents.length > 0) {
|
|
123
|
+
log.error(`Directory "${projectName}" already exists and is not empty.`);
|
|
124
|
+
process.exit(1);
|
|
90
125
|
}
|
|
91
|
-
} else {
|
|
92
|
-
log.skip("tailwindcss() plugin already in vite config");
|
|
93
126
|
}
|
|
94
127
|
|
|
95
|
-
|
|
96
|
-
|
|
128
|
+
console.log();
|
|
129
|
+
log.info(`Creating ${pc.bold(projectName)} with React + Vite + TypeScript...`);
|
|
130
|
+
log.info(pc.dim(`Directory: ${projectDir}`));
|
|
131
|
+
console.log();
|
|
132
|
+
|
|
133
|
+
// Step A: Create Vite project
|
|
134
|
+
log.info("Scaffolding Vite project...");
|
|
135
|
+
const createCmd = `npm create vite@latest ${projectName} -- --template react-ts`;
|
|
136
|
+
log.info(pc.dim(`$ ${createCmd}`));
|
|
137
|
+
await sleep(200);
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
execSync(createCmd, { cwd, stdio: "pipe" });
|
|
141
|
+
log.success("Created Vite + React + TypeScript project");
|
|
142
|
+
} catch (err) {
|
|
143
|
+
log.error("Failed to create Vite project.");
|
|
144
|
+
log.info("Make sure you have npm installed and try again.");
|
|
145
|
+
process.exit(1);
|
|
97
146
|
}
|
|
98
|
-
}
|
|
99
147
|
|
|
100
|
-
|
|
101
|
-
// Patch CSS file — prepend required @import / @source lines
|
|
102
|
-
// ---------------------------------------------------------------------------
|
|
103
|
-
function patchCSS(filePath, created) {
|
|
104
|
-
let content = created ? "" : readFileSync(filePath, "utf8");
|
|
105
|
-
const requiredLines = [
|
|
106
|
-
'@import "tailwindcss";',
|
|
107
|
-
'@import "orizon/preset.css";',
|
|
108
|
-
'@source "../node_modules/orizon/dist";',
|
|
109
|
-
];
|
|
148
|
+
await sleep(300);
|
|
110
149
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
150
|
+
// Step B: Install base dependencies
|
|
151
|
+
log.info("Installing base dependencies...");
|
|
152
|
+
const installCmd = "npm install";
|
|
153
|
+
log.info(pc.dim(`$ ${installCmd}`));
|
|
154
|
+
await sleep(200);
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
execSync(installCmd, { cwd: projectDir, stdio: "pipe" });
|
|
158
|
+
log.success("Installed base dependencies");
|
|
159
|
+
} catch {
|
|
160
|
+
log.error("Failed to install base dependencies.");
|
|
161
|
+
process.exit(1);
|
|
116
162
|
}
|
|
117
163
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
164
|
+
await sleep(300);
|
|
165
|
+
|
|
166
|
+
// Step C: Install Orizon
|
|
167
|
+
log.info("Installing Orizon...");
|
|
168
|
+
const orizonCmd = "npm install orizon";
|
|
169
|
+
log.info(pc.dim(`$ ${orizonCmd}`));
|
|
170
|
+
await sleep(200);
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
execSync(orizonCmd, { cwd: projectDir, stdio: "pipe" });
|
|
174
|
+
log.success("Installed orizon");
|
|
175
|
+
} catch {
|
|
176
|
+
log.error("Failed to install orizon.");
|
|
177
|
+
process.exit(1);
|
|
121
178
|
}
|
|
122
179
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
180
|
+
return { projectDir, projectName };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
// Find vite config in a directory
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
function findViteConfig(dir) {
|
|
187
|
+
const candidates = ["vite.config.ts", "vite.config.js", "vite.config.mts"];
|
|
188
|
+
for (const name of candidates) {
|
|
189
|
+
const full = join(dir, name);
|
|
190
|
+
if (existsSync(full)) return full;
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
129
193
|
}
|
|
130
194
|
|
|
131
195
|
// ---------------------------------------------------------------------------
|
|
132
196
|
// Main init function
|
|
133
197
|
// ---------------------------------------------------------------------------
|
|
134
198
|
export async function init() {
|
|
135
|
-
|
|
199
|
+
let cwd = process.cwd();
|
|
200
|
+
let scaffolded = false;
|
|
201
|
+
let projectName = "";
|
|
136
202
|
|
|
137
203
|
console.log();
|
|
138
|
-
console.log(
|
|
204
|
+
console.log(
|
|
205
|
+
pc.bold(pc.cyan(" ⬡ Orizon")) + pc.dim(" — project setup wizard"),
|
|
206
|
+
);
|
|
139
207
|
console.log();
|
|
208
|
+
log.divider();
|
|
140
209
|
|
|
141
210
|
// -----------------------------------------------------------------------
|
|
142
|
-
//
|
|
211
|
+
// Pre-flight: Detect project or scaffold a new one
|
|
143
212
|
// -----------------------------------------------------------------------
|
|
144
213
|
const pkgPath = join(cwd, "package.json");
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
214
|
+
const hasProject = existsSync(pkgPath);
|
|
215
|
+
const hasViteConfig = hasProject && findViteConfig(cwd) !== null;
|
|
216
|
+
|
|
217
|
+
if (hasProject && hasViteConfig) {
|
|
218
|
+
// Existing Vite project — proceed with setup
|
|
219
|
+
log.success("Detected existing Vite project");
|
|
220
|
+
} else if (hasProject && !hasViteConfig) {
|
|
221
|
+
// Has package.json but not a Vite project
|
|
222
|
+
log.info("Found package.json but no vite.config — not a Vite project.");
|
|
223
|
+
log.error("Currently only React + Vite projects are supported.");
|
|
148
224
|
process.exit(1);
|
|
149
|
-
}
|
|
225
|
+
} else {
|
|
226
|
+
// No project at all — offer to create one
|
|
227
|
+
log.info("No project detected in this directory.");
|
|
228
|
+
console.log();
|
|
229
|
+
log.info("Orizon can create a new React + Vite + TypeScript project for you");
|
|
230
|
+
log.info("and set up everything automatically.");
|
|
231
|
+
|
|
232
|
+
const proceed = await ask("Create a new project?");
|
|
233
|
+
if (!proceed) {
|
|
234
|
+
log.info("No worries! To set up an existing project, run this from its root directory.");
|
|
235
|
+
process.exit(0);
|
|
236
|
+
}
|
|
150
237
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const viteConfigPath = existsSync(viteConfigTS)
|
|
156
|
-
? viteConfigTS
|
|
157
|
-
: existsSync(viteConfigJS)
|
|
158
|
-
? viteConfigJS
|
|
159
|
-
: existsSync(viteConfigMTS)
|
|
160
|
-
? viteConfigMTS
|
|
161
|
-
: null;
|
|
162
|
-
|
|
163
|
-
if (!viteConfigPath) {
|
|
164
|
-
log.error("No vite.config.ts/js found. Currently only Vite projects are supported.");
|
|
165
|
-
process.exit(1);
|
|
166
|
-
}
|
|
238
|
+
const result = await scaffoldProject(cwd);
|
|
239
|
+
cwd = result.projectDir;
|
|
240
|
+
projectName = result.projectName;
|
|
241
|
+
scaffolded = true;
|
|
167
242
|
|
|
168
|
-
|
|
243
|
+
console.log();
|
|
244
|
+
log.divider();
|
|
245
|
+
console.log();
|
|
246
|
+
log.info("Project created! Now configuring Orizon...");
|
|
247
|
+
}
|
|
169
248
|
|
|
170
249
|
// -----------------------------------------------------------------------
|
|
171
|
-
//
|
|
250
|
+
// From here on, we're in a valid Vite project (either existing or new)
|
|
172
251
|
// -----------------------------------------------------------------------
|
|
252
|
+
const viteConfigPath = findViteConfig(cwd);
|
|
173
253
|
const pm = detectPM(cwd);
|
|
174
|
-
|
|
175
|
-
|
|
254
|
+
const currentPkgJson = JSON.parse(
|
|
255
|
+
readFileSync(join(cwd, "package.json"), "utf8"),
|
|
256
|
+
);
|
|
176
257
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
if (!
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
258
|
+
const TOTAL_STEPS = scaffolded ? 6 : 5;
|
|
259
|
+
let stepNum = scaffolded ? 2 : 1; // if scaffolded, step 1 was "Create project"
|
|
260
|
+
|
|
261
|
+
if (scaffolded) {
|
|
262
|
+
log.step(1, TOTAL_STEPS, "Create project");
|
|
263
|
+
log.success(`Created ${pc.bold(projectName)} with React + Vite + TypeScript`);
|
|
264
|
+
log.success("Installed base dependencies & Orizon");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (!scaffolded) {
|
|
268
|
+
log.info(`Package manager: ${pc.bold(pm.name)}`);
|
|
269
|
+
log.info(`Config file: ${pc.bold(viteConfigPath.split(/[\\/]/).pop())}`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// =======================================================================
|
|
273
|
+
// STEP: Install Tailwind CSS dependencies
|
|
274
|
+
// =======================================================================
|
|
275
|
+
log.step(stepNum, TOTAL_STEPS, "Tailwind CSS dependencies");
|
|
276
|
+
log.info("Orizon uses Tailwind CSS v4 for styling.");
|
|
277
|
+
log.info("These packages are required for Tailwind to work with Vite.");
|
|
278
|
+
await sleep(300);
|
|
279
|
+
|
|
280
|
+
const tailwindDeps = [
|
|
281
|
+
{ pkg: "tailwindcss", label: "Tailwind CSS core" },
|
|
282
|
+
{ pkg: "@tailwindcss/vite", label: "Tailwind CSS Vite plugin" },
|
|
283
|
+
];
|
|
284
|
+
|
|
285
|
+
const tailwindMissing = tailwindDeps.filter(
|
|
286
|
+
(d) => !isInstalled(d.pkg, currentPkgJson),
|
|
287
|
+
);
|
|
288
|
+
const tailwindInstalled = tailwindDeps.filter((d) =>
|
|
289
|
+
isInstalled(d.pkg, currentPkgJson),
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
for (const d of tailwindInstalled) {
|
|
293
|
+
log.skip(`${d.pkg} — already installed`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (tailwindMissing.length > 0) {
|
|
297
|
+
for (const d of tailwindMissing) {
|
|
298
|
+
log.detail(`${pc.bold(d.pkg)} ${pc.dim(`— ${d.label}`)}`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const proceed = scaffolded
|
|
302
|
+
? true
|
|
303
|
+
: await ask(
|
|
304
|
+
`Install ${tailwindMissing.length} Tailwind package${tailwindMissing.length > 1 ? "s" : ""}?`,
|
|
305
|
+
);
|
|
306
|
+
if (proceed) {
|
|
307
|
+
const cmd = `${pm.install} ${tailwindMissing.map((d) => d.pkg).join(" ")}`;
|
|
308
|
+
log.info(pc.dim(`Running: ${cmd}`));
|
|
309
|
+
await sleep(200);
|
|
310
|
+
try {
|
|
311
|
+
execSync(cmd, { cwd, stdio: "pipe" });
|
|
312
|
+
for (const d of tailwindMissing) {
|
|
313
|
+
log.success(`Installed ${d.pkg}`);
|
|
314
|
+
}
|
|
315
|
+
} catch {
|
|
316
|
+
log.error(`Failed to install. Run manually:\n ${cmd}`);
|
|
317
|
+
}
|
|
318
|
+
} else {
|
|
319
|
+
log.skip("Skipped — you can install these manually later");
|
|
198
320
|
}
|
|
199
|
-
} else {
|
|
200
|
-
log.skip("tailwindcss & @tailwindcss/vite already installed");
|
|
201
321
|
}
|
|
322
|
+
stepNum++;
|
|
323
|
+
|
|
324
|
+
// =======================================================================
|
|
325
|
+
// STEP: Install Form & Validation dependencies
|
|
326
|
+
// =======================================================================
|
|
327
|
+
log.step(stepNum, TOTAL_STEPS, "Form & validation dependencies");
|
|
328
|
+
log.info("These packages power the Form component with Zod validation.");
|
|
329
|
+
log.info("Needed for: Form, Form.Item, Form.useForm, Form.List");
|
|
330
|
+
await sleep(300);
|
|
331
|
+
|
|
332
|
+
const formDeps = [
|
|
333
|
+
{ pkg: "react-hook-form", label: "Form state management" },
|
|
334
|
+
{ pkg: "@hookform/resolvers", label: "Schema validation adapters" },
|
|
335
|
+
{ pkg: "zod", label: "Schema validation library" },
|
|
336
|
+
];
|
|
202
337
|
|
|
203
|
-
|
|
338
|
+
const formMissing = formDeps.filter(
|
|
339
|
+
(d) => !isInstalled(d.pkg, currentPkgJson),
|
|
340
|
+
);
|
|
341
|
+
const formAlreadyInstalled = formDeps.filter((d) =>
|
|
342
|
+
isInstalled(d.pkg, currentPkgJson),
|
|
343
|
+
);
|
|
204
344
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
log.step("Configuring Vite...");
|
|
209
|
-
patchViteConfig(viteConfigPath);
|
|
210
|
-
console.log();
|
|
345
|
+
for (const d of formAlreadyInstalled) {
|
|
346
|
+
log.skip(`${d.pkg} — already installed`);
|
|
347
|
+
}
|
|
211
348
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
349
|
+
if (formMissing.length > 0) {
|
|
350
|
+
for (const d of formMissing) {
|
|
351
|
+
log.detail(`${pc.bold(d.pkg)} ${pc.dim(`— ${d.label}`)}`);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const proceed = scaffolded
|
|
355
|
+
? true
|
|
356
|
+
: await ask(
|
|
357
|
+
`Install ${formMissing.length} form package${formMissing.length > 1 ? "s" : ""}?`,
|
|
358
|
+
);
|
|
359
|
+
if (proceed) {
|
|
360
|
+
const cmd = `${pm.install} ${formMissing.map((d) => d.pkg).join(" ")}`;
|
|
361
|
+
log.info(pc.dim(`Running: ${cmd}`));
|
|
362
|
+
await sleep(200);
|
|
363
|
+
try {
|
|
364
|
+
execSync(cmd, { cwd, stdio: "pipe" });
|
|
365
|
+
for (const d of formMissing) {
|
|
366
|
+
log.success(`Installed ${d.pkg}`);
|
|
367
|
+
}
|
|
368
|
+
} catch {
|
|
369
|
+
log.error(`Failed to install. Run manually:\n ${cmd}`);
|
|
370
|
+
}
|
|
371
|
+
} else {
|
|
372
|
+
log.skip("Skipped — Form component will not work without these");
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
stepNum++;
|
|
376
|
+
|
|
377
|
+
// =======================================================================
|
|
378
|
+
// STEP: Configure Vite
|
|
379
|
+
// =======================================================================
|
|
380
|
+
log.step(stepNum, TOTAL_STEPS, "Configure Vite");
|
|
381
|
+
log.info("The Tailwind CSS Vite plugin must be added to your vite config.");
|
|
382
|
+
log.info(`File: ${pc.bold(viteConfigPath.split(/[\\/]/).pop())}`);
|
|
383
|
+
await sleep(300);
|
|
384
|
+
|
|
385
|
+
const viteContent = readFileSync(viteConfigPath, "utf8");
|
|
386
|
+
const viteNeedsImport = !viteContent.includes("@tailwindcss/vite");
|
|
387
|
+
const viteNeedsPlugin = !viteContent.includes("tailwindcss()");
|
|
388
|
+
|
|
389
|
+
if (viteNeedsImport || viteNeedsPlugin) {
|
|
390
|
+
log.info("Changes needed:");
|
|
391
|
+
if (viteNeedsImport) {
|
|
392
|
+
log.detail(
|
|
393
|
+
pc.green("+ ") +
|
|
394
|
+
pc.cyan('import tailwindcss from "@tailwindcss/vite"'),
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
if (viteNeedsPlugin) {
|
|
398
|
+
log.detail(
|
|
399
|
+
pc.green("+ ") +
|
|
400
|
+
pc.cyan("tailwindcss()") +
|
|
401
|
+
pc.dim(" in plugins array"),
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const proceed = scaffolded ? true : await ask("Apply these changes?");
|
|
406
|
+
if (proceed) {
|
|
407
|
+
let content = readFileSync(viteConfigPath, "utf8");
|
|
408
|
+
let modified = false;
|
|
409
|
+
|
|
410
|
+
// Add import
|
|
411
|
+
if (viteNeedsImport) {
|
|
412
|
+
const importRegex = /^import\s.+$/gm;
|
|
413
|
+
let lastImportMatch = null;
|
|
414
|
+
let match;
|
|
415
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
416
|
+
lastImportMatch = match;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const importLine = 'import tailwindcss from "@tailwindcss/vite";';
|
|
420
|
+
if (lastImportMatch) {
|
|
421
|
+
const insertPos =
|
|
422
|
+
lastImportMatch.index + lastImportMatch[0].length;
|
|
423
|
+
content =
|
|
424
|
+
content.slice(0, insertPos) +
|
|
425
|
+
"\n" +
|
|
426
|
+
importLine +
|
|
427
|
+
content.slice(insertPos);
|
|
428
|
+
} else {
|
|
429
|
+
content = importLine + "\n" + content;
|
|
430
|
+
}
|
|
431
|
+
modified = true;
|
|
432
|
+
log.success("Added tailwindcss import");
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Add plugin
|
|
436
|
+
if (viteNeedsPlugin) {
|
|
437
|
+
const pluginsMatch = content.match(/plugins\s*:\s*\[/);
|
|
438
|
+
if (pluginsMatch) {
|
|
439
|
+
const insertPos = pluginsMatch.index + pluginsMatch[0].length;
|
|
440
|
+
content =
|
|
441
|
+
content.slice(0, insertPos) +
|
|
442
|
+
"tailwindcss(), " +
|
|
443
|
+
content.slice(insertPos);
|
|
444
|
+
modified = true;
|
|
445
|
+
log.success("Added tailwindcss() to plugins");
|
|
446
|
+
} else {
|
|
447
|
+
log.error(
|
|
448
|
+
"Could not find plugins array — please add tailwindcss() manually",
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (modified) {
|
|
454
|
+
writeFileSync(viteConfigPath, content, "utf8");
|
|
455
|
+
}
|
|
456
|
+
} else {
|
|
457
|
+
log.skip("Skipped — remember to add tailwindcss to your vite config");
|
|
458
|
+
}
|
|
459
|
+
} else {
|
|
460
|
+
log.skip("Vite config already has tailwindcss configured");
|
|
461
|
+
}
|
|
462
|
+
stepNum++;
|
|
463
|
+
|
|
464
|
+
// =======================================================================
|
|
465
|
+
// STEP: Set up CSS
|
|
466
|
+
// =======================================================================
|
|
467
|
+
log.step(stepNum, TOTAL_STEPS, "Set up CSS");
|
|
468
|
+
log.info("Orizon needs 3 lines at the top of your main CSS file:");
|
|
469
|
+
log.detail(pc.cyan('@import "tailwindcss";'));
|
|
470
|
+
log.detail(pc.cyan('@import "orizon/preset.css";'));
|
|
471
|
+
log.detail(pc.cyan('@source "../node_modules/orizon/dist";'));
|
|
472
|
+
await sleep(300);
|
|
216
473
|
|
|
217
474
|
const cssSearchOrder = [
|
|
218
475
|
"src/index.css",
|
|
@@ -230,28 +487,152 @@ export async function init() {
|
|
|
230
487
|
const full = join(cwd, candidate);
|
|
231
488
|
if (existsSync(full)) {
|
|
232
489
|
cssPath = full;
|
|
490
|
+
log.info(`Found: ${pc.bold(candidate)}`);
|
|
233
491
|
break;
|
|
234
492
|
}
|
|
235
493
|
}
|
|
236
494
|
|
|
237
495
|
if (!cssPath) {
|
|
238
|
-
|
|
239
|
-
const
|
|
240
|
-
if (
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
496
|
+
log.info("No existing CSS file found.");
|
|
497
|
+
const proceed = scaffolded ? true : await ask("Create src/index.css?");
|
|
498
|
+
if (proceed) {
|
|
499
|
+
const srcDir = join(cwd, "src");
|
|
500
|
+
if (!existsSync(srcDir)) mkdirSync(srcDir, { recursive: true });
|
|
501
|
+
cssPath = join(srcDir, "index.css");
|
|
502
|
+
cssCreated = true;
|
|
503
|
+
} else {
|
|
504
|
+
log.skip("Skipped — you'll need to add the CSS imports manually");
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (cssPath) {
|
|
509
|
+
const currentCSS = cssCreated ? "" : readFileSync(cssPath, "utf8");
|
|
510
|
+
const requiredLines = [
|
|
511
|
+
'@import "tailwindcss";',
|
|
512
|
+
'@import "orizon/preset.css";',
|
|
513
|
+
'@source "../node_modules/orizon/dist";',
|
|
514
|
+
];
|
|
515
|
+
const linesToAdd = requiredLines.filter((l) => !currentCSS.includes(l));
|
|
516
|
+
|
|
517
|
+
if (linesToAdd.length > 0) {
|
|
518
|
+
log.info(`Lines to add to ${pc.bold(cssPath.split(/[\\/]/).pop())}:`);
|
|
519
|
+
for (const line of linesToAdd) {
|
|
520
|
+
log.detail(pc.green("+ ") + pc.cyan(line));
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const proceed = scaffolded ? true : await ask("Add these lines?");
|
|
524
|
+
if (proceed) {
|
|
525
|
+
let content = cssCreated ? "" : readFileSync(cssPath, "utf8");
|
|
526
|
+
content = linesToAdd.join("\n") + (content ? "\n" + content : "\n");
|
|
527
|
+
writeFileSync(cssPath, content, "utf8");
|
|
528
|
+
log.success(
|
|
529
|
+
`Added ${linesToAdd.length} line${linesToAdd.length > 1 ? "s" : ""} to ${cssPath.split(/[\\/]/).pop()}`,
|
|
530
|
+
);
|
|
531
|
+
} else {
|
|
532
|
+
log.skip("Skipped CSS changes");
|
|
533
|
+
}
|
|
534
|
+
} else {
|
|
535
|
+
log.skip("CSS file already has all required imports");
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
stepNum++;
|
|
539
|
+
|
|
540
|
+
// =======================================================================
|
|
541
|
+
// STEP: Link stylesheet in index.html
|
|
542
|
+
// =======================================================================
|
|
543
|
+
log.step(stepNum, TOTAL_STEPS, "Link stylesheet in HTML");
|
|
544
|
+
log.info(
|
|
545
|
+
"Per Tailwind CSS docs, the stylesheet should be linked in index.html.",
|
|
546
|
+
);
|
|
547
|
+
await sleep(300);
|
|
548
|
+
|
|
549
|
+
const htmlPath = join(cwd, "index.html");
|
|
550
|
+
|
|
551
|
+
if (existsSync(htmlPath) && cssPath) {
|
|
552
|
+
const cssRelFromRoot = "/" + relative(cwd, cssPath).replace(/\\/g, "/");
|
|
553
|
+
const htmlContent = readFileSync(htmlPath, "utf8");
|
|
554
|
+
|
|
555
|
+
// Check if already linked (ignoring commented-out versions)
|
|
556
|
+
const commentPattern = /<!--[\s\S]*?-->/g;
|
|
557
|
+
const withoutComments = htmlContent.replace(commentPattern, "");
|
|
558
|
+
const alreadyLinked = withoutComments.includes(`href="${cssRelFromRoot}"`);
|
|
559
|
+
|
|
560
|
+
if (!alreadyLinked) {
|
|
561
|
+
const linkTag = `<link rel="stylesheet" href="${cssRelFromRoot}">`;
|
|
562
|
+
log.info(`Add to ${pc.bold("index.html")} <head>:`);
|
|
563
|
+
log.detail(pc.green("+ ") + pc.cyan(linkTag));
|
|
564
|
+
|
|
565
|
+
if (
|
|
566
|
+
htmlContent.includes(`<!--`) &&
|
|
567
|
+
htmlContent.includes(cssRelFromRoot)
|
|
568
|
+
) {
|
|
569
|
+
log.info(
|
|
570
|
+
pc.dim("Note: found a commented-out link — it will be replaced."),
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const proceed = scaffolded
|
|
575
|
+
? true
|
|
576
|
+
: await ask("Add stylesheet link to index.html?");
|
|
577
|
+
if (proceed) {
|
|
578
|
+
let content = htmlContent;
|
|
579
|
+
|
|
580
|
+
// Remove commented-out link if present
|
|
581
|
+
const commentedLinkRegex = new RegExp(
|
|
582
|
+
`\\s*<!--\\s*<link[^>]*href="${cssRelFromRoot.replace(/\//g, "\\/")}"[^>]*>\\s*-->`,
|
|
583
|
+
"g",
|
|
584
|
+
);
|
|
585
|
+
content = content.replace(commentedLinkRegex, "");
|
|
586
|
+
|
|
587
|
+
// Insert before </head>
|
|
588
|
+
const headCloseIndex = content.indexOf("</head>");
|
|
589
|
+
if (headCloseIndex !== -1) {
|
|
590
|
+
const indent = " ";
|
|
591
|
+
content =
|
|
592
|
+
content.slice(0, headCloseIndex) +
|
|
593
|
+
`${indent}<link rel="stylesheet" href="${cssRelFromRoot}">\n ` +
|
|
594
|
+
content.slice(headCloseIndex);
|
|
595
|
+
writeFileSync(htmlPath, content, "utf8");
|
|
596
|
+
log.success("Added stylesheet link to index.html");
|
|
597
|
+
} else {
|
|
598
|
+
log.error(
|
|
599
|
+
"Could not find </head> tag — please add the link manually",
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
} else {
|
|
603
|
+
log.skip("Skipped — make sure your CSS file is imported somewhere");
|
|
604
|
+
}
|
|
605
|
+
} else {
|
|
606
|
+
log.skip("Stylesheet already linked in index.html");
|
|
607
|
+
}
|
|
608
|
+
} else if (!existsSync(htmlPath)) {
|
|
609
|
+
log.skip("No index.html found — skipping");
|
|
244
610
|
}
|
|
245
611
|
|
|
246
|
-
|
|
612
|
+
// =======================================================================
|
|
613
|
+
// Done!
|
|
614
|
+
// =======================================================================
|
|
615
|
+
console.log();
|
|
616
|
+
log.divider();
|
|
617
|
+
console.log();
|
|
618
|
+
console.log(pc.green(pc.bold(" ✓ Orizon setup complete!")));
|
|
247
619
|
console.log();
|
|
248
620
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
621
|
+
if (scaffolded) {
|
|
622
|
+
console.log(" Get started:");
|
|
623
|
+
console.log(pc.cyan(` cd ${projectName}`));
|
|
624
|
+
console.log(pc.cyan(" npm run dev"));
|
|
625
|
+
console.log();
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
console.log(" Import components:");
|
|
629
|
+
console.log(
|
|
630
|
+
pc.cyan(' import { Button, Form, Input, Select } from "orizon";'),
|
|
631
|
+
);
|
|
253
632
|
console.log();
|
|
254
|
-
console.log("
|
|
255
|
-
console.log(
|
|
633
|
+
console.log(" Example with Form + Zod:");
|
|
634
|
+
console.log(
|
|
635
|
+
pc.dim(" const [form] = Form.useForm({ schema: myZodSchema });"),
|
|
636
|
+
);
|
|
256
637
|
console.log();
|
|
257
638
|
}
|