@rootnative/cli 0.0.0-alpha.4 → 0.0.0-alpha.7
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 +378 -167
- 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 path6 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";
|
|
@@ -212,6 +289,7 @@ async function fetchFileContent(config, filePath) {
|
|
|
212
289
|
}
|
|
213
290
|
|
|
214
291
|
// src/lib/resolver.ts
|
|
292
|
+
import path4 from "path";
|
|
215
293
|
async function resolveComponents(config, requestedNames) {
|
|
216
294
|
const resolved = /* @__PURE__ */ new Map();
|
|
217
295
|
const utilsRegistry = await fetchUtilsRegistry(config);
|
|
@@ -244,9 +322,11 @@ function buildResult(resolved, utilsRegistry) {
|
|
|
244
322
|
optionalDeps[pkg] = version;
|
|
245
323
|
}
|
|
246
324
|
}
|
|
325
|
+
const utilFileNames = {};
|
|
247
326
|
for (const utilName of utilSet) {
|
|
248
327
|
const utilEntry = utilsRegistry[utilName];
|
|
249
328
|
if (utilEntry) {
|
|
329
|
+
utilFileNames[utilName] = path4.basename(utilEntry.file);
|
|
250
330
|
for (const [pkg, version] of Object.entries(utilEntry.dependencies)) {
|
|
251
331
|
optionalDeps[pkg] = version;
|
|
252
332
|
}
|
|
@@ -255,6 +335,7 @@ function buildResult(resolved, utilsRegistry) {
|
|
|
255
335
|
return {
|
|
256
336
|
components,
|
|
257
337
|
utils: Array.from(utilSet).sort(),
|
|
338
|
+
utilFileNames,
|
|
258
339
|
npmDependencies: npmDeps,
|
|
259
340
|
optionalNpmDependencies: optionalDeps
|
|
260
341
|
};
|
|
@@ -264,16 +345,44 @@ function getComponentNames(result) {
|
|
|
264
345
|
}
|
|
265
346
|
|
|
266
347
|
// src/lib/transform.ts
|
|
348
|
+
import path5 from "path";
|
|
267
349
|
var SINGLE_LINE_IMPORT_REGEX = /((?:import|export)\s+(?:type\s+)?(?:\{[^}]*\}|\*\s+as\s+\w+|[\w,\s]+)\s+from\s+)(['"])([^'"]+)\2/g;
|
|
268
350
|
var MULTI_LINE_IMPORT_REGEX = /((?:import|export)\s+(?:type\s+)?\{[\s\S]*?\}\s+from\s+)(['"])([^'"]+)\2/g;
|
|
269
351
|
var SHARED_ROOT_MODULES = /* @__PURE__ */ new Set(["safe-area"]);
|
|
352
|
+
function isPrefixAlias(alias) {
|
|
353
|
+
return !alias.startsWith(".");
|
|
354
|
+
}
|
|
355
|
+
function buildSpecifier(alias, rest, componentsAlias, componentName) {
|
|
356
|
+
if (isPrefixAlias(alias)) {
|
|
357
|
+
return `${alias}/${rest}`;
|
|
358
|
+
}
|
|
359
|
+
const fromDir = path5.posix.join(
|
|
360
|
+
toPosix(stripRelativePrefix(componentsAlias)),
|
|
361
|
+
componentName
|
|
362
|
+
);
|
|
363
|
+
const target = path5.posix.join(toPosix(stripRelativePrefix(alias)), rest);
|
|
364
|
+
const relative = path5.posix.relative(fromDir, target);
|
|
365
|
+
return relative.startsWith(".") ? relative : `./${relative}`;
|
|
366
|
+
}
|
|
367
|
+
function stripRelativePrefix(alias) {
|
|
368
|
+
return alias.replace(/^\.\//, "");
|
|
369
|
+
}
|
|
370
|
+
function toPosix(value) {
|
|
371
|
+
return value.replace(/\\/g, "/");
|
|
372
|
+
}
|
|
270
373
|
function transformImports(source, options) {
|
|
271
|
-
const { config, installedComponents } = options;
|
|
374
|
+
const { config, componentName, installedComponents } = options;
|
|
272
375
|
const componentsAlias = config.aliases.components;
|
|
273
376
|
const libAlias = config.aliases.lib;
|
|
274
377
|
function rewriteImport(match, prefix, quote, importPath) {
|
|
275
378
|
if (importPath === "@rootnative/utils") {
|
|
276
|
-
|
|
379
|
+
const specifier = buildSpecifier(
|
|
380
|
+
libAlias,
|
|
381
|
+
"rootnative-utils",
|
|
382
|
+
componentsAlias,
|
|
383
|
+
componentName
|
|
384
|
+
);
|
|
385
|
+
return `${prefix}${quote}${specifier}${quote}`;
|
|
277
386
|
}
|
|
278
387
|
if (importPath.startsWith("@rootnative/core")) {
|
|
279
388
|
return match;
|
|
@@ -290,7 +399,13 @@ function transformImports(source, options) {
|
|
|
290
399
|
const targetComponent = extractComponentName(importPath);
|
|
291
400
|
if (targetComponent && installedComponents.includes(targetComponent)) {
|
|
292
401
|
const restOfPath = importPath.replace(/^\.\.\//, "");
|
|
293
|
-
|
|
402
|
+
const specifier = buildSpecifier(
|
|
403
|
+
componentsAlias,
|
|
404
|
+
restOfPath,
|
|
405
|
+
componentsAlias,
|
|
406
|
+
componentName
|
|
407
|
+
);
|
|
408
|
+
return `${prefix}${quote}${specifier}${quote}`;
|
|
294
409
|
}
|
|
295
410
|
}
|
|
296
411
|
return match;
|
|
@@ -333,8 +448,8 @@ async function installComponents(options) {
|
|
|
333
448
|
await generateBarrel(resolution, utilsRegistry, libDir);
|
|
334
449
|
spinner.succeed("Utility files copied");
|
|
335
450
|
for (const { entry } of resolution.components) {
|
|
336
|
-
const componentDir =
|
|
337
|
-
const exists = await
|
|
451
|
+
const componentDir = path6.join(componentsDir, entry.name);
|
|
452
|
+
const exists = await fs4.pathExists(componentDir);
|
|
338
453
|
if (exists && !force) {
|
|
339
454
|
logger.warn(
|
|
340
455
|
`${entry.name} already exists, skipping (use --force to overwrite)`
|
|
@@ -343,17 +458,17 @@ async function installComponents(options) {
|
|
|
343
458
|
}
|
|
344
459
|
const compSpinner = createSpinner(`Adding ${entry.name}...`);
|
|
345
460
|
compSpinner.start();
|
|
346
|
-
await
|
|
461
|
+
await fs4.ensureDir(componentDir);
|
|
347
462
|
for (const filePath of entry.files) {
|
|
348
463
|
const content = await fetchFileContent(config, filePath);
|
|
349
|
-
const fileName =
|
|
464
|
+
const fileName = path6.basename(filePath);
|
|
350
465
|
const transformed = transformImports(content, {
|
|
351
466
|
config,
|
|
352
467
|
componentName: entry.name,
|
|
353
468
|
installedComponents: allComponentNames
|
|
354
469
|
});
|
|
355
|
-
await
|
|
356
|
-
|
|
470
|
+
await fs4.writeFile(
|
|
471
|
+
path6.join(componentDir, fileName),
|
|
357
472
|
transformed,
|
|
358
473
|
"utf-8"
|
|
359
474
|
);
|
|
@@ -379,13 +494,13 @@ async function installComponents(options) {
|
|
|
379
494
|
}
|
|
380
495
|
}
|
|
381
496
|
async function copyUtilFiles(config, resolution, utilsRegistry, libDir) {
|
|
382
|
-
await
|
|
497
|
+
await fs4.ensureDir(libDir);
|
|
383
498
|
for (const utilName of resolution.utils) {
|
|
384
499
|
const utilEntry = utilsRegistry[utilName];
|
|
385
500
|
if (!utilEntry) continue;
|
|
386
501
|
const content = await fetchFileContent(config, utilEntry.file);
|
|
387
|
-
const fileName =
|
|
388
|
-
await
|
|
502
|
+
const fileName = path6.basename(utilEntry.file);
|
|
503
|
+
await fs4.writeFile(path6.join(libDir, fileName), content, "utf-8");
|
|
389
504
|
}
|
|
390
505
|
}
|
|
391
506
|
async function generateBarrel(resolution, utilsRegistry, libDir) {
|
|
@@ -405,18 +520,18 @@ async function generateBarrel(resolution, utilsRegistry, libDir) {
|
|
|
405
520
|
utilExports,
|
|
406
521
|
utilTypeExports
|
|
407
522
|
);
|
|
408
|
-
await
|
|
409
|
-
|
|
523
|
+
await fs4.writeFile(
|
|
524
|
+
path6.join(libDir, "rootnative-utils.ts"),
|
|
410
525
|
barrelContent,
|
|
411
526
|
"utf-8"
|
|
412
527
|
);
|
|
413
528
|
}
|
|
414
529
|
function collectDepsToInstall(resolution, cwd) {
|
|
415
530
|
const deps = [];
|
|
416
|
-
const pkgPath =
|
|
531
|
+
const pkgPath = path6.resolve(cwd, "package.json");
|
|
417
532
|
let existingDeps = {};
|
|
418
533
|
try {
|
|
419
|
-
const pkg =
|
|
534
|
+
const pkg = fs4.readJSONSync(pkgPath);
|
|
420
535
|
existingDeps = {
|
|
421
536
|
...pkg.dependencies,
|
|
422
537
|
...pkg.devDependencies
|
|
@@ -464,7 +579,10 @@ async function addCommand(componentNames, cwd, options) {
|
|
|
464
579
|
if (resolution.utils.length > 0) {
|
|
465
580
|
logger.break();
|
|
466
581
|
console.log(chalk2.bold("Utilities to copy:"));
|
|
467
|
-
|
|
582
|
+
const fileNames = resolution.utils.map(
|
|
583
|
+
(u) => resolution.utilFileNames[u] ?? u
|
|
584
|
+
);
|
|
585
|
+
console.log(` ${fileNames.join(", ")}`);
|
|
468
586
|
}
|
|
469
587
|
const npmDeps = Object.keys(resolution.npmDependencies);
|
|
470
588
|
const optionalDeps = Object.keys(resolution.optionalNpmDependencies);
|
|
@@ -483,15 +601,17 @@ async function addCommand(componentNames, cwd, options) {
|
|
|
483
601
|
logger.info("Dry run complete. No files were written.");
|
|
484
602
|
return;
|
|
485
603
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
604
|
+
if (!options.yes) {
|
|
605
|
+
const { proceed } = await prompts({
|
|
606
|
+
type: "confirm",
|
|
607
|
+
name: "proceed",
|
|
608
|
+
message: "Proceed with installation?",
|
|
609
|
+
initial: true
|
|
610
|
+
});
|
|
611
|
+
if (!proceed) {
|
|
612
|
+
logger.info("Cancelled.");
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
495
615
|
}
|
|
496
616
|
const project = await detectProject(cwd);
|
|
497
617
|
const pm = options.packageManager ?? project.packageManager;
|
|
@@ -510,10 +630,10 @@ async function addCommand(componentNames, cwd, options) {
|
|
|
510
630
|
}
|
|
511
631
|
|
|
512
632
|
// src/commands/create.ts
|
|
513
|
-
import
|
|
633
|
+
import path7 from "path";
|
|
514
634
|
import chalk3 from "chalk";
|
|
515
635
|
import { execa as execa2 } from "execa";
|
|
516
|
-
import
|
|
636
|
+
import fs5 from "fs-extra";
|
|
517
637
|
import prompts2 from "prompts";
|
|
518
638
|
var NPM_REGISTRY = "https://registry.npmjs.org";
|
|
519
639
|
var ROOTNATIVE_PACKAGES = ["@rootnative/core", "@rootnative/components"];
|
|
@@ -570,7 +690,7 @@ var TEMPLATE_BINARY_FILES = [
|
|
|
570
690
|
"assets/adaptive-icon.png",
|
|
571
691
|
"assets/favicon.png"
|
|
572
692
|
];
|
|
573
|
-
var TEMPLATE_OPTIONAL_TEXT_FILES = ["CLAUDE.md"];
|
|
693
|
+
var TEMPLATE_OPTIONAL_TEXT_FILES = ["CLAUDE.md", "README.md"];
|
|
574
694
|
function isValidTemplate(value) {
|
|
575
695
|
return value in TEMPLATE_CONFIGS;
|
|
576
696
|
}
|
|
@@ -700,8 +820,8 @@ async function createCommand(name, options = {}) {
|
|
|
700
820
|
}
|
|
701
821
|
packageManager = value;
|
|
702
822
|
}
|
|
703
|
-
const targetDir =
|
|
704
|
-
if (await
|
|
823
|
+
const targetDir = path7.resolve(process.cwd(), projectName);
|
|
824
|
+
if (await fs5.pathExists(targetDir)) {
|
|
705
825
|
if (options.yes) {
|
|
706
826
|
logger.warn(`Directory ${chalk3.bold(projectName)} already exists.`);
|
|
707
827
|
process.exit(1);
|
|
@@ -716,7 +836,7 @@ async function createCommand(name, options = {}) {
|
|
|
716
836
|
logger.info("Create cancelled.");
|
|
717
837
|
return;
|
|
718
838
|
}
|
|
719
|
-
await
|
|
839
|
+
await fs5.remove(targetDir);
|
|
720
840
|
}
|
|
721
841
|
const templateConfig = TEMPLATE_CONFIGS[templateName];
|
|
722
842
|
const { baseUrl, pinnedVersion } = await resolveTemplateSource();
|
|
@@ -725,7 +845,7 @@ async function createCommand(name, options = {}) {
|
|
|
725
845
|
spinner.start();
|
|
726
846
|
try {
|
|
727
847
|
for (const dir of templateConfig.dirs) {
|
|
728
|
-
await
|
|
848
|
+
await fs5.ensureDir(path7.join(targetDir, dir));
|
|
729
849
|
}
|
|
730
850
|
for (const file of templateConfig.textFiles) {
|
|
731
851
|
let content = await fetchText(`${templateBaseUrl}/${file}`);
|
|
@@ -750,18 +870,18 @@ async function createCommand(name, options = {}) {
|
|
|
750
870
|
}
|
|
751
871
|
content = JSON.stringify(appJson, null, 2) + "\n";
|
|
752
872
|
}
|
|
753
|
-
await
|
|
873
|
+
await fs5.outputFile(path7.join(targetDir, file), content);
|
|
754
874
|
}
|
|
755
875
|
for (const file of TEMPLATE_OPTIONAL_TEXT_FILES) {
|
|
756
876
|
const content = await fetchTextOptional(`${templateBaseUrl}/${file}`);
|
|
757
877
|
if (content !== null) {
|
|
758
|
-
await
|
|
878
|
+
await fs5.outputFile(path7.join(targetDir, file), content);
|
|
759
879
|
}
|
|
760
880
|
}
|
|
761
881
|
for (const file of TEMPLATE_BINARY_FILES) {
|
|
762
882
|
const buffer = await fetchBinary(`${templateBaseUrl}/${file}`);
|
|
763
883
|
if (buffer) {
|
|
764
|
-
await
|
|
884
|
+
await fs5.outputFile(path7.join(targetDir, file), buffer);
|
|
765
885
|
}
|
|
766
886
|
}
|
|
767
887
|
spinner.succeed("Project created");
|
|
@@ -810,28 +930,34 @@ async function createCommand(name, options = {}) {
|
|
|
810
930
|
}
|
|
811
931
|
|
|
812
932
|
// src/commands/doctor.ts
|
|
813
|
-
import
|
|
933
|
+
import path8 from "path";
|
|
814
934
|
import chalk4 from "chalk";
|
|
815
|
-
import
|
|
935
|
+
import fs6 from "fs-extra";
|
|
816
936
|
function logCheck(status, message) {
|
|
817
|
-
const icon = status === "pass" ? chalk4.green("[pass]") : status === "warn" ? chalk4.yellow("[warn]") : chalk4.red("[fail]");
|
|
937
|
+
const icon = status === "pass" ? chalk4.green("[pass]") : status === "warn" ? chalk4.yellow("[warn]") : status === "info" ? chalk4.cyan("[info]") : chalk4.red("[fail]");
|
|
818
938
|
console.log(` ${icon} ${message}`);
|
|
819
939
|
}
|
|
940
|
+
var REQUIRED_BARREL_PEERS = [
|
|
941
|
+
{
|
|
942
|
+
name: "react-native-svg",
|
|
943
|
+
reason: "the @rootnative/components barrel requires it at load time (circular-progress, loading-indicator)"
|
|
944
|
+
}
|
|
945
|
+
];
|
|
820
946
|
async function doctorCommand(cwd) {
|
|
821
947
|
logger.break();
|
|
822
948
|
console.log(chalk4.bold("RootNative Doctor"));
|
|
823
949
|
logger.break();
|
|
824
950
|
let issues = 0;
|
|
825
|
-
|
|
951
|
+
const initialized = await configExists(cwd);
|
|
952
|
+
if (initialized) {
|
|
826
953
|
logCheck("pass", "rootnative.json found");
|
|
827
954
|
} else {
|
|
828
|
-
logCheck(
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
return;
|
|
955
|
+
logCheck(
|
|
956
|
+
"info",
|
|
957
|
+
'Not initialized for the CLI workflow (no rootnative.json). Run "rootnative init" to copy component source.'
|
|
958
|
+
);
|
|
833
959
|
}
|
|
834
|
-
const config = await readConfig(cwd);
|
|
960
|
+
const config = initialized ? await readConfig(cwd) : null;
|
|
835
961
|
const project = await detectProject(cwd);
|
|
836
962
|
if (project.type !== "unknown") {
|
|
837
963
|
logCheck(
|
|
@@ -842,9 +968,9 @@ async function doctorCommand(cwd) {
|
|
|
842
968
|
logCheck("fail", "Not a React Native or Expo project");
|
|
843
969
|
issues++;
|
|
844
970
|
}
|
|
845
|
-
const pkgPath =
|
|
846
|
-
if (await
|
|
847
|
-
const pkg = await
|
|
971
|
+
const pkgPath = path8.resolve(cwd, "package.json");
|
|
972
|
+
if (await fs6.pathExists(pkgPath)) {
|
|
973
|
+
const pkg = await fs6.readJSON(pkgPath);
|
|
848
974
|
const allDeps = {
|
|
849
975
|
...pkg.dependencies,
|
|
850
976
|
...pkg.devDependencies
|
|
@@ -857,15 +983,15 @@ async function doctorCommand(cwd) {
|
|
|
857
983
|
issues++;
|
|
858
984
|
}
|
|
859
985
|
}
|
|
860
|
-
const corePkgPath =
|
|
986
|
+
const corePkgPath = path8.resolve(
|
|
861
987
|
cwd,
|
|
862
988
|
"node_modules",
|
|
863
989
|
"@rootnative",
|
|
864
990
|
"core",
|
|
865
991
|
"package.json"
|
|
866
992
|
);
|
|
867
|
-
if (await
|
|
868
|
-
const corePkg = await
|
|
993
|
+
if (await fs6.pathExists(corePkgPath)) {
|
|
994
|
+
const corePkg = await fs6.readJSON(corePkgPath);
|
|
869
995
|
logCheck("pass", `@rootnative/core@${corePkg.version} installed`);
|
|
870
996
|
} else {
|
|
871
997
|
logCheck(
|
|
@@ -882,65 +1008,67 @@ async function doctorCommand(cwd) {
|
|
|
882
1008
|
"TypeScript not detected. RootNative components use TypeScript."
|
|
883
1009
|
);
|
|
884
1010
|
}
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
const
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
}
|
|
896
|
-
if (componentDirs.length > 0) {
|
|
897
|
-
let integrityOk = true;
|
|
898
|
-
for (const dir of componentDirs) {
|
|
899
|
-
const indexPath = path5.join(componentsDir, dir, "index.ts");
|
|
900
|
-
if (!await fs5.pathExists(indexPath)) {
|
|
901
|
-
logCheck("warn", `Component ${dir} is missing index.ts`);
|
|
902
|
-
integrityOk = false;
|
|
1011
|
+
if (config) {
|
|
1012
|
+
const componentsDir = resolveAliasPath(config.aliases.components, cwd);
|
|
1013
|
+
if (await fs6.pathExists(componentsDir)) {
|
|
1014
|
+
const dirs = await fs6.readdir(componentsDir);
|
|
1015
|
+
const componentDirs = [];
|
|
1016
|
+
for (const dir of dirs) {
|
|
1017
|
+
const fullPath = path8.join(componentsDir, dir);
|
|
1018
|
+
const stat = await fs6.stat(fullPath);
|
|
1019
|
+
if (stat.isDirectory()) {
|
|
1020
|
+
componentDirs.push(dir);
|
|
903
1021
|
}
|
|
904
1022
|
}
|
|
905
|
-
if (
|
|
1023
|
+
if (componentDirs.length > 0) {
|
|
1024
|
+
let integrityOk = true;
|
|
1025
|
+
for (const dir of componentDirs) {
|
|
1026
|
+
const indexPath = path8.join(componentsDir, dir, "index.ts");
|
|
1027
|
+
if (!await fs6.pathExists(indexPath)) {
|
|
1028
|
+
logCheck("warn", `Component ${dir} is missing index.ts`);
|
|
1029
|
+
integrityOk = false;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
if (integrityOk) {
|
|
1033
|
+
logCheck(
|
|
1034
|
+
"pass",
|
|
1035
|
+
`${componentDirs.length} component(s) installed, all files present`
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
} else {
|
|
906
1039
|
logCheck(
|
|
907
|
-
"
|
|
908
|
-
|
|
1040
|
+
"warn",
|
|
1041
|
+
'No components installed yet. Run "rootnative add <component>".'
|
|
909
1042
|
);
|
|
910
1043
|
}
|
|
911
1044
|
} else {
|
|
912
1045
|
logCheck(
|
|
913
1046
|
"warn",
|
|
914
|
-
|
|
1047
|
+
`Components directory not found at ${config.aliases.components}`
|
|
915
1048
|
);
|
|
916
1049
|
}
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
} else {
|
|
928
|
-
if (await fs5.pathExists(componentsDir)) {
|
|
929
|
-
const dirs = await fs5.readdir(componentsDir);
|
|
930
|
-
if (dirs.length > 0) {
|
|
931
|
-
logCheck("warn", "Utility barrel file (rootnative-utils.ts) missing");
|
|
1050
|
+
const libDir = resolveAliasPath(config.aliases.lib, cwd);
|
|
1051
|
+
const barrelPath = path8.join(libDir, "rootnative-utils.ts");
|
|
1052
|
+
if (await fs6.pathExists(barrelPath)) {
|
|
1053
|
+
logCheck("pass", "Utility barrel file present");
|
|
1054
|
+
} else {
|
|
1055
|
+
if (await fs6.pathExists(componentsDir)) {
|
|
1056
|
+
const dirs = await fs6.readdir(componentsDir);
|
|
1057
|
+
if (dirs.length > 0) {
|
|
1058
|
+
logCheck("warn", "Utility barrel file (rootnative-utils.ts) missing");
|
|
1059
|
+
}
|
|
932
1060
|
}
|
|
933
1061
|
}
|
|
934
1062
|
}
|
|
935
|
-
const nodeModules =
|
|
936
|
-
const inertiaPkgPath =
|
|
1063
|
+
const nodeModules = path8.resolve(cwd, "node_modules");
|
|
1064
|
+
const inertiaPkgPath = path8.join(
|
|
937
1065
|
nodeModules,
|
|
938
1066
|
"@rootnative",
|
|
939
1067
|
"inertia",
|
|
940
1068
|
"package.json"
|
|
941
1069
|
);
|
|
942
|
-
if (await
|
|
943
|
-
const inertiaPkg = await
|
|
1070
|
+
if (await fs6.pathExists(inertiaPkgPath)) {
|
|
1071
|
+
const inertiaPkg = await fs6.readJSON(inertiaPkgPath);
|
|
944
1072
|
logCheck("pass", `@rootnative/inertia@${inertiaPkg.version} installed`);
|
|
945
1073
|
} else {
|
|
946
1074
|
logCheck(
|
|
@@ -949,19 +1077,28 @@ async function doctorCommand(cwd) {
|
|
|
949
1077
|
);
|
|
950
1078
|
issues++;
|
|
951
1079
|
}
|
|
952
|
-
const
|
|
953
|
-
|
|
1080
|
+
for (const peer of REQUIRED_BARREL_PEERS) {
|
|
1081
|
+
if (await fs6.pathExists(path8.join(nodeModules, peer.name))) {
|
|
1082
|
+
logCheck("pass", `${peer.name} installed`);
|
|
1083
|
+
continue;
|
|
1084
|
+
}
|
|
1085
|
+
logCheck("fail", `${peer.name} is not installed \u2014 ${peer.reason}.`);
|
|
1086
|
+
console.log(` Run: ${chalk4.bold(`npx expo install ${peer.name}`)}`);
|
|
1087
|
+
issues++;
|
|
1088
|
+
}
|
|
1089
|
+
const safeAreaInstalled = await fs6.pathExists(
|
|
1090
|
+
path8.join(nodeModules, "react-native-safe-area-context")
|
|
954
1091
|
);
|
|
955
1092
|
if (safeAreaInstalled) {
|
|
956
1093
|
logCheck("pass", "react-native-safe-area-context installed");
|
|
957
1094
|
} else {
|
|
958
1095
|
logCheck(
|
|
959
1096
|
"warn",
|
|
960
|
-
"react-native-safe-area-context not installed (needed by: appbar, layout)"
|
|
1097
|
+
"react-native-safe-area-context not installed (needed by: appbar, layout, bottom-sheet, navigation-bar, snackbar)"
|
|
961
1098
|
);
|
|
962
1099
|
}
|
|
963
|
-
const vectorIconsInstalled = await
|
|
964
|
-
|
|
1100
|
+
const vectorIconsInstalled = await fs6.pathExists(
|
|
1101
|
+
path8.join(nodeModules, "@expo", "vector-icons")
|
|
965
1102
|
);
|
|
966
1103
|
if (vectorIconsInstalled) {
|
|
967
1104
|
logCheck("pass", "@expo/vector-icons installed");
|
|
@@ -974,6 +1111,7 @@ async function doctorCommand(cwd) {
|
|
|
974
1111
|
logger.break();
|
|
975
1112
|
if (issues > 0) {
|
|
976
1113
|
logger.error(`${issues} issue(s) found.`);
|
|
1114
|
+
process.exitCode = 1;
|
|
977
1115
|
} else {
|
|
978
1116
|
logger.success("All checks passed!");
|
|
979
1117
|
}
|
|
@@ -986,8 +1124,8 @@ import { execa as execa3 } from "execa";
|
|
|
986
1124
|
import prompts3 from "prompts";
|
|
987
1125
|
|
|
988
1126
|
// src/lib/llm-docs.ts
|
|
989
|
-
import
|
|
990
|
-
import
|
|
1127
|
+
import path9 from "path";
|
|
1128
|
+
import fs7 from "fs-extra";
|
|
991
1129
|
var CLAUDE_MD = "CLAUDE.md";
|
|
992
1130
|
var POINTER_MARKER = "@rootnative/components/llms.txt";
|
|
993
1131
|
var LLM_DOCS_POINTER = `## RootNative UI docs for AI agents
|
|
@@ -1002,22 +1140,49 @@ This project uses RootNative UI (\`@rootnative/*\`). LLM-optimized docs:
|
|
|
1002
1140
|
Prefer the \`node_modules\` copies \u2014 they match the installed version exactly.
|
|
1003
1141
|
`;
|
|
1004
1142
|
async function ensureLlmDocsPointer(cwd) {
|
|
1005
|
-
const filePath =
|
|
1006
|
-
if (!await
|
|
1007
|
-
await
|
|
1143
|
+
const filePath = path9.join(cwd, CLAUDE_MD);
|
|
1144
|
+
if (!await fs7.pathExists(filePath)) {
|
|
1145
|
+
await fs7.outputFile(filePath, `# CLAUDE.md
|
|
1008
1146
|
|
|
1009
1147
|
${LLM_DOCS_POINTER}`);
|
|
1010
1148
|
return "created";
|
|
1011
1149
|
}
|
|
1012
|
-
const existing = await
|
|
1150
|
+
const existing = await fs7.readFile(filePath, "utf8");
|
|
1013
1151
|
if (existing.includes(POINTER_MARKER)) {
|
|
1014
1152
|
return "already-present";
|
|
1015
1153
|
}
|
|
1016
1154
|
const separator = existing.endsWith("\n") ? "\n" : "\n\n";
|
|
1017
|
-
await
|
|
1155
|
+
await fs7.outputFile(filePath, `${existing}${separator}${LLM_DOCS_POINTER}`);
|
|
1018
1156
|
return "appended";
|
|
1019
1157
|
}
|
|
1020
1158
|
|
|
1159
|
+
// src/lib/registry-version.ts
|
|
1160
|
+
var NPM_REGISTRY2 = "https://registry.npmjs.org";
|
|
1161
|
+
async function resolveRegistryVersion() {
|
|
1162
|
+
const fallback = "main";
|
|
1163
|
+
try {
|
|
1164
|
+
const res = await fetch(`${NPM_REGISTRY2}/@rootnative/core`);
|
|
1165
|
+
if (!res.ok) {
|
|
1166
|
+
return { version: fallback, fallback: { reason: "npm-unreachable" } };
|
|
1167
|
+
}
|
|
1168
|
+
const data = await res.json();
|
|
1169
|
+
const version = data["dist-tags"]?.latest;
|
|
1170
|
+
if (!version) {
|
|
1171
|
+
return { version: fallback, fallback: { reason: "no-latest-tag" } };
|
|
1172
|
+
}
|
|
1173
|
+
const ref = `v${version}`;
|
|
1174
|
+
const probe = await fetch(
|
|
1175
|
+
`${DEFAULT_CONFIG.registryUrl}/${ref}/registry/index.json`
|
|
1176
|
+
);
|
|
1177
|
+
if (!probe.ok) {
|
|
1178
|
+
return { version: fallback, fallback: { reason: "tag-missing", version } };
|
|
1179
|
+
}
|
|
1180
|
+
return { version: ref };
|
|
1181
|
+
} catch {
|
|
1182
|
+
return { version: fallback, fallback: { reason: "npm-unreachable" } };
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1021
1186
|
// src/commands/init.ts
|
|
1022
1187
|
async function initCommand(cwd, options = {}) {
|
|
1023
1188
|
logger.break();
|
|
@@ -1092,8 +1257,10 @@ async function initCommand(cwd, options = {}) {
|
|
|
1092
1257
|
componentsAlias = answers.componentsAlias;
|
|
1093
1258
|
libAlias = answers.libAlias;
|
|
1094
1259
|
}
|
|
1260
|
+
const registry = await resolveRegistryVersion();
|
|
1095
1261
|
const config = {
|
|
1096
1262
|
...DEFAULT_CONFIG,
|
|
1263
|
+
registryVersion: registry.version,
|
|
1097
1264
|
aliases: {
|
|
1098
1265
|
components: componentsAlias,
|
|
1099
1266
|
lib: libAlias
|
|
@@ -1101,6 +1268,34 @@ async function initCommand(cwd, options = {}) {
|
|
|
1101
1268
|
};
|
|
1102
1269
|
await writeConfig(cwd, config);
|
|
1103
1270
|
logger.success("Created rootnative.json");
|
|
1271
|
+
if (registry.fallback) {
|
|
1272
|
+
logger.break();
|
|
1273
|
+
if (registry.fallback.reason === "tag-missing") {
|
|
1274
|
+
logger.warn(
|
|
1275
|
+
`Could not pin the registry to v${registry.fallback.version}; using "main".`
|
|
1276
|
+
);
|
|
1277
|
+
logger.warn(
|
|
1278
|
+
"Copied component source may not match your installed packages."
|
|
1279
|
+
);
|
|
1280
|
+
} else {
|
|
1281
|
+
logger.warn('Could not reach npm to pin the registry; using "main".');
|
|
1282
|
+
logger.warn(
|
|
1283
|
+
"Copied component source will come from the development branch."
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
1286
|
+
logger.break();
|
|
1287
|
+
}
|
|
1288
|
+
const tsconfigPatch = await ensureTsconfigPaths(cwd, componentsAlias);
|
|
1289
|
+
if (tsconfigPatch.status === "added") {
|
|
1290
|
+
logger.success(
|
|
1291
|
+
`Added "${tsconfigPatch.prefix}/*" path alias to tsconfig.json`
|
|
1292
|
+
);
|
|
1293
|
+
} else if (tsconfigPatch.status === "failed") {
|
|
1294
|
+
logger.warn(`Could not update tsconfig.json: ${tsconfigPatch.error}`);
|
|
1295
|
+
logger.info(
|
|
1296
|
+
`Add a "paths" mapping for "${componentsAlias.split("/")[0]}/*" by hand.`
|
|
1297
|
+
);
|
|
1298
|
+
}
|
|
1104
1299
|
let addLlmDocs = options.yes;
|
|
1105
1300
|
if (!options.yes) {
|
|
1106
1301
|
const answer = await prompts3({
|
|
@@ -1157,9 +1352,9 @@ async function initCommand(cwd, options = {}) {
|
|
|
1157
1352
|
}
|
|
1158
1353
|
|
|
1159
1354
|
// src/commands/list.ts
|
|
1160
|
-
import
|
|
1355
|
+
import path10 from "path";
|
|
1161
1356
|
import chalk6 from "chalk";
|
|
1162
|
-
import
|
|
1357
|
+
import fs8 from "fs-extra";
|
|
1163
1358
|
async function listCommand(cwd) {
|
|
1164
1359
|
logger.break();
|
|
1165
1360
|
const config = await readConfig(cwd);
|
|
@@ -1185,8 +1380,8 @@ async function listCommand(cwd) {
|
|
|
1185
1380
|
` ${chalk6.dim("-".repeat(70))}`
|
|
1186
1381
|
);
|
|
1187
1382
|
for (const component of registryIndex.components) {
|
|
1188
|
-
const componentDir =
|
|
1189
|
-
const installed = await
|
|
1383
|
+
const componentDir = path10.join(componentsDir, component.name);
|
|
1384
|
+
const installed = await fs8.pathExists(componentDir);
|
|
1190
1385
|
const status = installed ? chalk6.green("installed") : chalk6.dim("-");
|
|
1191
1386
|
const nameDisplay = installed ? chalk6.green(component.name) : component.name;
|
|
1192
1387
|
console.log(
|
|
@@ -1206,9 +1401,9 @@ function padEnd(str, length) {
|
|
|
1206
1401
|
}
|
|
1207
1402
|
|
|
1208
1403
|
// src/commands/update.ts
|
|
1209
|
-
import
|
|
1404
|
+
import path11 from "path";
|
|
1210
1405
|
import chalk8 from "chalk";
|
|
1211
|
-
import
|
|
1406
|
+
import fs9 from "fs-extra";
|
|
1212
1407
|
import prompts4 from "prompts";
|
|
1213
1408
|
|
|
1214
1409
|
// src/lib/diff.ts
|
|
@@ -1358,8 +1553,8 @@ async function updateCommand(componentNames, cwd, options) {
|
|
|
1358
1553
|
targetNames = componentNames;
|
|
1359
1554
|
const missing = [];
|
|
1360
1555
|
for (const name of targetNames) {
|
|
1361
|
-
const dir =
|
|
1362
|
-
if (!await
|
|
1556
|
+
const dir = path11.join(componentsDir, name);
|
|
1557
|
+
if (!await fs9.pathExists(dir)) {
|
|
1363
1558
|
missing.push(name);
|
|
1364
1559
|
}
|
|
1365
1560
|
}
|
|
@@ -1382,8 +1577,8 @@ async function updateCommand(componentNames, cwd, options) {
|
|
|
1382
1577
|
resolveSpinner.succeed("Registry fetched");
|
|
1383
1578
|
const componentDiffs = [];
|
|
1384
1579
|
for (const { entry } of resolution.components) {
|
|
1385
|
-
const componentDir =
|
|
1386
|
-
if (!await
|
|
1580
|
+
const componentDir = path11.join(componentsDir, entry.name);
|
|
1581
|
+
if (!await fs9.pathExists(componentDir)) {
|
|
1387
1582
|
componentDiffs.push({
|
|
1388
1583
|
name: entry.name,
|
|
1389
1584
|
diffs: [],
|
|
@@ -1393,8 +1588,8 @@ async function updateCommand(componentNames, cwd, options) {
|
|
|
1393
1588
|
}
|
|
1394
1589
|
const diffs = [];
|
|
1395
1590
|
for (const filePath of entry.files) {
|
|
1396
|
-
const fileName =
|
|
1397
|
-
const localPath =
|
|
1591
|
+
const fileName = path11.basename(filePath);
|
|
1592
|
+
const localPath = path11.join(componentDir, fileName);
|
|
1398
1593
|
const remoteContent = await fetchFileContent(config, filePath);
|
|
1399
1594
|
const transformed = transformImports(remoteContent, {
|
|
1400
1595
|
config,
|
|
@@ -1402,8 +1597,8 @@ async function updateCommand(componentNames, cwd, options) {
|
|
|
1402
1597
|
installedComponents: allComponentNames
|
|
1403
1598
|
});
|
|
1404
1599
|
let localContent = "";
|
|
1405
|
-
if (await
|
|
1406
|
-
localContent = await
|
|
1600
|
+
if (await fs9.pathExists(localPath)) {
|
|
1601
|
+
localContent = await fs9.readFile(localPath, "utf-8");
|
|
1407
1602
|
}
|
|
1408
1603
|
diffs.push(computeDiff(localContent, transformed, fileName));
|
|
1409
1604
|
}
|
|
@@ -1414,12 +1609,12 @@ async function updateCommand(componentNames, cwd, options) {
|
|
|
1414
1609
|
for (const utilName of resolution.utils) {
|
|
1415
1610
|
const utilEntry = utilsRegistry[utilName];
|
|
1416
1611
|
if (!utilEntry) continue;
|
|
1417
|
-
const fileName =
|
|
1418
|
-
const localPath =
|
|
1612
|
+
const fileName = path11.basename(utilEntry.file);
|
|
1613
|
+
const localPath = path11.join(libDir, fileName);
|
|
1419
1614
|
const remoteContent = await fetchFileContent(config, utilEntry.file);
|
|
1420
1615
|
let localContent = "";
|
|
1421
|
-
if (await
|
|
1422
|
-
localContent = await
|
|
1616
|
+
if (await fs9.pathExists(localPath)) {
|
|
1617
|
+
localContent = await fs9.readFile(localPath, "utf-8");
|
|
1423
1618
|
}
|
|
1424
1619
|
utilDiffs.push(computeDiff(localContent, remoteContent, `lib/${fileName}`));
|
|
1425
1620
|
}
|
|
@@ -1431,10 +1626,10 @@ async function updateCommand(componentNames, cwd, options) {
|
|
|
1431
1626
|
}
|
|
1432
1627
|
}
|
|
1433
1628
|
const newBarrel = generateUtilsBarrel(resolution.utils, utilExports);
|
|
1434
|
-
const barrelPath =
|
|
1629
|
+
const barrelPath = path11.join(libDir, "rootnative-utils.ts");
|
|
1435
1630
|
let oldBarrel = "";
|
|
1436
|
-
if (await
|
|
1437
|
-
oldBarrel = await
|
|
1631
|
+
if (await fs9.pathExists(barrelPath)) {
|
|
1632
|
+
oldBarrel = await fs9.readFile(barrelPath, "utf-8");
|
|
1438
1633
|
}
|
|
1439
1634
|
utilDiffs.push(computeDiff(oldBarrel, newBarrel, "lib/rootnative-utils.ts"));
|
|
1440
1635
|
const changedComponents = componentDiffs.filter((c) => c.hasChanges);
|
|
@@ -1497,8 +1692,8 @@ async function updateCommand(componentNames, cwd, options) {
|
|
|
1497
1692
|
const applySpinner = createSpinner("Applying updates...");
|
|
1498
1693
|
applySpinner.start();
|
|
1499
1694
|
for (const comp of changedComponents) {
|
|
1500
|
-
const componentDir =
|
|
1501
|
-
await
|
|
1695
|
+
const componentDir = path11.join(componentsDir, comp.name);
|
|
1696
|
+
await fs9.ensureDir(componentDir);
|
|
1502
1697
|
if (comp.diffs.length === 0) {
|
|
1503
1698
|
const entry = resolution.components.find(
|
|
1504
1699
|
(c) => c.entry.name === comp.name
|
|
@@ -1506,14 +1701,14 @@ async function updateCommand(componentNames, cwd, options) {
|
|
|
1506
1701
|
if (!entry) continue;
|
|
1507
1702
|
for (const filePath of entry.entry.files) {
|
|
1508
1703
|
const content = await fetchFileContent(config, filePath);
|
|
1509
|
-
const fileName =
|
|
1704
|
+
const fileName = path11.basename(filePath);
|
|
1510
1705
|
const transformed = transformImports(content, {
|
|
1511
1706
|
config,
|
|
1512
1707
|
componentName: comp.name,
|
|
1513
1708
|
installedComponents: allComponentNames
|
|
1514
1709
|
});
|
|
1515
|
-
await
|
|
1516
|
-
|
|
1710
|
+
await fs9.writeFile(
|
|
1711
|
+
path11.join(componentDir, fileName),
|
|
1517
1712
|
transformed,
|
|
1518
1713
|
"utf-8"
|
|
1519
1714
|
);
|
|
@@ -1523,23 +1718,23 @@ async function updateCommand(componentNames, cwd, options) {
|
|
|
1523
1718
|
for (const diff of comp.diffs) {
|
|
1524
1719
|
if (!diff.hasChanges) continue;
|
|
1525
1720
|
const newContent = diff.lines.filter((l) => l.type !== "remove").map((l) => l.content).join("\n");
|
|
1526
|
-
await
|
|
1527
|
-
|
|
1721
|
+
await fs9.writeFile(
|
|
1722
|
+
path11.join(componentDir, diff.fileName),
|
|
1528
1723
|
newContent,
|
|
1529
1724
|
"utf-8"
|
|
1530
1725
|
);
|
|
1531
1726
|
}
|
|
1532
1727
|
}
|
|
1533
|
-
await
|
|
1728
|
+
await fs9.ensureDir(libDir);
|
|
1534
1729
|
for (const diff of changedUtils) {
|
|
1535
1730
|
if (!diff.hasChanges) continue;
|
|
1536
1731
|
const newContent = diff.lines.filter((l) => l.type !== "remove").map((l) => l.content).join("\n");
|
|
1537
|
-
const filePath =
|
|
1732
|
+
const filePath = path11.join(
|
|
1538
1733
|
libDir,
|
|
1539
1734
|
// diff.fileName is like "lib/color.ts" — strip the "lib/" prefix
|
|
1540
1735
|
diff.fileName.replace(/^lib\//, "")
|
|
1541
1736
|
);
|
|
1542
|
-
await
|
|
1737
|
+
await fs9.writeFile(filePath, newContent, "utf-8");
|
|
1543
1738
|
}
|
|
1544
1739
|
applySpinner.succeed("Updates applied");
|
|
1545
1740
|
logger.break();
|
|
@@ -1549,17 +1744,17 @@ async function updateCommand(componentNames, cwd, options) {
|
|
|
1549
1744
|
logger.break();
|
|
1550
1745
|
}
|
|
1551
1746
|
async function getInstalledComponents(componentsDir) {
|
|
1552
|
-
if (!await
|
|
1747
|
+
if (!await fs9.pathExists(componentsDir)) {
|
|
1553
1748
|
return [];
|
|
1554
1749
|
}
|
|
1555
|
-
const entries = await
|
|
1750
|
+
const entries = await fs9.readdir(componentsDir);
|
|
1556
1751
|
const components = [];
|
|
1557
1752
|
for (const entry of entries) {
|
|
1558
|
-
const fullPath =
|
|
1559
|
-
const stat = await
|
|
1753
|
+
const fullPath = path11.join(componentsDir, entry);
|
|
1754
|
+
const stat = await fs9.stat(fullPath);
|
|
1560
1755
|
if (stat.isDirectory()) {
|
|
1561
|
-
const indexPath =
|
|
1562
|
-
if (await
|
|
1756
|
+
const indexPath = path11.join(fullPath, "index.ts");
|
|
1757
|
+
if (await fs9.pathExists(indexPath)) {
|
|
1563
1758
|
components.push(entry);
|
|
1564
1759
|
}
|
|
1565
1760
|
}
|
|
@@ -1568,10 +1763,10 @@ async function getInstalledComponents(componentsDir) {
|
|
|
1568
1763
|
}
|
|
1569
1764
|
|
|
1570
1765
|
// src/commands/upgrade.ts
|
|
1571
|
-
import
|
|
1766
|
+
import path12 from "path";
|
|
1572
1767
|
import chalk9 from "chalk";
|
|
1573
1768
|
import { execa as execa4 } from "execa";
|
|
1574
|
-
import
|
|
1769
|
+
import fs10 from "fs-extra";
|
|
1575
1770
|
import prompts5 from "prompts";
|
|
1576
1771
|
async function fetchLatestFromNpm(packageName) {
|
|
1577
1772
|
const url = `https://registry.npmjs.org/${packageName}/latest`;
|
|
@@ -1584,16 +1779,16 @@ async function fetchLatestFromNpm(packageName) {
|
|
|
1584
1779
|
return response.json();
|
|
1585
1780
|
}
|
|
1586
1781
|
function getInstalledPackageInfo(cwd, packageName) {
|
|
1587
|
-
const pkgPath =
|
|
1782
|
+
const pkgPath = path12.resolve(
|
|
1588
1783
|
cwd,
|
|
1589
1784
|
"node_modules",
|
|
1590
1785
|
...packageName.split("/"),
|
|
1591
1786
|
"package.json"
|
|
1592
1787
|
);
|
|
1593
|
-
if (!
|
|
1788
|
+
if (!fs10.pathExistsSync(pkgPath)) {
|
|
1594
1789
|
return null;
|
|
1595
1790
|
}
|
|
1596
|
-
const pkg =
|
|
1791
|
+
const pkg = fs10.readJSONSync(pkgPath);
|
|
1597
1792
|
return {
|
|
1598
1793
|
version: pkg.version,
|
|
1599
1794
|
peerDependencies: pkg.peerDependencies,
|
|
@@ -1624,9 +1819,9 @@ function diffPeerDeps(current, latest) {
|
|
|
1624
1819
|
return { added, changed, removed };
|
|
1625
1820
|
}
|
|
1626
1821
|
function getProjectDeps(cwd) {
|
|
1627
|
-
const pkgPath =
|
|
1822
|
+
const pkgPath = path12.resolve(cwd, "package.json");
|
|
1628
1823
|
try {
|
|
1629
|
-
const pkg =
|
|
1824
|
+
const pkg = fs10.readJSONSync(pkgPath);
|
|
1630
1825
|
return {
|
|
1631
1826
|
...pkg.dependencies,
|
|
1632
1827
|
...pkg.devDependencies
|
|
@@ -1638,6 +1833,19 @@ function getProjectDeps(cwd) {
|
|
|
1638
1833
|
function hasDiffChanges(diff) {
|
|
1639
1834
|
return Object.keys(diff.added).length > 0 || Object.keys(diff.changed).length > 0 || diff.removed.length > 0;
|
|
1640
1835
|
}
|
|
1836
|
+
async function repinRegistryVersion(cwd) {
|
|
1837
|
+
const config = await readConfig(cwd);
|
|
1838
|
+
const next = await resolveRegistryVersion();
|
|
1839
|
+
if (next.fallback && config.registryVersion !== "main") {
|
|
1840
|
+
logger.warn(
|
|
1841
|
+
`Kept the registry pinned to ${chalk9.bold(config.registryVersion)} \u2014 could not resolve a newer release tag.`
|
|
1842
|
+
);
|
|
1843
|
+
return;
|
|
1844
|
+
}
|
|
1845
|
+
if (config.registryVersion === next.version) return;
|
|
1846
|
+
await writeConfig(cwd, { ...config, registryVersion: next.version });
|
|
1847
|
+
logger.success(`Registry pinned to ${chalk9.bold(next.version)}`);
|
|
1848
|
+
}
|
|
1641
1849
|
async function upgradeCommand(cwd, options = {}) {
|
|
1642
1850
|
logger.break();
|
|
1643
1851
|
await readConfig(cwd);
|
|
@@ -1666,6 +1874,7 @@ async function upgradeCommand(cwd, options = {}) {
|
|
|
1666
1874
|
`Already on the latest version ${chalk9.bold(`v${installed.version}`)}`
|
|
1667
1875
|
);
|
|
1668
1876
|
logger.break();
|
|
1877
|
+
await repinRegistryVersion(cwd);
|
|
1669
1878
|
if (options.all) {
|
|
1670
1879
|
logger.info("Updating installed components...");
|
|
1671
1880
|
await updateCommand([], cwd, { all: true, dryRun: false });
|
|
@@ -1783,6 +1992,7 @@ async function upgradeCommand(cwd, options = {}) {
|
|
|
1783
1992
|
logger.info(` ${pkg}`);
|
|
1784
1993
|
}
|
|
1785
1994
|
}
|
|
1995
|
+
await repinRegistryVersion(cwd);
|
|
1786
1996
|
logger.break();
|
|
1787
1997
|
if (options.all) {
|
|
1788
1998
|
logger.info("Updating installed components...");
|
|
@@ -1834,7 +2044,7 @@ program.command("init").description("Initialize your project for RootNative UI")
|
|
|
1834
2044
|
handleError(error);
|
|
1835
2045
|
}
|
|
1836
2046
|
});
|
|
1837
|
-
program.command("add").description("Add components to your project").argument("<components...>", "Component names to add").option("-f, --force", "Overwrite existing components", false).option(
|
|
2047
|
+
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(
|
|
1838
2048
|
"-d, --dry-run",
|
|
1839
2049
|
"Show what would be installed without making changes",
|
|
1840
2050
|
false
|
|
@@ -1849,6 +2059,7 @@ program.command("add").description("Add components to your project").argument("<
|
|
|
1849
2059
|
await addCommand(components, process.cwd(), {
|
|
1850
2060
|
force: options.force,
|
|
1851
2061
|
dryRun: options.dryRun,
|
|
2062
|
+
yes: options.yes,
|
|
1852
2063
|
packageManager: options.packageManager
|
|
1853
2064
|
});
|
|
1854
2065
|
} 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.7
|
|
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.7"
|
|
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
|
|