orizon 0.1.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/cli.mjs +52 -0
- package/bin/init.mjs +638 -0
- package/package.json +8 -2
package/bin/cli.mjs
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { dirname, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const pkg = JSON.parse(
|
|
9
|
+
readFileSync(resolve(__dirname, "..", "package.json"), "utf8"),
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
const [command] = process.argv.slice(2);
|
|
13
|
+
|
|
14
|
+
function printHelp() {
|
|
15
|
+
console.log(`
|
|
16
|
+
orizon v${pkg.version}
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
npx orizon <command>
|
|
20
|
+
|
|
21
|
+
Commands:
|
|
22
|
+
init Set up Orizon in your React + Vite project
|
|
23
|
+
|
|
24
|
+
Options:
|
|
25
|
+
--version Show version number
|
|
26
|
+
--help Show this help message
|
|
27
|
+
`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
switch (command) {
|
|
31
|
+
case "init": {
|
|
32
|
+
const { init } = await import("./init.mjs");
|
|
33
|
+
await init();
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
case "--version":
|
|
38
|
+
case "-v":
|
|
39
|
+
console.log(pkg.version);
|
|
40
|
+
break;
|
|
41
|
+
|
|
42
|
+
case "--help":
|
|
43
|
+
case "-h":
|
|
44
|
+
case undefined:
|
|
45
|
+
printHelp();
|
|
46
|
+
break;
|
|
47
|
+
|
|
48
|
+
default:
|
|
49
|
+
console.error(`Unknown command: ${command}\n`);
|
|
50
|
+
printHelp();
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
package/bin/init.mjs
ADDED
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
4
|
+
import { join, relative, basename } from "node:path";
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
6
|
+
import { createInterface } from "node:readline";
|
|
7
|
+
import pc from "picocolors";
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Logging helpers
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
const log = {
|
|
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))),
|
|
21
|
+
};
|
|
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
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Detect package manager
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
function detectPM(cwd) {
|
|
71
|
+
if (existsSync(join(cwd, "bun.lock")) || existsSync(join(cwd, "bun.lockb")))
|
|
72
|
+
return { name: "bun", install: "bun add -D", installProd: "bun add", create: "bunx", exec: "bunx" };
|
|
73
|
+
if (existsSync(join(cwd, "pnpm-lock.yaml")))
|
|
74
|
+
return { name: "pnpm", install: "pnpm add -D", installProd: "pnpm add", create: "pnpm create", exec: "pnpm dlx" };
|
|
75
|
+
if (existsSync(join(cwd, "yarn.lock")))
|
|
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" };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
// Check if a package is already installed (deps or devDeps)
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
function isInstalled(pkg, pkgJson) {
|
|
84
|
+
const deps = pkgJson.dependencies || {};
|
|
85
|
+
const devDeps = pkgJson.devDependencies || {};
|
|
86
|
+
return Boolean(deps[pkg] || devDeps[pkg]);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Sanitize project name for npm
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
function sanitizeName(name) {
|
|
93
|
+
return name
|
|
94
|
+
.toLowerCase()
|
|
95
|
+
.replace(/\s+/g, "-")
|
|
96
|
+
.replace(/[^a-z0-9\-_.]/g, "")
|
|
97
|
+
.replace(/^[.\-_]+/, "");
|
|
98
|
+
}
|
|
99
|
+
|
|
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";
|
|
106
|
+
|
|
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);
|
|
114
|
+
}
|
|
115
|
+
|
|
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);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
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);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
await sleep(300);
|
|
149
|
+
|
|
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);
|
|
162
|
+
}
|
|
163
|
+
|
|
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);
|
|
178
|
+
}
|
|
179
|
+
|
|
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;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ---------------------------------------------------------------------------
|
|
196
|
+
// Main init function
|
|
197
|
+
// ---------------------------------------------------------------------------
|
|
198
|
+
export async function init() {
|
|
199
|
+
let cwd = process.cwd();
|
|
200
|
+
let scaffolded = false;
|
|
201
|
+
let projectName = "";
|
|
202
|
+
|
|
203
|
+
console.log();
|
|
204
|
+
console.log(
|
|
205
|
+
pc.bold(pc.cyan(" ⬡ Orizon")) + pc.dim(" — project setup wizard"),
|
|
206
|
+
);
|
|
207
|
+
console.log();
|
|
208
|
+
log.divider();
|
|
209
|
+
|
|
210
|
+
// -----------------------------------------------------------------------
|
|
211
|
+
// Pre-flight: Detect project or scaffold a new one
|
|
212
|
+
// -----------------------------------------------------------------------
|
|
213
|
+
const pkgPath = join(cwd, "package.json");
|
|
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.");
|
|
224
|
+
process.exit(1);
|
|
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
|
+
}
|
|
237
|
+
|
|
238
|
+
const result = await scaffoldProject(cwd);
|
|
239
|
+
cwd = result.projectDir;
|
|
240
|
+
projectName = result.projectName;
|
|
241
|
+
scaffolded = true;
|
|
242
|
+
|
|
243
|
+
console.log();
|
|
244
|
+
log.divider();
|
|
245
|
+
console.log();
|
|
246
|
+
log.info("Project created! Now configuring Orizon...");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// -----------------------------------------------------------------------
|
|
250
|
+
// From here on, we're in a valid Vite project (either existing or new)
|
|
251
|
+
// -----------------------------------------------------------------------
|
|
252
|
+
const viteConfigPath = findViteConfig(cwd);
|
|
253
|
+
const pm = detectPM(cwd);
|
|
254
|
+
const currentPkgJson = JSON.parse(
|
|
255
|
+
readFileSync(join(cwd, "package.json"), "utf8"),
|
|
256
|
+
);
|
|
257
|
+
|
|
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");
|
|
320
|
+
}
|
|
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
|
+
];
|
|
337
|
+
|
|
338
|
+
const formMissing = formDeps.filter(
|
|
339
|
+
(d) => !isInstalled(d.pkg, currentPkgJson),
|
|
340
|
+
);
|
|
341
|
+
const formAlreadyInstalled = formDeps.filter((d) =>
|
|
342
|
+
isInstalled(d.pkg, currentPkgJson),
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
for (const d of formAlreadyInstalled) {
|
|
346
|
+
log.skip(`${d.pkg} — already installed`);
|
|
347
|
+
}
|
|
348
|
+
|
|
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);
|
|
473
|
+
|
|
474
|
+
const cssSearchOrder = [
|
|
475
|
+
"src/index.css",
|
|
476
|
+
"src/main.css",
|
|
477
|
+
"src/styles.css",
|
|
478
|
+
"src/globals.css",
|
|
479
|
+
"src/app.css",
|
|
480
|
+
"src/App.css",
|
|
481
|
+
];
|
|
482
|
+
|
|
483
|
+
let cssPath = null;
|
|
484
|
+
let cssCreated = false;
|
|
485
|
+
|
|
486
|
+
for (const candidate of cssSearchOrder) {
|
|
487
|
+
const full = join(cwd, candidate);
|
|
488
|
+
if (existsSync(full)) {
|
|
489
|
+
cssPath = full;
|
|
490
|
+
log.info(`Found: ${pc.bold(candidate)}`);
|
|
491
|
+
break;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (!cssPath) {
|
|
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");
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// =======================================================================
|
|
613
|
+
// Done!
|
|
614
|
+
// =======================================================================
|
|
615
|
+
console.log();
|
|
616
|
+
log.divider();
|
|
617
|
+
console.log();
|
|
618
|
+
console.log(pc.green(pc.bold(" ✓ Orizon setup complete!")));
|
|
619
|
+
console.log();
|
|
620
|
+
|
|
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
|
+
);
|
|
632
|
+
console.log();
|
|
633
|
+
console.log(" Example with Form + Zod:");
|
|
634
|
+
console.log(
|
|
635
|
+
pc.dim(" const [form] = Form.useForm({ schema: myZodSchema });"),
|
|
636
|
+
);
|
|
637
|
+
console.log();
|
|
638
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orizon",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Ant Design API on shadcn/ui primitives — 68 components for React with Tailwind CSS",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
"typescript",
|
|
18
18
|
"accessible"
|
|
19
19
|
],
|
|
20
|
+
"bin": {
|
|
21
|
+
"orizon": "./bin/cli.mjs"
|
|
22
|
+
},
|
|
20
23
|
"sideEffects": false,
|
|
21
24
|
"main": "./dist/orizon.cjs.js",
|
|
22
25
|
"module": "./dist/orizon.es.js",
|
|
@@ -31,6 +34,7 @@
|
|
|
31
34
|
},
|
|
32
35
|
"files": [
|
|
33
36
|
"dist",
|
|
37
|
+
"bin",
|
|
34
38
|
"README.md",
|
|
35
39
|
"LICENSE"
|
|
36
40
|
],
|
|
@@ -58,7 +62,9 @@
|
|
|
58
62
|
"optional": true
|
|
59
63
|
}
|
|
60
64
|
},
|
|
61
|
-
"dependencies": {
|
|
65
|
+
"dependencies": {
|
|
66
|
+
"picocolors": "^1.1.1"
|
|
67
|
+
},
|
|
62
68
|
"devDependencies": {
|
|
63
69
|
"@base-ui/react": "^1.2.0",
|
|
64
70
|
"@eslint/js": "^9.39.1",
|