@rootnative/cli 0.0.0-alpha.3 → 0.0.0-alpha.5
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/README.md +36 -8
- package/dist/index.mjs +338 -163
- package/llms.txt +8 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -69,8 +69,11 @@ This will:
|
|
|
69
69
|
2. Detect your package manager (npm, yarn, pnpm, bun)
|
|
70
70
|
3. Detect path aliases from your `tsconfig.json`
|
|
71
71
|
4. Prompt for component and utility output directories
|
|
72
|
-
5.
|
|
73
|
-
6.
|
|
72
|
+
5. Create an `rootnative.json` config file
|
|
73
|
+
6. Offer to add the LLM-docs pointer to your project's `CLAUDE.md`
|
|
74
|
+
7. Offer to install `@rootnative/core`
|
|
75
|
+
|
|
76
|
+
Steps 6 and 7 are confirmation prompts; `-y` accepts both.
|
|
74
77
|
|
|
75
78
|
Options:
|
|
76
79
|
|
|
@@ -98,7 +101,7 @@ Add one or more components to your project.
|
|
|
98
101
|
```bash
|
|
99
102
|
npx rootnative add button
|
|
100
103
|
npx rootnative add card chip text-field
|
|
101
|
-
npx rootnative add appbar # auto-adds icon-button + typography dependencies
|
|
104
|
+
npx rootnative add appbar # auto-adds button + icon-button + typography dependencies
|
|
102
105
|
```
|
|
103
106
|
|
|
104
107
|
Options:
|
|
@@ -111,7 +114,7 @@ Options:
|
|
|
111
114
|
|
|
112
115
|
The `add` command:
|
|
113
116
|
|
|
114
|
-
1. Resolves the full dependency graph (e.g. `appbar` requires `icon-button` and `typography`)
|
|
117
|
+
1. Resolves the full dependency graph (e.g. `appbar` requires `button`, `icon-button` and `typography`)
|
|
115
118
|
2. Shows a plan of components, utilities, and npm packages to install
|
|
116
119
|
3. Copies component files with import paths rewritten to match your project
|
|
117
120
|
4. Generates a utility barrel file (`rootnative-utils.ts`)
|
|
@@ -161,6 +164,8 @@ The `upgrade` command:
|
|
|
161
164
|
5. Upgrades `@rootnative/core` and installs any new required peer dependencies
|
|
162
165
|
6. Reports optional peer deps that aren't installed (does not auto-install them)
|
|
163
166
|
7. Lists peer deps that are no longer required so you can remove them manually
|
|
167
|
+
8. Moves `registryVersion` in `rootnative.json` forward to the new release tag, so subsequent `add`/`update` calls fetch matching sources
|
|
168
|
+
9. With `-a, --all`, also updates the installed component files
|
|
164
169
|
|
|
165
170
|
Non-interactive mode for CI/automation:
|
|
166
171
|
|
|
@@ -190,7 +195,8 @@ Checks include:
|
|
|
190
195
|
- `@rootnative/core` is installed
|
|
191
196
|
- React Native version compatibility
|
|
192
197
|
- Installed component file integrity
|
|
193
|
-
-
|
|
198
|
+
- `@rootnative/inertia` — a hard failure when missing, since every animated component needs it
|
|
199
|
+
- Optional peer dependencies (`react-native-safe-area-context`, `@expo/vector-icons`) — reported as warnings
|
|
194
200
|
|
|
195
201
|
## Configuration
|
|
196
202
|
|
|
@@ -204,7 +210,7 @@ Checks include:
|
|
|
204
210
|
"lib": "@/lib"
|
|
205
211
|
},
|
|
206
212
|
"registryUrl": "https://raw.githubusercontent.com/rootnative/ui",
|
|
207
|
-
"registryVersion": "
|
|
213
|
+
"registryVersion": "v0.0.0-alpha.4"
|
|
208
214
|
}
|
|
209
215
|
```
|
|
210
216
|
|
|
@@ -213,7 +219,7 @@ Checks include:
|
|
|
213
219
|
| `aliases.components` | Where component directories are created |
|
|
214
220
|
| `aliases.lib` | Where utility files are placed |
|
|
215
221
|
| `registryUrl` | Base URL for fetching component source files |
|
|
216
|
-
| `registryVersion` | Git ref to fetch from (branch, tag, or commit) |
|
|
222
|
+
| `registryVersion` | Git ref to fetch from (branch, tag, or commit). `init` pins this to the `v<version>` tag of the latest published release, falling back to `main` only when npm is unreachable or the tag isn't pushed yet. `upgrade` moves the pin forward |
|
|
217
223
|
|
|
218
224
|
## Output structure
|
|
219
225
|
|
|
@@ -228,28 +234,41 @@ src/
|
|
|
228
234
|
types.ts
|
|
229
235
|
styles.ts
|
|
230
236
|
index.ts
|
|
237
|
+
elevationShadow.ts # shared internal, flattened per component
|
|
238
|
+
usePressMorph.ts
|
|
239
|
+
useStateLayer.ts
|
|
231
240
|
icon-button/ # auto-added (appbar dependency)
|
|
232
241
|
IconButton.tsx
|
|
233
242
|
types.ts
|
|
234
243
|
styles.ts
|
|
235
244
|
index.ts
|
|
245
|
+
useBooleanProgress.ts
|
|
246
|
+
usePressMorph.ts
|
|
247
|
+
useStateLayer.ts
|
|
236
248
|
typography/ # auto-added (appbar dependency)
|
|
237
249
|
Typography.tsx
|
|
238
250
|
types.ts
|
|
251
|
+
styles.ts
|
|
239
252
|
index.ts
|
|
240
253
|
appbar/
|
|
241
254
|
AppBar.tsx
|
|
242
255
|
types.ts
|
|
243
256
|
styles.ts
|
|
244
257
|
index.ts
|
|
258
|
+
safe-area.tsx
|
|
245
259
|
lib/
|
|
246
260
|
color.ts
|
|
247
261
|
elevation.ts
|
|
248
|
-
|
|
262
|
+
pressable.ts
|
|
263
|
+
render-icon.tsx
|
|
249
264
|
rtl.ts
|
|
250
265
|
rootnative-utils.ts # generated barrel
|
|
251
266
|
```
|
|
252
267
|
|
|
268
|
+
Shared internal hooks (`useStateLayer`, `usePressMorph`, …) are copied into
|
|
269
|
+
*each* component directory that uses them rather than into a shared folder, so
|
|
270
|
+
each installed component stays self-contained.
|
|
271
|
+
|
|
253
272
|
## Available components
|
|
254
273
|
|
|
255
274
|
| Component | Description |
|
|
@@ -271,7 +290,16 @@ src/
|
|
|
271
290
|
| `text-field` | Text input with animated floating label, 2 variants (filled, outlined) |
|
|
272
291
|
| `layout` | Layout primitives: Box, Row, Column, Grid, and SafeAreaView wrapper |
|
|
273
292
|
| `list` | List container with interactive items and dividers |
|
|
293
|
+
| `divider` | Horizontal or vertical 1dp rule with optional leading/trailing insets and thickness/color overrides. |
|
|
294
|
+
| `loading-indicator` | MD3 Expressive shape-morphing loading spinner (contained + uncontained, determinate + indeterminate) |
|
|
274
295
|
| `portal` | Render children into a host elsewhere in the tree (overlays, dialogs, sheets) |
|
|
296
|
+
| `dialog` | Basic and full-screen modal dialogs with Icon / Title / Content / Actions slots, scrim, and Android back handling. |
|
|
297
|
+
| `snackbar` | Imperative snackbar queue — SnackbarProvider plus useSnackbar() with actions, durations, and safe-area aware placement. |
|
|
298
|
+
| `menu` | Anchored dropdown menu (Menu + Menu.Item) that flips and shifts to stay on screen, with self-managing or controlled visibility. |
|
|
299
|
+
| `tooltip` | Plain and rich tooltips anchored to a control, shown on hover or a long press |
|
|
300
|
+
| `bottom-sheet` | MD3 bottom sheet — modal (scrim) and standard variants, drag handle, velocity-based snap points, drag-to-dismiss. |
|
|
301
|
+
| `tabs` | Primary and secondary tab rows, fixed or scrollable, with a sliding active indicator |
|
|
302
|
+
| `navigation-bar` | MD3 navigation bar — 80dp bottom destination bar with an animated indicator pill |
|
|
275
303
|
| `keyboard-avoiding-wrapper` | Zero-config keyboard-aware wrapper for form layouts |
|
|
276
304
|
|
|
277
305
|
## Import rewriting
|
package/dist/index.mjs
CHANGED
|
@@ -45,15 +45,96 @@ function resolveAliasPath(alias, cwd) {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
// src/lib/detector.ts
|
|
48
|
+
import path3 from "path";
|
|
49
|
+
import fs3 from "fs-extra";
|
|
50
|
+
|
|
51
|
+
// src/lib/tsconfig-paths.ts
|
|
48
52
|
import path2 from "path";
|
|
49
53
|
import fs2 from "fs-extra";
|
|
54
|
+
function stripJsonComments(raw) {
|
|
55
|
+
let out = "";
|
|
56
|
+
let inString = false;
|
|
57
|
+
let escaped = false;
|
|
58
|
+
for (let i = 0; i < raw.length; i++) {
|
|
59
|
+
const char = raw[i];
|
|
60
|
+
if (inString) {
|
|
61
|
+
out += char;
|
|
62
|
+
if (escaped) {
|
|
63
|
+
escaped = false;
|
|
64
|
+
} else if (char === "\\") {
|
|
65
|
+
escaped = true;
|
|
66
|
+
} else if (char === '"') {
|
|
67
|
+
inString = false;
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (char === '"') {
|
|
72
|
+
inString = true;
|
|
73
|
+
out += char;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (char === "/" && raw[i + 1] === "*") {
|
|
77
|
+
const end = raw.indexOf("*/", i + 2);
|
|
78
|
+
i = end === -1 ? raw.length : end + 1;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (char === "/" && raw[i + 1] === "/") {
|
|
82
|
+
while (i < raw.length && raw[i] !== "\n") i++;
|
|
83
|
+
out += "\n";
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
out += char;
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
function parseAliasPrefix(alias) {
|
|
91
|
+
const match = /^([^./\\][^/\\]*)\//.exec(alias);
|
|
92
|
+
if (!match) return null;
|
|
93
|
+
return { prefix: match[1], target: "src" };
|
|
94
|
+
}
|
|
95
|
+
async function ensureTsconfigPaths(cwd, alias) {
|
|
96
|
+
const parsed = parseAliasPrefix(alias);
|
|
97
|
+
if (!parsed) return { status: "not-an-alias" };
|
|
98
|
+
const { prefix, target } = parsed;
|
|
99
|
+
const tsconfigPath = path2.resolve(cwd, "tsconfig.json");
|
|
100
|
+
if (!await fs2.pathExists(tsconfigPath)) {
|
|
101
|
+
return { status: "no-tsconfig" };
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
const raw = await fs2.readFile(tsconfigPath, "utf-8");
|
|
105
|
+
const tsconfig = JSON.parse(stripJsonComments(raw));
|
|
106
|
+
const key = `${prefix}/*`;
|
|
107
|
+
const existing = tsconfig.compilerOptions?.paths;
|
|
108
|
+
if (existing && key in existing) {
|
|
109
|
+
return { status: "already-mapped", prefix };
|
|
110
|
+
}
|
|
111
|
+
const compilerOptions = tsconfig.compilerOptions ?? {};
|
|
112
|
+
tsconfig.compilerOptions = {
|
|
113
|
+
...compilerOptions,
|
|
114
|
+
baseUrl: compilerOptions.baseUrl ?? ".",
|
|
115
|
+
paths: {
|
|
116
|
+
...existing,
|
|
117
|
+
[key]: [`./${target}/*`]
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
await fs2.writeJSON(tsconfigPath, tsconfig, { spaces: 2 });
|
|
121
|
+
return { status: "added", prefix, target };
|
|
122
|
+
} catch (error) {
|
|
123
|
+
return {
|
|
124
|
+
status: "failed",
|
|
125
|
+
error: error instanceof Error ? error.message : String(error)
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// src/lib/detector.ts
|
|
50
131
|
async function detectProjectType(cwd) {
|
|
51
|
-
const pkgPath =
|
|
52
|
-
const hasPackageJson = await
|
|
132
|
+
const pkgPath = path3.resolve(cwd, "package.json");
|
|
133
|
+
const hasPackageJson = await fs3.pathExists(pkgPath);
|
|
53
134
|
if (!hasPackageJson) {
|
|
54
135
|
return "unknown";
|
|
55
136
|
}
|
|
56
|
-
const pkg = await
|
|
137
|
+
const pkg = await fs3.readJSON(pkgPath);
|
|
57
138
|
const allDeps = {
|
|
58
139
|
...pkg.dependencies,
|
|
59
140
|
...pkg.devDependencies
|
|
@@ -75,37 +156,33 @@ async function detectPackageManager(cwd) {
|
|
|
75
156
|
["package-lock.json", "npm"]
|
|
76
157
|
];
|
|
77
158
|
for (const [file, manager] of lockFiles) {
|
|
78
|
-
if (await
|
|
159
|
+
if (await fs3.pathExists(path3.resolve(cwd, file))) {
|
|
79
160
|
return manager;
|
|
80
161
|
}
|
|
81
162
|
}
|
|
82
163
|
return "npm";
|
|
83
164
|
}
|
|
84
165
|
async function detectTypeScript(cwd) {
|
|
85
|
-
return
|
|
166
|
+
return fs3.pathExists(path3.resolve(cwd, "tsconfig.json"));
|
|
86
167
|
}
|
|
87
168
|
async function detectSrcDir(cwd) {
|
|
88
169
|
const candidates = ["src", "app"];
|
|
89
170
|
for (const dir of candidates) {
|
|
90
|
-
const dirPath =
|
|
91
|
-
if (await
|
|
171
|
+
const dirPath = path3.resolve(cwd, dir);
|
|
172
|
+
if (await fs3.pathExists(dirPath)) {
|
|
92
173
|
return dir;
|
|
93
174
|
}
|
|
94
175
|
}
|
|
95
176
|
return null;
|
|
96
177
|
}
|
|
97
178
|
async function detectAliases(cwd) {
|
|
98
|
-
const tsconfigPath =
|
|
99
|
-
if (!await
|
|
179
|
+
const tsconfigPath = path3.resolve(cwd, "tsconfig.json");
|
|
180
|
+
if (!await fs3.pathExists(tsconfigPath)) {
|
|
100
181
|
return null;
|
|
101
182
|
}
|
|
102
183
|
try {
|
|
103
|
-
const raw = await
|
|
104
|
-
const
|
|
105
|
-
/\/\*[\s\S]*?\*\/|\/\/.*/g,
|
|
106
|
-
""
|
|
107
|
-
);
|
|
108
|
-
const tsconfig = JSON.parse(stripped);
|
|
184
|
+
const raw = await fs3.readFile(tsconfigPath, "utf-8");
|
|
185
|
+
const tsconfig = JSON.parse(stripJsonComments(raw));
|
|
109
186
|
const paths = tsconfig.compilerOptions?.paths;
|
|
110
187
|
if (!paths) {
|
|
111
188
|
return null;
|
|
@@ -147,9 +224,9 @@ function getInstallCommand(pm, packages) {
|
|
|
147
224
|
}
|
|
148
225
|
|
|
149
226
|
// src/lib/installer.ts
|
|
150
|
-
import
|
|
227
|
+
import path4 from "path";
|
|
151
228
|
import { execa } from "execa";
|
|
152
|
-
import
|
|
229
|
+
import fs4 from "fs-extra";
|
|
153
230
|
|
|
154
231
|
// src/lib/logger.ts
|
|
155
232
|
import chalk from "chalk";
|
|
@@ -266,6 +343,7 @@ function getComponentNames(result) {
|
|
|
266
343
|
// src/lib/transform.ts
|
|
267
344
|
var SINGLE_LINE_IMPORT_REGEX = /((?:import|export)\s+(?:type\s+)?(?:\{[^}]*\}|\*\s+as\s+\w+|[\w,\s]+)\s+from\s+)(['"])([^'"]+)\2/g;
|
|
268
345
|
var MULTI_LINE_IMPORT_REGEX = /((?:import|export)\s+(?:type\s+)?\{[\s\S]*?\}\s+from\s+)(['"])([^'"]+)\2/g;
|
|
346
|
+
var SHARED_ROOT_MODULES = /* @__PURE__ */ new Set(["safe-area"]);
|
|
269
347
|
function transformImports(source, options) {
|
|
270
348
|
const { config, installedComponents } = options;
|
|
271
349
|
const componentsAlias = config.aliases.components;
|
|
@@ -281,6 +359,10 @@ function transformImports(source, options) {
|
|
|
281
359
|
const restOfPath = importPath.replace(/^\.\.\/internal\//, "");
|
|
282
360
|
return `${prefix}${quote}./${restOfPath}${quote}`;
|
|
283
361
|
}
|
|
362
|
+
if (SHARED_ROOT_MODULES.has(importPath.replace(/^\.\.\//, ""))) {
|
|
363
|
+
const restOfPath = importPath.replace(/^\.\.\//, "");
|
|
364
|
+
return `${prefix}${quote}./${restOfPath}${quote}`;
|
|
365
|
+
}
|
|
284
366
|
if (importPath.startsWith("../")) {
|
|
285
367
|
const targetComponent = extractComponentName(importPath);
|
|
286
368
|
if (targetComponent && installedComponents.includes(targetComponent)) {
|
|
@@ -328,8 +410,8 @@ async function installComponents(options) {
|
|
|
328
410
|
await generateBarrel(resolution, utilsRegistry, libDir);
|
|
329
411
|
spinner.succeed("Utility files copied");
|
|
330
412
|
for (const { entry } of resolution.components) {
|
|
331
|
-
const componentDir =
|
|
332
|
-
const exists = await
|
|
413
|
+
const componentDir = path4.join(componentsDir, entry.name);
|
|
414
|
+
const exists = await fs4.pathExists(componentDir);
|
|
333
415
|
if (exists && !force) {
|
|
334
416
|
logger.warn(
|
|
335
417
|
`${entry.name} already exists, skipping (use --force to overwrite)`
|
|
@@ -338,17 +420,17 @@ async function installComponents(options) {
|
|
|
338
420
|
}
|
|
339
421
|
const compSpinner = createSpinner(`Adding ${entry.name}...`);
|
|
340
422
|
compSpinner.start();
|
|
341
|
-
await
|
|
423
|
+
await fs4.ensureDir(componentDir);
|
|
342
424
|
for (const filePath of entry.files) {
|
|
343
425
|
const content = await fetchFileContent(config, filePath);
|
|
344
|
-
const fileName =
|
|
426
|
+
const fileName = path4.basename(filePath);
|
|
345
427
|
const transformed = transformImports(content, {
|
|
346
428
|
config,
|
|
347
429
|
componentName: entry.name,
|
|
348
430
|
installedComponents: allComponentNames
|
|
349
431
|
});
|
|
350
|
-
await
|
|
351
|
-
|
|
432
|
+
await fs4.writeFile(
|
|
433
|
+
path4.join(componentDir, fileName),
|
|
352
434
|
transformed,
|
|
353
435
|
"utf-8"
|
|
354
436
|
);
|
|
@@ -374,13 +456,13 @@ async function installComponents(options) {
|
|
|
374
456
|
}
|
|
375
457
|
}
|
|
376
458
|
async function copyUtilFiles(config, resolution, utilsRegistry, libDir) {
|
|
377
|
-
await
|
|
459
|
+
await fs4.ensureDir(libDir);
|
|
378
460
|
for (const utilName of resolution.utils) {
|
|
379
461
|
const utilEntry = utilsRegistry[utilName];
|
|
380
462
|
if (!utilEntry) continue;
|
|
381
463
|
const content = await fetchFileContent(config, utilEntry.file);
|
|
382
|
-
const fileName =
|
|
383
|
-
await
|
|
464
|
+
const fileName = path4.basename(utilEntry.file);
|
|
465
|
+
await fs4.writeFile(path4.join(libDir, fileName), content, "utf-8");
|
|
384
466
|
}
|
|
385
467
|
}
|
|
386
468
|
async function generateBarrel(resolution, utilsRegistry, libDir) {
|
|
@@ -400,18 +482,18 @@ async function generateBarrel(resolution, utilsRegistry, libDir) {
|
|
|
400
482
|
utilExports,
|
|
401
483
|
utilTypeExports
|
|
402
484
|
);
|
|
403
|
-
await
|
|
404
|
-
|
|
485
|
+
await fs4.writeFile(
|
|
486
|
+
path4.join(libDir, "rootnative-utils.ts"),
|
|
405
487
|
barrelContent,
|
|
406
488
|
"utf-8"
|
|
407
489
|
);
|
|
408
490
|
}
|
|
409
491
|
function collectDepsToInstall(resolution, cwd) {
|
|
410
492
|
const deps = [];
|
|
411
|
-
const pkgPath =
|
|
493
|
+
const pkgPath = path4.resolve(cwd, "package.json");
|
|
412
494
|
let existingDeps = {};
|
|
413
495
|
try {
|
|
414
|
-
const pkg =
|
|
496
|
+
const pkg = fs4.readJSONSync(pkgPath);
|
|
415
497
|
existingDeps = {
|
|
416
498
|
...pkg.dependencies,
|
|
417
499
|
...pkg.devDependencies
|
|
@@ -478,15 +560,17 @@ async function addCommand(componentNames, cwd, options) {
|
|
|
478
560
|
logger.info("Dry run complete. No files were written.");
|
|
479
561
|
return;
|
|
480
562
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
563
|
+
if (!options.yes) {
|
|
564
|
+
const { proceed } = await prompts({
|
|
565
|
+
type: "confirm",
|
|
566
|
+
name: "proceed",
|
|
567
|
+
message: "Proceed with installation?",
|
|
568
|
+
initial: true
|
|
569
|
+
});
|
|
570
|
+
if (!proceed) {
|
|
571
|
+
logger.info("Cancelled.");
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
490
574
|
}
|
|
491
575
|
const project = await detectProject(cwd);
|
|
492
576
|
const pm = options.packageManager ?? project.packageManager;
|
|
@@ -505,10 +589,10 @@ async function addCommand(componentNames, cwd, options) {
|
|
|
505
589
|
}
|
|
506
590
|
|
|
507
591
|
// src/commands/create.ts
|
|
508
|
-
import
|
|
592
|
+
import path5 from "path";
|
|
509
593
|
import chalk3 from "chalk";
|
|
510
594
|
import { execa as execa2 } from "execa";
|
|
511
|
-
import
|
|
595
|
+
import fs5 from "fs-extra";
|
|
512
596
|
import prompts2 from "prompts";
|
|
513
597
|
var NPM_REGISTRY = "https://registry.npmjs.org";
|
|
514
598
|
var ROOTNATIVE_PACKAGES = ["@rootnative/core", "@rootnative/components"];
|
|
@@ -565,7 +649,7 @@ var TEMPLATE_BINARY_FILES = [
|
|
|
565
649
|
"assets/adaptive-icon.png",
|
|
566
650
|
"assets/favicon.png"
|
|
567
651
|
];
|
|
568
|
-
var TEMPLATE_OPTIONAL_TEXT_FILES = ["CLAUDE.md"];
|
|
652
|
+
var TEMPLATE_OPTIONAL_TEXT_FILES = ["CLAUDE.md", "README.md"];
|
|
569
653
|
function isValidTemplate(value) {
|
|
570
654
|
return value in TEMPLATE_CONFIGS;
|
|
571
655
|
}
|
|
@@ -695,8 +779,8 @@ async function createCommand(name, options = {}) {
|
|
|
695
779
|
}
|
|
696
780
|
packageManager = value;
|
|
697
781
|
}
|
|
698
|
-
const targetDir =
|
|
699
|
-
if (await
|
|
782
|
+
const targetDir = path5.resolve(process.cwd(), projectName);
|
|
783
|
+
if (await fs5.pathExists(targetDir)) {
|
|
700
784
|
if (options.yes) {
|
|
701
785
|
logger.warn(`Directory ${chalk3.bold(projectName)} already exists.`);
|
|
702
786
|
process.exit(1);
|
|
@@ -711,7 +795,7 @@ async function createCommand(name, options = {}) {
|
|
|
711
795
|
logger.info("Create cancelled.");
|
|
712
796
|
return;
|
|
713
797
|
}
|
|
714
|
-
await
|
|
798
|
+
await fs5.remove(targetDir);
|
|
715
799
|
}
|
|
716
800
|
const templateConfig = TEMPLATE_CONFIGS[templateName];
|
|
717
801
|
const { baseUrl, pinnedVersion } = await resolveTemplateSource();
|
|
@@ -720,7 +804,7 @@ async function createCommand(name, options = {}) {
|
|
|
720
804
|
spinner.start();
|
|
721
805
|
try {
|
|
722
806
|
for (const dir of templateConfig.dirs) {
|
|
723
|
-
await
|
|
807
|
+
await fs5.ensureDir(path5.join(targetDir, dir));
|
|
724
808
|
}
|
|
725
809
|
for (const file of templateConfig.textFiles) {
|
|
726
810
|
let content = await fetchText(`${templateBaseUrl}/${file}`);
|
|
@@ -745,18 +829,18 @@ async function createCommand(name, options = {}) {
|
|
|
745
829
|
}
|
|
746
830
|
content = JSON.stringify(appJson, null, 2) + "\n";
|
|
747
831
|
}
|
|
748
|
-
await
|
|
832
|
+
await fs5.outputFile(path5.join(targetDir, file), content);
|
|
749
833
|
}
|
|
750
834
|
for (const file of TEMPLATE_OPTIONAL_TEXT_FILES) {
|
|
751
835
|
const content = await fetchTextOptional(`${templateBaseUrl}/${file}`);
|
|
752
836
|
if (content !== null) {
|
|
753
|
-
await
|
|
837
|
+
await fs5.outputFile(path5.join(targetDir, file), content);
|
|
754
838
|
}
|
|
755
839
|
}
|
|
756
840
|
for (const file of TEMPLATE_BINARY_FILES) {
|
|
757
841
|
const buffer = await fetchBinary(`${templateBaseUrl}/${file}`);
|
|
758
842
|
if (buffer) {
|
|
759
|
-
await
|
|
843
|
+
await fs5.outputFile(path5.join(targetDir, file), buffer);
|
|
760
844
|
}
|
|
761
845
|
}
|
|
762
846
|
spinner.succeed("Project created");
|
|
@@ -805,28 +889,34 @@ async function createCommand(name, options = {}) {
|
|
|
805
889
|
}
|
|
806
890
|
|
|
807
891
|
// src/commands/doctor.ts
|
|
808
|
-
import
|
|
892
|
+
import path6 from "path";
|
|
809
893
|
import chalk4 from "chalk";
|
|
810
|
-
import
|
|
894
|
+
import fs6 from "fs-extra";
|
|
811
895
|
function logCheck(status, message) {
|
|
812
|
-
const icon = status === "pass" ? chalk4.green("[pass]") : status === "warn" ? chalk4.yellow("[warn]") : chalk4.red("[fail]");
|
|
896
|
+
const icon = status === "pass" ? chalk4.green("[pass]") : status === "warn" ? chalk4.yellow("[warn]") : status === "info" ? chalk4.cyan("[info]") : chalk4.red("[fail]");
|
|
813
897
|
console.log(` ${icon} ${message}`);
|
|
814
898
|
}
|
|
899
|
+
var REQUIRED_BARREL_PEERS = [
|
|
900
|
+
{
|
|
901
|
+
name: "react-native-svg",
|
|
902
|
+
reason: "the @rootnative/components barrel requires it at load time (circular-progress, loading-indicator)"
|
|
903
|
+
}
|
|
904
|
+
];
|
|
815
905
|
async function doctorCommand(cwd) {
|
|
816
906
|
logger.break();
|
|
817
907
|
console.log(chalk4.bold("RootNative Doctor"));
|
|
818
908
|
logger.break();
|
|
819
909
|
let issues = 0;
|
|
820
|
-
|
|
910
|
+
const initialized = await configExists(cwd);
|
|
911
|
+
if (initialized) {
|
|
821
912
|
logCheck("pass", "rootnative.json found");
|
|
822
913
|
} else {
|
|
823
|
-
logCheck(
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
return;
|
|
914
|
+
logCheck(
|
|
915
|
+
"info",
|
|
916
|
+
'Not initialized for the CLI workflow (no rootnative.json). Run "rootnative init" to copy component source.'
|
|
917
|
+
);
|
|
828
918
|
}
|
|
829
|
-
const config = await readConfig(cwd);
|
|
919
|
+
const config = initialized ? await readConfig(cwd) : null;
|
|
830
920
|
const project = await detectProject(cwd);
|
|
831
921
|
if (project.type !== "unknown") {
|
|
832
922
|
logCheck(
|
|
@@ -837,9 +927,9 @@ async function doctorCommand(cwd) {
|
|
|
837
927
|
logCheck("fail", "Not a React Native or Expo project");
|
|
838
928
|
issues++;
|
|
839
929
|
}
|
|
840
|
-
const pkgPath =
|
|
841
|
-
if (await
|
|
842
|
-
const pkg = await
|
|
930
|
+
const pkgPath = path6.resolve(cwd, "package.json");
|
|
931
|
+
if (await fs6.pathExists(pkgPath)) {
|
|
932
|
+
const pkg = await fs6.readJSON(pkgPath);
|
|
843
933
|
const allDeps = {
|
|
844
934
|
...pkg.dependencies,
|
|
845
935
|
...pkg.devDependencies
|
|
@@ -852,15 +942,15 @@ async function doctorCommand(cwd) {
|
|
|
852
942
|
issues++;
|
|
853
943
|
}
|
|
854
944
|
}
|
|
855
|
-
const corePkgPath =
|
|
945
|
+
const corePkgPath = path6.resolve(
|
|
856
946
|
cwd,
|
|
857
947
|
"node_modules",
|
|
858
948
|
"@rootnative",
|
|
859
949
|
"core",
|
|
860
950
|
"package.json"
|
|
861
951
|
);
|
|
862
|
-
if (await
|
|
863
|
-
const corePkg = await
|
|
952
|
+
if (await fs6.pathExists(corePkgPath)) {
|
|
953
|
+
const corePkg = await fs6.readJSON(corePkgPath);
|
|
864
954
|
logCheck("pass", `@rootnative/core@${corePkg.version} installed`);
|
|
865
955
|
} else {
|
|
866
956
|
logCheck(
|
|
@@ -877,65 +967,67 @@ async function doctorCommand(cwd) {
|
|
|
877
967
|
"TypeScript not detected. RootNative components use TypeScript."
|
|
878
968
|
);
|
|
879
969
|
}
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
const
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
}
|
|
891
|
-
if (componentDirs.length > 0) {
|
|
892
|
-
let integrityOk = true;
|
|
893
|
-
for (const dir of componentDirs) {
|
|
894
|
-
const indexPath = path5.join(componentsDir, dir, "index.ts");
|
|
895
|
-
if (!await fs5.pathExists(indexPath)) {
|
|
896
|
-
logCheck("warn", `Component ${dir} is missing index.ts`);
|
|
897
|
-
integrityOk = false;
|
|
970
|
+
if (config) {
|
|
971
|
+
const componentsDir = resolveAliasPath(config.aliases.components, cwd);
|
|
972
|
+
if (await fs6.pathExists(componentsDir)) {
|
|
973
|
+
const dirs = await fs6.readdir(componentsDir);
|
|
974
|
+
const componentDirs = [];
|
|
975
|
+
for (const dir of dirs) {
|
|
976
|
+
const fullPath = path6.join(componentsDir, dir);
|
|
977
|
+
const stat = await fs6.stat(fullPath);
|
|
978
|
+
if (stat.isDirectory()) {
|
|
979
|
+
componentDirs.push(dir);
|
|
898
980
|
}
|
|
899
981
|
}
|
|
900
|
-
if (
|
|
982
|
+
if (componentDirs.length > 0) {
|
|
983
|
+
let integrityOk = true;
|
|
984
|
+
for (const dir of componentDirs) {
|
|
985
|
+
const indexPath = path6.join(componentsDir, dir, "index.ts");
|
|
986
|
+
if (!await fs6.pathExists(indexPath)) {
|
|
987
|
+
logCheck("warn", `Component ${dir} is missing index.ts`);
|
|
988
|
+
integrityOk = false;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
if (integrityOk) {
|
|
992
|
+
logCheck(
|
|
993
|
+
"pass",
|
|
994
|
+
`${componentDirs.length} component(s) installed, all files present`
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
} else {
|
|
901
998
|
logCheck(
|
|
902
|
-
"
|
|
903
|
-
|
|
999
|
+
"warn",
|
|
1000
|
+
'No components installed yet. Run "rootnative add <component>".'
|
|
904
1001
|
);
|
|
905
1002
|
}
|
|
906
1003
|
} else {
|
|
907
1004
|
logCheck(
|
|
908
1005
|
"warn",
|
|
909
|
-
|
|
1006
|
+
`Components directory not found at ${config.aliases.components}`
|
|
910
1007
|
);
|
|
911
1008
|
}
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
} else {
|
|
923
|
-
if (await fs5.pathExists(componentsDir)) {
|
|
924
|
-
const dirs = await fs5.readdir(componentsDir);
|
|
925
|
-
if (dirs.length > 0) {
|
|
926
|
-
logCheck("warn", "Utility barrel file (rootnative-utils.ts) missing");
|
|
1009
|
+
const libDir = resolveAliasPath(config.aliases.lib, cwd);
|
|
1010
|
+
const barrelPath = path6.join(libDir, "rootnative-utils.ts");
|
|
1011
|
+
if (await fs6.pathExists(barrelPath)) {
|
|
1012
|
+
logCheck("pass", "Utility barrel file present");
|
|
1013
|
+
} else {
|
|
1014
|
+
if (await fs6.pathExists(componentsDir)) {
|
|
1015
|
+
const dirs = await fs6.readdir(componentsDir);
|
|
1016
|
+
if (dirs.length > 0) {
|
|
1017
|
+
logCheck("warn", "Utility barrel file (rootnative-utils.ts) missing");
|
|
1018
|
+
}
|
|
927
1019
|
}
|
|
928
1020
|
}
|
|
929
1021
|
}
|
|
930
|
-
const nodeModules =
|
|
931
|
-
const inertiaPkgPath =
|
|
1022
|
+
const nodeModules = path6.resolve(cwd, "node_modules");
|
|
1023
|
+
const inertiaPkgPath = path6.join(
|
|
932
1024
|
nodeModules,
|
|
933
1025
|
"@rootnative",
|
|
934
1026
|
"inertia",
|
|
935
1027
|
"package.json"
|
|
936
1028
|
);
|
|
937
|
-
if (await
|
|
938
|
-
const inertiaPkg = await
|
|
1029
|
+
if (await fs6.pathExists(inertiaPkgPath)) {
|
|
1030
|
+
const inertiaPkg = await fs6.readJSON(inertiaPkgPath);
|
|
939
1031
|
logCheck("pass", `@rootnative/inertia@${inertiaPkg.version} installed`);
|
|
940
1032
|
} else {
|
|
941
1033
|
logCheck(
|
|
@@ -944,19 +1036,28 @@ async function doctorCommand(cwd) {
|
|
|
944
1036
|
);
|
|
945
1037
|
issues++;
|
|
946
1038
|
}
|
|
947
|
-
const
|
|
948
|
-
|
|
1039
|
+
for (const peer of REQUIRED_BARREL_PEERS) {
|
|
1040
|
+
if (await fs6.pathExists(path6.join(nodeModules, peer.name))) {
|
|
1041
|
+
logCheck("pass", `${peer.name} installed`);
|
|
1042
|
+
continue;
|
|
1043
|
+
}
|
|
1044
|
+
logCheck("fail", `${peer.name} is not installed \u2014 ${peer.reason}.`);
|
|
1045
|
+
console.log(` Run: ${chalk4.bold(`npx expo install ${peer.name}`)}`);
|
|
1046
|
+
issues++;
|
|
1047
|
+
}
|
|
1048
|
+
const safeAreaInstalled = await fs6.pathExists(
|
|
1049
|
+
path6.join(nodeModules, "react-native-safe-area-context")
|
|
949
1050
|
);
|
|
950
1051
|
if (safeAreaInstalled) {
|
|
951
1052
|
logCheck("pass", "react-native-safe-area-context installed");
|
|
952
1053
|
} else {
|
|
953
1054
|
logCheck(
|
|
954
1055
|
"warn",
|
|
955
|
-
"react-native-safe-area-context not installed (needed by: appbar, layout)"
|
|
1056
|
+
"react-native-safe-area-context not installed (needed by: appbar, layout, bottom-sheet, navigation-bar, snackbar)"
|
|
956
1057
|
);
|
|
957
1058
|
}
|
|
958
|
-
const vectorIconsInstalled = await
|
|
959
|
-
|
|
1059
|
+
const vectorIconsInstalled = await fs6.pathExists(
|
|
1060
|
+
path6.join(nodeModules, "@expo", "vector-icons")
|
|
960
1061
|
);
|
|
961
1062
|
if (vectorIconsInstalled) {
|
|
962
1063
|
logCheck("pass", "@expo/vector-icons installed");
|
|
@@ -969,6 +1070,7 @@ async function doctorCommand(cwd) {
|
|
|
969
1070
|
logger.break();
|
|
970
1071
|
if (issues > 0) {
|
|
971
1072
|
logger.error(`${issues} issue(s) found.`);
|
|
1073
|
+
process.exitCode = 1;
|
|
972
1074
|
} else {
|
|
973
1075
|
logger.success("All checks passed!");
|
|
974
1076
|
}
|
|
@@ -981,8 +1083,8 @@ import { execa as execa3 } from "execa";
|
|
|
981
1083
|
import prompts3 from "prompts";
|
|
982
1084
|
|
|
983
1085
|
// src/lib/llm-docs.ts
|
|
984
|
-
import
|
|
985
|
-
import
|
|
1086
|
+
import path7 from "path";
|
|
1087
|
+
import fs7 from "fs-extra";
|
|
986
1088
|
var CLAUDE_MD = "CLAUDE.md";
|
|
987
1089
|
var POINTER_MARKER = "@rootnative/components/llms.txt";
|
|
988
1090
|
var LLM_DOCS_POINTER = `## RootNative UI docs for AI agents
|
|
@@ -997,22 +1099,49 @@ This project uses RootNative UI (\`@rootnative/*\`). LLM-optimized docs:
|
|
|
997
1099
|
Prefer the \`node_modules\` copies \u2014 they match the installed version exactly.
|
|
998
1100
|
`;
|
|
999
1101
|
async function ensureLlmDocsPointer(cwd) {
|
|
1000
|
-
const filePath =
|
|
1001
|
-
if (!await
|
|
1002
|
-
await
|
|
1102
|
+
const filePath = path7.join(cwd, CLAUDE_MD);
|
|
1103
|
+
if (!await fs7.pathExists(filePath)) {
|
|
1104
|
+
await fs7.outputFile(filePath, `# CLAUDE.md
|
|
1003
1105
|
|
|
1004
1106
|
${LLM_DOCS_POINTER}`);
|
|
1005
1107
|
return "created";
|
|
1006
1108
|
}
|
|
1007
|
-
const existing = await
|
|
1109
|
+
const existing = await fs7.readFile(filePath, "utf8");
|
|
1008
1110
|
if (existing.includes(POINTER_MARKER)) {
|
|
1009
1111
|
return "already-present";
|
|
1010
1112
|
}
|
|
1011
1113
|
const separator = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
1012
|
-
await
|
|
1114
|
+
await fs7.outputFile(filePath, `${existing}${separator}${LLM_DOCS_POINTER}`);
|
|
1013
1115
|
return "appended";
|
|
1014
1116
|
}
|
|
1015
1117
|
|
|
1118
|
+
// src/lib/registry-version.ts
|
|
1119
|
+
var NPM_REGISTRY2 = "https://registry.npmjs.org";
|
|
1120
|
+
async function resolveRegistryVersion() {
|
|
1121
|
+
const fallback = "main";
|
|
1122
|
+
try {
|
|
1123
|
+
const res = await fetch(`${NPM_REGISTRY2}/@rootnative/core`);
|
|
1124
|
+
if (!res.ok) {
|
|
1125
|
+
return { version: fallback, fallback: { reason: "npm-unreachable" } };
|
|
1126
|
+
}
|
|
1127
|
+
const data = await res.json();
|
|
1128
|
+
const version = data["dist-tags"]?.latest;
|
|
1129
|
+
if (!version) {
|
|
1130
|
+
return { version: fallback, fallback: { reason: "no-latest-tag" } };
|
|
1131
|
+
}
|
|
1132
|
+
const ref = `v${version}`;
|
|
1133
|
+
const probe = await fetch(
|
|
1134
|
+
`${DEFAULT_CONFIG.registryUrl}/${ref}/registry/index.json`
|
|
1135
|
+
);
|
|
1136
|
+
if (!probe.ok) {
|
|
1137
|
+
return { version: fallback, fallback: { reason: "tag-missing", version } };
|
|
1138
|
+
}
|
|
1139
|
+
return { version: ref };
|
|
1140
|
+
} catch {
|
|
1141
|
+
return { version: fallback, fallback: { reason: "npm-unreachable" } };
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1016
1145
|
// src/commands/init.ts
|
|
1017
1146
|
async function initCommand(cwd, options = {}) {
|
|
1018
1147
|
logger.break();
|
|
@@ -1087,8 +1216,10 @@ async function initCommand(cwd, options = {}) {
|
|
|
1087
1216
|
componentsAlias = answers.componentsAlias;
|
|
1088
1217
|
libAlias = answers.libAlias;
|
|
1089
1218
|
}
|
|
1219
|
+
const registry = await resolveRegistryVersion();
|
|
1090
1220
|
const config = {
|
|
1091
1221
|
...DEFAULT_CONFIG,
|
|
1222
|
+
registryVersion: registry.version,
|
|
1092
1223
|
aliases: {
|
|
1093
1224
|
components: componentsAlias,
|
|
1094
1225
|
lib: libAlias
|
|
@@ -1096,6 +1227,34 @@ async function initCommand(cwd, options = {}) {
|
|
|
1096
1227
|
};
|
|
1097
1228
|
await writeConfig(cwd, config);
|
|
1098
1229
|
logger.success("Created rootnative.json");
|
|
1230
|
+
if (registry.fallback) {
|
|
1231
|
+
logger.break();
|
|
1232
|
+
if (registry.fallback.reason === "tag-missing") {
|
|
1233
|
+
logger.warn(
|
|
1234
|
+
`Could not pin the registry to v${registry.fallback.version}; using "main".`
|
|
1235
|
+
);
|
|
1236
|
+
logger.warn(
|
|
1237
|
+
"Copied component source may not match your installed packages."
|
|
1238
|
+
);
|
|
1239
|
+
} else {
|
|
1240
|
+
logger.warn('Could not reach npm to pin the registry; using "main".');
|
|
1241
|
+
logger.warn(
|
|
1242
|
+
"Copied component source will come from the development branch."
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
logger.break();
|
|
1246
|
+
}
|
|
1247
|
+
const tsconfigPatch = await ensureTsconfigPaths(cwd, componentsAlias);
|
|
1248
|
+
if (tsconfigPatch.status === "added") {
|
|
1249
|
+
logger.success(
|
|
1250
|
+
`Added "${tsconfigPatch.prefix}/*" path alias to tsconfig.json`
|
|
1251
|
+
);
|
|
1252
|
+
} else if (tsconfigPatch.status === "failed") {
|
|
1253
|
+
logger.warn(`Could not update tsconfig.json: ${tsconfigPatch.error}`);
|
|
1254
|
+
logger.info(
|
|
1255
|
+
`Add a "paths" mapping for "${componentsAlias.split("/")[0]}/*" by hand.`
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1099
1258
|
let addLlmDocs = options.yes;
|
|
1100
1259
|
if (!options.yes) {
|
|
1101
1260
|
const answer = await prompts3({
|
|
@@ -1152,9 +1311,9 @@ async function initCommand(cwd, options = {}) {
|
|
|
1152
1311
|
}
|
|
1153
1312
|
|
|
1154
1313
|
// src/commands/list.ts
|
|
1155
|
-
import
|
|
1314
|
+
import path8 from "path";
|
|
1156
1315
|
import chalk6 from "chalk";
|
|
1157
|
-
import
|
|
1316
|
+
import fs8 from "fs-extra";
|
|
1158
1317
|
async function listCommand(cwd) {
|
|
1159
1318
|
logger.break();
|
|
1160
1319
|
const config = await readConfig(cwd);
|
|
@@ -1180,8 +1339,8 @@ async function listCommand(cwd) {
|
|
|
1180
1339
|
` ${chalk6.dim("-".repeat(70))}`
|
|
1181
1340
|
);
|
|
1182
1341
|
for (const component of registryIndex.components) {
|
|
1183
|
-
const componentDir =
|
|
1184
|
-
const installed = await
|
|
1342
|
+
const componentDir = path8.join(componentsDir, component.name);
|
|
1343
|
+
const installed = await fs8.pathExists(componentDir);
|
|
1185
1344
|
const status = installed ? chalk6.green("installed") : chalk6.dim("-");
|
|
1186
1345
|
const nameDisplay = installed ? chalk6.green(component.name) : component.name;
|
|
1187
1346
|
console.log(
|
|
@@ -1201,9 +1360,9 @@ function padEnd(str, length) {
|
|
|
1201
1360
|
}
|
|
1202
1361
|
|
|
1203
1362
|
// src/commands/update.ts
|
|
1204
|
-
import
|
|
1363
|
+
import path9 from "path";
|
|
1205
1364
|
import chalk8 from "chalk";
|
|
1206
|
-
import
|
|
1365
|
+
import fs9 from "fs-extra";
|
|
1207
1366
|
import prompts4 from "prompts";
|
|
1208
1367
|
|
|
1209
1368
|
// src/lib/diff.ts
|
|
@@ -1353,8 +1512,8 @@ async function updateCommand(componentNames, cwd, options) {
|
|
|
1353
1512
|
targetNames = componentNames;
|
|
1354
1513
|
const missing = [];
|
|
1355
1514
|
for (const name of targetNames) {
|
|
1356
|
-
const dir =
|
|
1357
|
-
if (!await
|
|
1515
|
+
const dir = path9.join(componentsDir, name);
|
|
1516
|
+
if (!await fs9.pathExists(dir)) {
|
|
1358
1517
|
missing.push(name);
|
|
1359
1518
|
}
|
|
1360
1519
|
}
|
|
@@ -1377,8 +1536,8 @@ async function updateCommand(componentNames, cwd, options) {
|
|
|
1377
1536
|
resolveSpinner.succeed("Registry fetched");
|
|
1378
1537
|
const componentDiffs = [];
|
|
1379
1538
|
for (const { entry } of resolution.components) {
|
|
1380
|
-
const componentDir =
|
|
1381
|
-
if (!await
|
|
1539
|
+
const componentDir = path9.join(componentsDir, entry.name);
|
|
1540
|
+
if (!await fs9.pathExists(componentDir)) {
|
|
1382
1541
|
componentDiffs.push({
|
|
1383
1542
|
name: entry.name,
|
|
1384
1543
|
diffs: [],
|
|
@@ -1388,8 +1547,8 @@ async function updateCommand(componentNames, cwd, options) {
|
|
|
1388
1547
|
}
|
|
1389
1548
|
const diffs = [];
|
|
1390
1549
|
for (const filePath of entry.files) {
|
|
1391
|
-
const fileName =
|
|
1392
|
-
const localPath =
|
|
1550
|
+
const fileName = path9.basename(filePath);
|
|
1551
|
+
const localPath = path9.join(componentDir, fileName);
|
|
1393
1552
|
const remoteContent = await fetchFileContent(config, filePath);
|
|
1394
1553
|
const transformed = transformImports(remoteContent, {
|
|
1395
1554
|
config,
|
|
@@ -1397,8 +1556,8 @@ async function updateCommand(componentNames, cwd, options) {
|
|
|
1397
1556
|
installedComponents: allComponentNames
|
|
1398
1557
|
});
|
|
1399
1558
|
let localContent = "";
|
|
1400
|
-
if (await
|
|
1401
|
-
localContent = await
|
|
1559
|
+
if (await fs9.pathExists(localPath)) {
|
|
1560
|
+
localContent = await fs9.readFile(localPath, "utf-8");
|
|
1402
1561
|
}
|
|
1403
1562
|
diffs.push(computeDiff(localContent, transformed, fileName));
|
|
1404
1563
|
}
|
|
@@ -1409,12 +1568,12 @@ async function updateCommand(componentNames, cwd, options) {
|
|
|
1409
1568
|
for (const utilName of resolution.utils) {
|
|
1410
1569
|
const utilEntry = utilsRegistry[utilName];
|
|
1411
1570
|
if (!utilEntry) continue;
|
|
1412
|
-
const fileName =
|
|
1413
|
-
const localPath =
|
|
1571
|
+
const fileName = path9.basename(utilEntry.file);
|
|
1572
|
+
const localPath = path9.join(libDir, fileName);
|
|
1414
1573
|
const remoteContent = await fetchFileContent(config, utilEntry.file);
|
|
1415
1574
|
let localContent = "";
|
|
1416
|
-
if (await
|
|
1417
|
-
localContent = await
|
|
1575
|
+
if (await fs9.pathExists(localPath)) {
|
|
1576
|
+
localContent = await fs9.readFile(localPath, "utf-8");
|
|
1418
1577
|
}
|
|
1419
1578
|
utilDiffs.push(computeDiff(localContent, remoteContent, `lib/${fileName}`));
|
|
1420
1579
|
}
|
|
@@ -1426,10 +1585,10 @@ async function updateCommand(componentNames, cwd, options) {
|
|
|
1426
1585
|
}
|
|
1427
1586
|
}
|
|
1428
1587
|
const newBarrel = generateUtilsBarrel(resolution.utils, utilExports);
|
|
1429
|
-
const barrelPath =
|
|
1588
|
+
const barrelPath = path9.join(libDir, "rootnative-utils.ts");
|
|
1430
1589
|
let oldBarrel = "";
|
|
1431
|
-
if (await
|
|
1432
|
-
oldBarrel = await
|
|
1590
|
+
if (await fs9.pathExists(barrelPath)) {
|
|
1591
|
+
oldBarrel = await fs9.readFile(barrelPath, "utf-8");
|
|
1433
1592
|
}
|
|
1434
1593
|
utilDiffs.push(computeDiff(oldBarrel, newBarrel, "lib/rootnative-utils.ts"));
|
|
1435
1594
|
const changedComponents = componentDiffs.filter((c) => c.hasChanges);
|
|
@@ -1492,8 +1651,8 @@ async function updateCommand(componentNames, cwd, options) {
|
|
|
1492
1651
|
const applySpinner = createSpinner("Applying updates...");
|
|
1493
1652
|
applySpinner.start();
|
|
1494
1653
|
for (const comp of changedComponents) {
|
|
1495
|
-
const componentDir =
|
|
1496
|
-
await
|
|
1654
|
+
const componentDir = path9.join(componentsDir, comp.name);
|
|
1655
|
+
await fs9.ensureDir(componentDir);
|
|
1497
1656
|
if (comp.diffs.length === 0) {
|
|
1498
1657
|
const entry = resolution.components.find(
|
|
1499
1658
|
(c) => c.entry.name === comp.name
|
|
@@ -1501,14 +1660,14 @@ async function updateCommand(componentNames, cwd, options) {
|
|
|
1501
1660
|
if (!entry) continue;
|
|
1502
1661
|
for (const filePath of entry.entry.files) {
|
|
1503
1662
|
const content = await fetchFileContent(config, filePath);
|
|
1504
|
-
const fileName =
|
|
1663
|
+
const fileName = path9.basename(filePath);
|
|
1505
1664
|
const transformed = transformImports(content, {
|
|
1506
1665
|
config,
|
|
1507
1666
|
componentName: comp.name,
|
|
1508
1667
|
installedComponents: allComponentNames
|
|
1509
1668
|
});
|
|
1510
|
-
await
|
|
1511
|
-
|
|
1669
|
+
await fs9.writeFile(
|
|
1670
|
+
path9.join(componentDir, fileName),
|
|
1512
1671
|
transformed,
|
|
1513
1672
|
"utf-8"
|
|
1514
1673
|
);
|
|
@@ -1518,23 +1677,23 @@ async function updateCommand(componentNames, cwd, options) {
|
|
|
1518
1677
|
for (const diff of comp.diffs) {
|
|
1519
1678
|
if (!diff.hasChanges) continue;
|
|
1520
1679
|
const newContent = diff.lines.filter((l) => l.type !== "remove").map((l) => l.content).join("\n");
|
|
1521
|
-
await
|
|
1522
|
-
|
|
1680
|
+
await fs9.writeFile(
|
|
1681
|
+
path9.join(componentDir, diff.fileName),
|
|
1523
1682
|
newContent,
|
|
1524
1683
|
"utf-8"
|
|
1525
1684
|
);
|
|
1526
1685
|
}
|
|
1527
1686
|
}
|
|
1528
|
-
await
|
|
1687
|
+
await fs9.ensureDir(libDir);
|
|
1529
1688
|
for (const diff of changedUtils) {
|
|
1530
1689
|
if (!diff.hasChanges) continue;
|
|
1531
1690
|
const newContent = diff.lines.filter((l) => l.type !== "remove").map((l) => l.content).join("\n");
|
|
1532
|
-
const filePath =
|
|
1691
|
+
const filePath = path9.join(
|
|
1533
1692
|
libDir,
|
|
1534
1693
|
// diff.fileName is like "lib/color.ts" — strip the "lib/" prefix
|
|
1535
1694
|
diff.fileName.replace(/^lib\//, "")
|
|
1536
1695
|
);
|
|
1537
|
-
await
|
|
1696
|
+
await fs9.writeFile(filePath, newContent, "utf-8");
|
|
1538
1697
|
}
|
|
1539
1698
|
applySpinner.succeed("Updates applied");
|
|
1540
1699
|
logger.break();
|
|
@@ -1544,17 +1703,17 @@ async function updateCommand(componentNames, cwd, options) {
|
|
|
1544
1703
|
logger.break();
|
|
1545
1704
|
}
|
|
1546
1705
|
async function getInstalledComponents(componentsDir) {
|
|
1547
|
-
if (!await
|
|
1706
|
+
if (!await fs9.pathExists(componentsDir)) {
|
|
1548
1707
|
return [];
|
|
1549
1708
|
}
|
|
1550
|
-
const entries = await
|
|
1709
|
+
const entries = await fs9.readdir(componentsDir);
|
|
1551
1710
|
const components = [];
|
|
1552
1711
|
for (const entry of entries) {
|
|
1553
|
-
const fullPath =
|
|
1554
|
-
const stat = await
|
|
1712
|
+
const fullPath = path9.join(componentsDir, entry);
|
|
1713
|
+
const stat = await fs9.stat(fullPath);
|
|
1555
1714
|
if (stat.isDirectory()) {
|
|
1556
|
-
const indexPath =
|
|
1557
|
-
if (await
|
|
1715
|
+
const indexPath = path9.join(fullPath, "index.ts");
|
|
1716
|
+
if (await fs9.pathExists(indexPath)) {
|
|
1558
1717
|
components.push(entry);
|
|
1559
1718
|
}
|
|
1560
1719
|
}
|
|
@@ -1563,10 +1722,10 @@ async function getInstalledComponents(componentsDir) {
|
|
|
1563
1722
|
}
|
|
1564
1723
|
|
|
1565
1724
|
// src/commands/upgrade.ts
|
|
1566
|
-
import
|
|
1725
|
+
import path10 from "path";
|
|
1567
1726
|
import chalk9 from "chalk";
|
|
1568
1727
|
import { execa as execa4 } from "execa";
|
|
1569
|
-
import
|
|
1728
|
+
import fs10 from "fs-extra";
|
|
1570
1729
|
import prompts5 from "prompts";
|
|
1571
1730
|
async function fetchLatestFromNpm(packageName) {
|
|
1572
1731
|
const url = `https://registry.npmjs.org/${packageName}/latest`;
|
|
@@ -1579,16 +1738,16 @@ async function fetchLatestFromNpm(packageName) {
|
|
|
1579
1738
|
return response.json();
|
|
1580
1739
|
}
|
|
1581
1740
|
function getInstalledPackageInfo(cwd, packageName) {
|
|
1582
|
-
const pkgPath =
|
|
1741
|
+
const pkgPath = path10.resolve(
|
|
1583
1742
|
cwd,
|
|
1584
1743
|
"node_modules",
|
|
1585
1744
|
...packageName.split("/"),
|
|
1586
1745
|
"package.json"
|
|
1587
1746
|
);
|
|
1588
|
-
if (!
|
|
1747
|
+
if (!fs10.pathExistsSync(pkgPath)) {
|
|
1589
1748
|
return null;
|
|
1590
1749
|
}
|
|
1591
|
-
const pkg =
|
|
1750
|
+
const pkg = fs10.readJSONSync(pkgPath);
|
|
1592
1751
|
return {
|
|
1593
1752
|
version: pkg.version,
|
|
1594
1753
|
peerDependencies: pkg.peerDependencies,
|
|
@@ -1619,9 +1778,9 @@ function diffPeerDeps(current, latest) {
|
|
|
1619
1778
|
return { added, changed, removed };
|
|
1620
1779
|
}
|
|
1621
1780
|
function getProjectDeps(cwd) {
|
|
1622
|
-
const pkgPath =
|
|
1781
|
+
const pkgPath = path10.resolve(cwd, "package.json");
|
|
1623
1782
|
try {
|
|
1624
|
-
const pkg =
|
|
1783
|
+
const pkg = fs10.readJSONSync(pkgPath);
|
|
1625
1784
|
return {
|
|
1626
1785
|
...pkg.dependencies,
|
|
1627
1786
|
...pkg.devDependencies
|
|
@@ -1633,6 +1792,19 @@ function getProjectDeps(cwd) {
|
|
|
1633
1792
|
function hasDiffChanges(diff) {
|
|
1634
1793
|
return Object.keys(diff.added).length > 0 || Object.keys(diff.changed).length > 0 || diff.removed.length > 0;
|
|
1635
1794
|
}
|
|
1795
|
+
async function repinRegistryVersion(cwd) {
|
|
1796
|
+
const config = await readConfig(cwd);
|
|
1797
|
+
const next = await resolveRegistryVersion();
|
|
1798
|
+
if (next.fallback && config.registryVersion !== "main") {
|
|
1799
|
+
logger.warn(
|
|
1800
|
+
`Kept the registry pinned to ${chalk9.bold(config.registryVersion)} \u2014 could not resolve a newer release tag.`
|
|
1801
|
+
);
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
if (config.registryVersion === next.version) return;
|
|
1805
|
+
await writeConfig(cwd, { ...config, registryVersion: next.version });
|
|
1806
|
+
logger.success(`Registry pinned to ${chalk9.bold(next.version)}`);
|
|
1807
|
+
}
|
|
1636
1808
|
async function upgradeCommand(cwd, options = {}) {
|
|
1637
1809
|
logger.break();
|
|
1638
1810
|
await readConfig(cwd);
|
|
@@ -1661,6 +1833,7 @@ async function upgradeCommand(cwd, options = {}) {
|
|
|
1661
1833
|
`Already on the latest version ${chalk9.bold(`v${installed.version}`)}`
|
|
1662
1834
|
);
|
|
1663
1835
|
logger.break();
|
|
1836
|
+
await repinRegistryVersion(cwd);
|
|
1664
1837
|
if (options.all) {
|
|
1665
1838
|
logger.info("Updating installed components...");
|
|
1666
1839
|
await updateCommand([], cwd, { all: true, dryRun: false });
|
|
@@ -1778,6 +1951,7 @@ async function upgradeCommand(cwd, options = {}) {
|
|
|
1778
1951
|
logger.info(` ${pkg}`);
|
|
1779
1952
|
}
|
|
1780
1953
|
}
|
|
1954
|
+
await repinRegistryVersion(cwd);
|
|
1781
1955
|
logger.break();
|
|
1782
1956
|
if (options.all) {
|
|
1783
1957
|
logger.info("Updating installed components...");
|
|
@@ -1829,7 +2003,7 @@ program.command("init").description("Initialize your project for RootNative UI")
|
|
|
1829
2003
|
handleError(error);
|
|
1830
2004
|
}
|
|
1831
2005
|
});
|
|
1832
|
-
program.command("add").description("Add components to your project").argument("<components...>", "Component names to add").option("-f, --force", "Overwrite existing components", false).option(
|
|
2006
|
+
program.command("add").description("Add components to your project").argument("<components...>", "Component names to add").option("-y, --yes", "Skip prompts and use defaults", false).option("-f, --force", "Overwrite existing components", false).option(
|
|
1833
2007
|
"-d, --dry-run",
|
|
1834
2008
|
"Show what would be installed without making changes",
|
|
1835
2009
|
false
|
|
@@ -1844,6 +2018,7 @@ program.command("add").description("Add components to your project").argument("<
|
|
|
1844
2018
|
await addCommand(components, process.cwd(), {
|
|
1845
2019
|
force: options.force,
|
|
1846
2020
|
dryRun: options.dryRun,
|
|
2021
|
+
yes: options.yes,
|
|
1847
2022
|
packageManager: options.packageManager
|
|
1848
2023
|
});
|
|
1849
2024
|
} catch (error) {
|
package/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @rootnative/cli — CLI for RootNative UI
|
|
2
2
|
|
|
3
|
-
> Version: 0.0.0-alpha.
|
|
3
|
+
> Version: 0.0.0-alpha.5
|
|
4
4
|
> Binary: `rootnative`
|
|
5
5
|
> Requirements: Node >=18
|
|
6
6
|
|
|
@@ -47,10 +47,12 @@ Creates `rootnative.json`:
|
|
|
47
47
|
"$schema": "https://rootnative.github.io/ui/schema.json",
|
|
48
48
|
"aliases": { "components": "@/components/ui", "lib": "@/lib" },
|
|
49
49
|
"registryUrl": "https://raw.githubusercontent.com/rootnative/ui",
|
|
50
|
-
"registryVersion": "
|
|
50
|
+
"registryVersion": "v0.0.0-alpha.5"
|
|
51
51
|
}
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
+
`init` pins `registryVersion` to the `v<version>` git tag of the latest published release, so a project keeps fetching the same component sources until you run `rootnative upgrade` (which moves the pin forward). It falls back to `main` only when npm is unreachable or the tag has not been pushed yet.
|
|
55
|
+
|
|
54
56
|
#### `rootnative add <components...>`
|
|
55
57
|
|
|
56
58
|
Add components to your project. Resolves dependency graph, copies files with rewritten imports, installs npm deps.
|
|
@@ -58,7 +60,7 @@ Add components to your project. Resolves dependency graph, copies files with rew
|
|
|
58
60
|
```bash
|
|
59
61
|
npx rootnative add button
|
|
60
62
|
npx rootnative add card chip text-field
|
|
61
|
-
npx rootnative add appbar # auto-adds icon-button + typography
|
|
63
|
+
npx rootnative add appbar # auto-adds button + icon-button + typography
|
|
62
64
|
```
|
|
63
65
|
|
|
64
66
|
Options:
|
|
@@ -85,6 +87,7 @@ npx rootnative upgrade -y # Non-interactive — skip confirmation
|
|
|
85
87
|
|
|
86
88
|
Options:
|
|
87
89
|
- `-y, --yes` — Skip confirmation prompt
|
|
90
|
+
- `-a, --all` — Also update the installed component files, not just the package
|
|
88
91
|
- `--package-manager <pm>` — Package manager to use (`npm`, `yarn`, `pnpm`, `bun`)
|
|
89
92
|
|
|
90
93
|
What it does:
|
|
@@ -95,6 +98,8 @@ What it does:
|
|
|
95
98
|
5. Upgrades `@rootnative/core` and installs any new required peer dependencies in one step
|
|
96
99
|
6. Reports optional peer deps that aren't installed (does not auto-install optional deps)
|
|
97
100
|
7. Lists peer deps that are no longer required so you can remove them manually
|
|
101
|
+
8. Moves `registryVersion` forward to the new release tag, so later `add`/`update` calls fetch matching sources
|
|
102
|
+
9. With `-a, --all`, also updates the installed component files
|
|
98
103
|
|
|
99
104
|
#### `rootnative list`
|
|
100
105
|
|