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/LICENSE +21 -0
- package/README.md +192 -0
- package/dist/commands/add.d.ts +4 -0
- package/dist/commands/add.js +166 -0
- package/dist/commands/diff.d.ts +1 -0
- package/dist/commands/diff.js +133 -0
- package/dist/commands/init.d.ts +4 -0
- package/dist/commands/init.js +133 -0
- package/dist/commands/list.d.ts +5 -0
- package/dist/commands/list.js +44 -0
- package/dist/commands/remove.d.ts +1 -0
- package/dist/commands/remove.js +97 -0
- package/dist/config.d.ts +26 -0
- package/dist/config.js +115 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +84 -0
- package/dist/registry.d.ts +19 -0
- package/dist/registry.js +53 -0
- package/dist/utils.d.ts +70 -0
- package/dist/utils.js +320 -0
- package/package.json +56 -0
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
|
+
}
|