nativeui-cli 1.0.0-beta.0

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/utils.js ADDED
@@ -0,0 +1,320 @@
1
+ // src/utils.ts
2
+ import { log } from "@clack/prompts";
3
+ import pc from "picocolors";
4
+ import fs from "fs";
5
+ import path from "path";
6
+ import spawn from "cross-spawn";
7
+ import { DEFAULT_CONFIG } from "./config.js";
8
+ const GRAPHQL_ENDPOINT = "https://cdn.nativeui.qzz.io/graphql";
9
+ async function postGraphQL(query, variables) {
10
+ const response = await fetch(GRAPHQL_ENDPOINT, {
11
+ method: "POST",
12
+ headers: {
13
+ "content-type": "application/json",
14
+ accept: "application/json",
15
+ },
16
+ body: JSON.stringify({
17
+ query,
18
+ variables,
19
+ }),
20
+ });
21
+ if (!response.ok) {
22
+ throw new Error(`Failed to fetch registry data from GraphQL (HTTP ${response.status})`);
23
+ }
24
+ const payload = (await response.json());
25
+ if (payload.errors?.length) {
26
+ throw new Error(payload.errors[0].message ?? "GraphQL request failed");
27
+ }
28
+ if (!payload.data) {
29
+ throw new Error("GraphQL response did not include data");
30
+ }
31
+ return payload.data;
32
+ }
33
+ export async function fetchRegistryEntries(keys) {
34
+ const uniqueKeys = [...new Set(keys.map((key) => key.toLowerCase()))];
35
+ if (uniqueKeys.length === 0)
36
+ return [];
37
+ if (uniqueKeys.length === 1) {
38
+ const data = await postGraphQL(`
39
+ query Registry($key: String!) {
40
+ registry(key: $key) {
41
+ key
42
+ title
43
+ description
44
+ registryDependencies
45
+ dependencies
46
+ category
47
+ files {
48
+ path
49
+ target
50
+ content
51
+ }
52
+ }
53
+ }
54
+ `, { key: uniqueKeys[0] });
55
+ return data.registry ? [data.registry] : [];
56
+ }
57
+ const data = await postGraphQL(`
58
+ query Registries($keys: [String!]!) {
59
+ registries(keys: $keys) {
60
+ key
61
+ title
62
+ description
63
+ registryDependencies
64
+ dependencies
65
+ category
66
+ files {
67
+ path
68
+ target
69
+ content
70
+ }
71
+ }
72
+ }
73
+ `, { keys: uniqueKeys });
74
+ return data.registries ?? [];
75
+ }
76
+ export async function fetchRegistryIndex() {
77
+ const data = await postGraphQL(`
78
+ query Registries {
79
+ registries {
80
+ key
81
+ }
82
+ }
83
+ `, {});
84
+ return (data.registries ?? [])
85
+ .map((entry) => entry.key?.toLowerCase())
86
+ .filter((key) => Boolean(key));
87
+ }
88
+ export async function fetchRegistryClosure(keys) {
89
+ const resolved = new Map();
90
+ const queue = [...new Set(keys.map((key) => key.toLowerCase()))];
91
+ while (queue.length > 0) {
92
+ const batch = queue
93
+ .splice(0, queue.length)
94
+ .filter((key) => !resolved.has(key));
95
+ if (batch.length === 0)
96
+ break;
97
+ const fetched = await fetchRegistryEntries(batch);
98
+ for (const entry of fetched) {
99
+ const normalizedKey = entry.key.toLowerCase();
100
+ if (!resolved.has(normalizedKey)) {
101
+ resolved.set(normalizedKey, { ...entry, key: normalizedKey });
102
+ }
103
+ }
104
+ for (const entry of fetched) {
105
+ for (const dep of entry.registryDependencies ?? []) {
106
+ const normalizedDep = dep.toLowerCase();
107
+ if (!resolved.has(normalizedDep)) {
108
+ queue.push(normalizedDep);
109
+ }
110
+ }
111
+ }
112
+ }
113
+ return [...resolved.values()];
114
+ }
115
+ // ─── Logging helpers ──────────────────────────────────────────
116
+ export function logSuccess(msg) {
117
+ log.success(pc.green(msg));
118
+ }
119
+ export function logError(msg) {
120
+ log.error(pc.red(msg));
121
+ }
122
+ export function logWarn(msg) {
123
+ log.warn(pc.yellow(msg));
124
+ }
125
+ export function logInfo(msg) {
126
+ log.info(pc.cyan(msg));
127
+ }
128
+ /** Print a labelled key → value pair (used in `init` summary). */
129
+ export function logKV(key, value) {
130
+ log.message(`${pc.dim(key.padEnd(18))} ${pc.white(value)}`);
131
+ }
132
+ // ─── Fetch helpers ───────────────────────────────────────────
133
+ /**
134
+ * Fetches the raw source of a component from its registry URL.
135
+ * Throws a descriptive error on non-200 responses.
136
+ */
137
+ export async function fetchComponent(name, url) {
138
+ try {
139
+ const [manifest] = await fetchRegistryEntries([url || name]);
140
+ const content = manifest?.files?.[0]?.content;
141
+ if (typeof content === "string")
142
+ return content;
143
+ throw new Error(`Registry response for ${name} did not include file content`);
144
+ }
145
+ catch (err) {
146
+ throw new Error(`Network error fetching ${name}: ${err.message}`);
147
+ }
148
+ }
149
+ // ─── File helpers ─────────────────────────────────────────────
150
+ /** Ensures a directory exists, creating it recursively if needed. */
151
+ export function ensureDir(dir) {
152
+ if (!fs.existsSync(dir)) {
153
+ fs.mkdirSync(dir, { recursive: true });
154
+ }
155
+ }
156
+ /** Writes text to a file, creating parent dirs as needed. */
157
+ export function writeFile(filePath, content) {
158
+ ensureDir(path.dirname(filePath));
159
+ fs.writeFileSync(filePath, content, "utf-8");
160
+ }
161
+ /** Returns file content or null if the file doesn't exist. */
162
+ export function readFileSafe(filePath) {
163
+ if (!fs.existsSync(filePath))
164
+ return null;
165
+ return fs.readFileSync(filePath, "utf-8");
166
+ }
167
+ // ─── Package manager helpers ──────────────────────────────────
168
+ /** Returns the install command for the given package manager. */
169
+ export function buildInstallCmd(runner, packages) {
170
+ const list = packages.join(" ");
171
+ if (runner === "npx")
172
+ return `npm exec --yes expo install ${list}`;
173
+ return `${runner} expo install ${list}`;
174
+ }
175
+ // 1. Track active process and cancellation state
176
+ let activeInstallProcess = null;
177
+ let isCancelled = false;
178
+ function runInstallCommand(runner, packages, // <-- Now takes an array of packages
179
+ cwd) {
180
+ const command = runner === "npx" ? "npx" : runner;
181
+ // Pass all packages at once: `expo install pkg1 pkg2 pkg3`
182
+ const args = ["expo", "install", ...packages];
183
+ return new Promise((resolve) => {
184
+ activeInstallProcess = spawn(command, args, {
185
+ stdio: "inherit",
186
+ cwd,
187
+ });
188
+ activeInstallProcess.on("error", () => {
189
+ activeInstallProcess = null;
190
+ resolve(false);
191
+ });
192
+ activeInstallProcess.on("exit", (code) => {
193
+ activeInstallProcess = null;
194
+ // If cancelled by the user, return false to prevent success messages
195
+ resolve(code === 0 && !isCancelled);
196
+ });
197
+ });
198
+ }
199
+ /**
200
+ * Installs Expo packages in a single batched command.
201
+ * Returns true on success, false on failure.
202
+ */
203
+ export function installPackages(packages, runner, cwd, onProgress) {
204
+ // ← sync now, no Promise
205
+ if (packages.length === 0)
206
+ return true;
207
+ const command = runner === "npx" ? "npx" : runner;
208
+ const args = ["expo", "install", ...packages];
209
+ onProgress?.(`${packages.length} dependencies`, 0, 1);
210
+ // spawnSync + stdio:'inherit' puts node and the child in the same
211
+ // foreground process group. Ctrl+C from the terminal goes to both
212
+ // simultaneously — the OS handles it, no SIGINT wrangling needed.
213
+ const result = spawn.sync(command, args, {
214
+ stdio: "inherit",
215
+ cwd,
216
+ // no shell:true needed — cross-spawn resolves .cmd files itself
217
+ });
218
+ return result.status === 0;
219
+ }
220
+ /** Detect which Expo runner should be used in the cwd. */
221
+ export function detectExpoRunner() {
222
+ if (fs.existsSync(path.join(process.cwd(), "pnpm-lock.yaml")))
223
+ return "pnpm";
224
+ if (fs.existsSync(path.join(process.cwd(), "bun.lockb")))
225
+ return "bunx";
226
+ if (fs.existsSync(path.join(process.cwd(), "yarn.lock")))
227
+ return "yarn";
228
+ return "npx";
229
+ }
230
+ /** Returns true if this looks like an Expo project. */
231
+ export function isExpoProject() {
232
+ return fs.existsSync(path.join(process.cwd(), "app.json"));
233
+ }
234
+ // ─── Diff helpers ─────────────────────────────────────────────
235
+ /**
236
+ * Very minimal line diff — returns a coloured unified-diff-style string.
237
+ * For a production CLI you'd swap this for the `diff` package.
238
+ */
239
+ export function lineDiff(a, b) {
240
+ const aLines = a.split("\n");
241
+ const bLines = b.split("\n");
242
+ const maxLen = Math.max(aLines.length, bLines.length);
243
+ const output = [];
244
+ for (let i = 0; i < maxLen; i++) {
245
+ const aLine = aLines[i];
246
+ const bLine = bLines[i];
247
+ if (aLine === undefined) {
248
+ output.push(pc.green(`+ ${bLine}`));
249
+ }
250
+ else if (bLine === undefined) {
251
+ output.push(pc.red(`- ${aLine}`));
252
+ }
253
+ else if (aLine !== bLine) {
254
+ output.push(pc.red(`- ${aLine}`));
255
+ output.push(pc.green(`+ ${bLine}`));
256
+ }
257
+ else {
258
+ output.push(pc.dim(` ${aLine}`));
259
+ }
260
+ }
261
+ return output.join("\n");
262
+ }
263
+ /** Returns true if two strings are identical (ignoring line-ending differences). */
264
+ export function isUpToDate(a, b) {
265
+ return a.replace(/\r\n/g, "\n") === b.replace(/\r\n/g, "\n");
266
+ }
267
+ // Add this helper function below `isExpoProject` or anywhere in the Package Manager helpers section.
268
+ /** Extracts the base package name, ignoring version tags (e.g., "pkg@1.0" -> "pkg", "@scope/pkg@latest" -> "@scope/pkg") */
269
+ function getPackageName(pkg) {
270
+ const match = pkg.match(/^(@[^\/]+\/[^@]+|[^@]+)/);
271
+ return match ? match[1] : pkg;
272
+ }
273
+ /** * Resolves the absolute destination path for a registry file, honouring
274
+ * the user-configured `outputDir` from native-ui.json.
275
+ *
276
+ * Registry entries store paths relative to the default output dir
277
+ * (e.g. "components/ui/button.tsx"). When the user has changed
278
+ * `outputDir` to something like "src/ui", this function strips the
279
+ * default prefix and replaces it with the configured one so the file
280
+ * lands in the right place.
281
+ */
282
+ export function resolveComponentDest(file, config, fallback) {
283
+ const rawPath = file?.target ?? file?.path ?? fallback;
284
+ const defaultOutDir = DEFAULT_CONFIG.outputDir; // "components/ui"
285
+ let relativePath;
286
+ if (rawPath === defaultOutDir ||
287
+ rawPath.startsWith(defaultOutDir + "/") ||
288
+ rawPath.startsWith(defaultOutDir + path.sep)) {
289
+ // Swap the registry's hardcoded prefix for the user's configured one.
290
+ const suffix = rawPath.slice(defaultOutDir.length); // includes leading "/" or ""
291
+ relativePath = config.outputDir + suffix;
292
+ }
293
+ else {
294
+ // Path is already custom / absolute — use as-is.
295
+ relativePath = rawPath;
296
+ }
297
+ return path.resolve(process.cwd(), relativePath);
298
+ }
299
+ export function getMissingPackages(packages, cwd) {
300
+ const pkgJsonPath = path.join(cwd, "package.json");
301
+ if (!fs.existsSync(pkgJsonPath)) {
302
+ return packages; // If no package.json is found, assume we need to install everything
303
+ }
304
+ try {
305
+ const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
306
+ const allInstalledDeps = {
307
+ ...(pkgJson.dependencies || {}),
308
+ ...(pkgJson.devDependencies || {}),
309
+ ...(pkgJson.peerDependencies || {}),
310
+ };
311
+ return packages.filter((pkg) => {
312
+ const name = getPackageName(pkg);
313
+ return !allInstalledDeps[name];
314
+ });
315
+ }
316
+ catch (err) {
317
+ // If parsing fails, default to installing everything to be safe
318
+ return packages;
319
+ }
320
+ }
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "nativeui-cli",
3
+ "version": "1.0.0-beta.0",
4
+ "description": "Add beautiful components to your Expo app.",
5
+ "keywords": [
6
+ "expo",
7
+ "react-native",
8
+ "ui",
9
+ "components",
10
+ "cli"
11
+ ],
12
+ "author": "Kishan Agarwal",
13
+ "license": "MIT",
14
+ "type": "module",
15
+ "bin": {
16
+ "nativeui-cli": "dist/index.js"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/Kishan-Agarwal-28/native-ui.git",
21
+ "directory": "apps/cli"
22
+ },
23
+ "files": [
24
+ "dist"
25
+ ],
26
+ "scripts": {
27
+ "build": "tsc",
28
+ "dev": "tsx src/index.ts",
29
+ "typecheck": "tsc --noEmit"
30
+ },
31
+ "dependencies": {
32
+ "@clack/prompts": "^0.7.0",
33
+ "chalk": "^5.3.0",
34
+ "commander": "^12.0.0",
35
+ "cross-spawn": "^7.0.6",
36
+ "fast-diff": "^1.3.0",
37
+ "ora": "^8.0.1",
38
+ "picocolors": "^1.0.1"
39
+ },
40
+ "devDependencies": {
41
+ "@types/cross-spawn": "^6.0.6",
42
+ "@types/node": "^20.0.0",
43
+ "tsx": "^4.7.0",
44
+ "typescript": "^5.4.0"
45
+ },
46
+ "engines": {
47
+ "node": ">=18.0.0"
48
+ },
49
+ "publishConfig": {
50
+ "access": "public"
51
+ },
52
+ "homepage": "https://github.com/Kishan-Agarwal-28/native-ui/tree/main/apps/cli#readme",
53
+ "bugs": {
54
+ "url": "https://github.com/Kishan-Agarwal-28/native-ui/issues"
55
+ }
56
+ }