@sublay/cli 7.0.1 → 7.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -13
- package/dist/index.js +583 -108
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -51,18 +51,27 @@ Sublay ships these layers as **pre-modeled bundles** that attach to one shared e
|
|
|
51
51
|
|
|
52
52
|
## Bundles you can install
|
|
53
53
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
- **
|
|
57
|
-
- **
|
|
58
|
-
- **
|
|
59
|
-
- **
|
|
60
|
-
- **Follows
|
|
61
|
-
- **
|
|
62
|
-
- **
|
|
63
|
-
- **
|
|
64
|
-
|
|
65
|
-
|
|
54
|
+
Every project always includes the **`core`** bundle — users, authentication, and OAuth identity — installed automatically and it can't be removed. On top of that, install only what your product needs:
|
|
55
|
+
|
|
56
|
+
- **Entities** (`entities`) — the base content unit for anything your users create (posts, articles, listings), with views, drafts, and publishing
|
|
57
|
+
- **Comments** (`comments`) — threaded comments on entities · requires `entities`
|
|
58
|
+
- **Reactions** (`reactions`) — emoji reactions on entities and comments
|
|
59
|
+
- **Storage** (`files-images`) — file and image uploads, including user avatars and banners
|
|
60
|
+
- **Follows** (`follows`) — one-way follow relationships
|
|
61
|
+
- **Connections** (`connections`) — bidirectional, friend-style connection requests
|
|
62
|
+
- **Spaces** (`spaces`) — hierarchical community spaces with membership, roles, and rules
|
|
63
|
+
- **Workspaces** (`workspaces`) — self-nesting SaaS/team workspaces with invitations and per-member authority
|
|
64
|
+
- **Chat** (`chat`) — real-time 1:1 and group conversations with message reactions
|
|
65
|
+
- **Collections** (`collections`) — user-owned bookmarking and folders for saving entities · requires `entities`
|
|
66
|
+
- **Events** (`events`) — online, physical, or hybrid events with RSVP tracking, capacity limits, hosts, and invites
|
|
67
|
+
- **Moderation** (`moderation`) — reports, report resolution, user blocking, and account suspensions
|
|
68
|
+
- **App Notifications** (`notifications`) — in-app notification fan-out
|
|
69
|
+
- **Push Notifications** (`push`) — native push delivery to iOS, Android, and Web
|
|
70
|
+
- **Reputation** (`reputation`) — per-space reputation buckets, a maintained overall total per user, and reputation grants
|
|
71
|
+
- **Semantic Search & AI** (`ai-search`) — content embeddings, semantic search, and an AI-answer endpoint
|
|
72
|
+
- **Interest Matching** (`interest-matching`) — activity-derived interest facets for matching users by what they engage with · requires `ai-search`
|
|
73
|
+
|
|
74
|
+
Every bundle attaches to the same project schema — no mismatches, no extra databases. Install, remove, or add bundles at any time from the dashboard's Database page; removing a bundle drops its tables (and clears references to it from other bundles) for good.
|
|
66
75
|
|
|
67
76
|
## How it works
|
|
68
77
|
|
|
@@ -84,7 +93,7 @@ The dashboard at [dash.sublay.io](https://dash.sublay.io) is a database/backend
|
|
|
84
93
|
|
|
85
94
|
## The Sublay SDK family
|
|
86
95
|
|
|
87
|
-
- [`@sublay/core`](https://www.npmjs.com/package/@sublay/core) —
|
|
96
|
+
- [`@sublay/core`](https://www.npmjs.com/package/@sublay/core) — hooks and utilities shared across the React, React Native, and Expo SDKs
|
|
88
97
|
- [`@sublay/react-js`](https://www.npmjs.com/package/@sublay/react-js) — React (web)
|
|
89
98
|
- [`@sublay/react-native`](https://www.npmjs.com/package/@sublay/react-native) — React Native
|
|
90
99
|
- [`@sublay/expo`](https://www.npmjs.com/package/@sublay/expo) — Expo with SecureStore token storage
|
package/dist/index.js
CHANGED
|
@@ -60,6 +60,34 @@ async function detectTypeScript() {
|
|
|
60
60
|
return false;
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
|
+
async function detectFramework(platform) {
|
|
64
|
+
if (platform !== "react") {
|
|
65
|
+
return platform;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const packageJsonPath = path.join(process.cwd(), "package.json");
|
|
69
|
+
if (!await fs.pathExists(packageJsonPath)) {
|
|
70
|
+
return "other";
|
|
71
|
+
}
|
|
72
|
+
const packageJson2 = await fs.readJson(packageJsonPath);
|
|
73
|
+
const deps = {
|
|
74
|
+
...packageJson2.dependencies,
|
|
75
|
+
...packageJson2.devDependencies
|
|
76
|
+
};
|
|
77
|
+
if (deps["next"]) {
|
|
78
|
+
return "next";
|
|
79
|
+
}
|
|
80
|
+
if (deps["react-scripts"]) {
|
|
81
|
+
return "cra";
|
|
82
|
+
}
|
|
83
|
+
if (deps["vite"]) {
|
|
84
|
+
return "vite";
|
|
85
|
+
}
|
|
86
|
+
return "other";
|
|
87
|
+
} catch (error) {
|
|
88
|
+
return "other";
|
|
89
|
+
}
|
|
90
|
+
}
|
|
63
91
|
|
|
64
92
|
// src/commands/init.ts
|
|
65
93
|
async function init() {
|
|
@@ -151,79 +179,283 @@ var __filename = fileURLToPath(import.meta.url);
|
|
|
151
179
|
var __dirname = path3.dirname(__filename);
|
|
152
180
|
var LOCAL_REGISTRY_PATH = path3.resolve(__dirname, "..", "..", "..", "registry");
|
|
153
181
|
var REMOTE_REGISTRY_BASE = "https://raw.githubusercontent.com/sublay-io/monorepo/main/registry";
|
|
154
|
-
|
|
182
|
+
var DEFAULT_REGISTRY_REF = "main";
|
|
183
|
+
var LOCAL_PATH_ENV = "SUBLAY_REGISTRY_LOCAL_PATH";
|
|
184
|
+
var REMOTE_BASE_ENV = "SUBLAY_REGISTRY_BASE_URL";
|
|
185
|
+
var RegistryError = class extends Error {
|
|
186
|
+
constructor(message) {
|
|
187
|
+
super(message);
|
|
188
|
+
this.name = "RegistryError";
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
function validateRef(ref) {
|
|
192
|
+
if (typeof ref !== "string" || ref.length === 0) {
|
|
193
|
+
return "A ref cannot be empty.";
|
|
194
|
+
}
|
|
195
|
+
if (!/^[A-Za-z0-9._\-/]+$/.test(ref)) {
|
|
196
|
+
return `Invalid ref "${ref}": a ref may only contain letters, digits, ".", "_", "-" and "/".`;
|
|
197
|
+
}
|
|
198
|
+
const segments = ref.split("/");
|
|
199
|
+
if (segments.some((segment) => segment === "..")) {
|
|
200
|
+
return `Invalid ref "${ref}": a ref cannot contain a ".." segment.`;
|
|
201
|
+
}
|
|
202
|
+
if (segments.some((segment) => segment.length === 0)) {
|
|
203
|
+
return `Invalid ref "${ref}": a ref cannot start or end with "/" or contain "//".`;
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
function validateComponentName(name) {
|
|
208
|
+
if (typeof name !== "string" || !/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) {
|
|
209
|
+
return `Invalid component name "${name}": a component name may only contain lowercase letters, digits and hyphens (for example "comments-threaded").`;
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
function getRemoteRegistryBase(ref = DEFAULT_REGISTRY_REF, env = process.env) {
|
|
214
|
+
const base = (env[REMOTE_BASE_ENV] || REMOTE_REGISTRY_BASE).replace(/\/+$/, "");
|
|
215
|
+
const suffix = `/${DEFAULT_REGISTRY_REF}/registry`;
|
|
216
|
+
if (!base.endsWith(suffix)) {
|
|
217
|
+
throw new RegistryError(
|
|
218
|
+
`Registry base URL "${base}" must end with "${suffix}".`
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
if (ref === DEFAULT_REGISTRY_REF) {
|
|
222
|
+
return base;
|
|
223
|
+
}
|
|
224
|
+
return `${base.slice(0, -suffix.length)}/${ref}/registry`;
|
|
225
|
+
}
|
|
226
|
+
function resolveRegistrySource(ref, env = process.env) {
|
|
227
|
+
if (ref === void 0) {
|
|
228
|
+
return {
|
|
229
|
+
ref: DEFAULT_REGISTRY_REF,
|
|
230
|
+
explicitRef: false,
|
|
231
|
+
localRoot: env[LOCAL_PATH_ENV] || LOCAL_REGISTRY_PATH,
|
|
232
|
+
remoteBase: getRemoteRegistryBase(DEFAULT_REGISTRY_REF, env)
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
const problem = validateRef(ref);
|
|
236
|
+
if (problem) {
|
|
237
|
+
throw new RegistryError(problem);
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
ref,
|
|
241
|
+
explicitRef: true,
|
|
242
|
+
localRoot: null,
|
|
243
|
+
remoteBase: getRemoteRegistryBase(ref, env)
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
function getRegistryPlatform(config) {
|
|
247
|
+
return config.platform === "expo" ? "react-native" : config.platform;
|
|
248
|
+
}
|
|
249
|
+
function getComponentLocation(componentName, config) {
|
|
250
|
+
const problem = validateComponentName(componentName);
|
|
251
|
+
if (problem) {
|
|
252
|
+
throw new RegistryError(problem);
|
|
253
|
+
}
|
|
254
|
+
return `${getRegistryPlatform(config)}/${componentName}/${config.style}`;
|
|
255
|
+
}
|
|
256
|
+
function getRemoteUrl(source, registryPath) {
|
|
257
|
+
return `${source.remoteBase}/${registryPath}`;
|
|
258
|
+
}
|
|
259
|
+
async function readRegistryPath(source, registryPath) {
|
|
260
|
+
if (source.localRoot) {
|
|
261
|
+
const localPath = path3.join(source.localRoot, ...registryPath.split("/"));
|
|
262
|
+
if (await fs3.pathExists(localPath)) {
|
|
263
|
+
return { ok: true, content: await fs3.readFile(localPath, "utf-8") };
|
|
264
|
+
}
|
|
265
|
+
if (source.localOnly) {
|
|
266
|
+
return {
|
|
267
|
+
ok: false,
|
|
268
|
+
url: localPath,
|
|
269
|
+
status: 404,
|
|
270
|
+
reason: "not found in the local registry"
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const url = getRemoteUrl(source, registryPath);
|
|
155
275
|
try {
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
const localExists = await fs3.pathExists(registryPath);
|
|
165
|
-
if (localExists) {
|
|
166
|
-
return await fs3.readJson(registryPath);
|
|
167
|
-
}
|
|
168
|
-
const url = `${REMOTE_REGISTRY_BASE}/${registryPlatform}/${componentName}/${config.style}/registry.json`;
|
|
169
|
-
try {
|
|
170
|
-
const response = await fetch(url);
|
|
171
|
-
if (!response.ok) {
|
|
172
|
-
if (response.status === 404) {
|
|
173
|
-
console.error(`Component "${componentName}" with style "${config.style}" not found in registry.`);
|
|
174
|
-
} else {
|
|
175
|
-
console.error(`Failed to fetch registry from ${url}: ${response.status} ${response.statusText}`);
|
|
176
|
-
}
|
|
177
|
-
return null;
|
|
178
|
-
}
|
|
179
|
-
return await response.json();
|
|
180
|
-
} catch (fetchError) {
|
|
181
|
-
console.error(`Error fetching from GitHub (${url}):`, fetchError);
|
|
182
|
-
return null;
|
|
276
|
+
const response = await fetch(url);
|
|
277
|
+
if (!response.ok) {
|
|
278
|
+
return {
|
|
279
|
+
ok: false,
|
|
280
|
+
url,
|
|
281
|
+
status: response.status,
|
|
282
|
+
reason: `${response.status} ${response.statusText}`.trim()
|
|
283
|
+
};
|
|
183
284
|
}
|
|
285
|
+
return { ok: true, content: await response.text() };
|
|
184
286
|
} catch (error) {
|
|
185
|
-
|
|
186
|
-
|
|
287
|
+
return {
|
|
288
|
+
ok: false,
|
|
289
|
+
url,
|
|
290
|
+
reason: error instanceof Error ? error.message : String(error)
|
|
291
|
+
};
|
|
187
292
|
}
|
|
188
293
|
}
|
|
189
|
-
async function
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
const fileUrl = `${registryUrl}/${filePath}`;
|
|
294
|
+
async function pinRegistrySource(source, componentName, config) {
|
|
295
|
+
if (!source.localRoot) {
|
|
296
|
+
return source;
|
|
297
|
+
}
|
|
298
|
+
const location = getComponentLocation(componentName, config);
|
|
299
|
+
const manifestPath = path3.join(
|
|
300
|
+
source.localRoot,
|
|
301
|
+
...location.split("/"),
|
|
302
|
+
"registry.json"
|
|
303
|
+
);
|
|
304
|
+
return await fs3.pathExists(manifestPath) ? { ...source, localOnly: true } : { ...source, localRoot: null };
|
|
305
|
+
}
|
|
306
|
+
async function fetchRegistry(componentName, config, source) {
|
|
307
|
+
const location = getComponentLocation(componentName, config);
|
|
308
|
+
const result = await readRegistryPath(source, `${location}/registry.json`);
|
|
309
|
+
if (result.ok) {
|
|
207
310
|
try {
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
return await response.text();
|
|
214
|
-
} catch (fetchError) {
|
|
215
|
-
console.error(`Error fetching file from GitHub (${fileUrl}):`, fetchError);
|
|
216
|
-
return null;
|
|
311
|
+
return JSON.parse(result.content);
|
|
312
|
+
} catch {
|
|
313
|
+
throw new RegistryError(
|
|
314
|
+
`The registry manifest for "${componentName}" (${location}/registry.json) is not valid JSON.`
|
|
315
|
+
);
|
|
217
316
|
}
|
|
218
|
-
} catch (error) {
|
|
219
|
-
console.error(`Error fetching file ${filePath}:`, error);
|
|
220
|
-
return null;
|
|
221
317
|
}
|
|
318
|
+
if (result.status !== 404) {
|
|
319
|
+
throw new RegistryError(
|
|
320
|
+
`Failed to fetch registry from ${result.url}: ${result.reason}`
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
if (!source.explicitRef) {
|
|
324
|
+
throw new RegistryError(
|
|
325
|
+
`Component "${componentName}" with style "${config.style}" not found in registry.`
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
const probe = await readRegistryPath(source, "package.json");
|
|
329
|
+
if (probe.ok) {
|
|
330
|
+
throw new RegistryError(
|
|
331
|
+
`Component "${componentName}" with style "${config.style}" not found on branch "${source.ref}".`
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
if (probe.status === 404) {
|
|
335
|
+
throw new RegistryError(
|
|
336
|
+
`Branch "${source.ref}" not found: no registry is served from that branch.`
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
throw new RegistryError(
|
|
340
|
+
`Failed to fetch registry from ${probe.url}: ${probe.reason}`
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
async function fetchFile(source, location, filePath) {
|
|
344
|
+
const result = await readRegistryPath(source, `${location}/${filePath}`);
|
|
345
|
+
if (!result.ok) {
|
|
346
|
+
throw new RegistryError(
|
|
347
|
+
`Could not fetch ${filePath} from ${result.url}: ${result.reason}`
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
return result.content;
|
|
351
|
+
}
|
|
352
|
+
function getSharedManifestPath(platform) {
|
|
353
|
+
return `shared/${platform}/shared.json`;
|
|
354
|
+
}
|
|
355
|
+
function isStringArray(value) {
|
|
356
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
357
|
+
}
|
|
358
|
+
function parseSharedManifest(raw, label) {
|
|
359
|
+
const invalid = (detail) => new RegistryError(`The shared manifest ${label} is malformed: ${detail}.`);
|
|
360
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
361
|
+
throw invalid("it must be an object");
|
|
362
|
+
}
|
|
363
|
+
const { platform, pieces } = raw;
|
|
364
|
+
if (typeof pieces !== "object" || pieces === null || Array.isArray(pieces)) {
|
|
365
|
+
throw invalid('"pieces" must be an object keyed by piece name');
|
|
366
|
+
}
|
|
367
|
+
const parsed = {};
|
|
368
|
+
for (const [name, value] of Object.entries(pieces)) {
|
|
369
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
370
|
+
throw invalid(`piece "${name}" must be an object`);
|
|
371
|
+
}
|
|
372
|
+
const { files, requires = [], dependencies = [] } = value;
|
|
373
|
+
if (!isStringArray(files) || files.length === 0) {
|
|
374
|
+
throw invalid(`piece "${name}" must list at least one file`);
|
|
375
|
+
}
|
|
376
|
+
const outside = files.find(
|
|
377
|
+
(file) => !/^[A-Za-z0-9_][A-Za-z0-9._/-]*$/.test(file) || file.split("/").includes("..")
|
|
378
|
+
);
|
|
379
|
+
if (outside !== void 0) {
|
|
380
|
+
throw invalid(
|
|
381
|
+
`piece "${name}" lists "${outside}", which is not a path inside the shared folder`
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
if (!isStringArray(requires)) {
|
|
385
|
+
throw invalid(`piece "${name}" has a "requires" that is not a list of piece names`);
|
|
386
|
+
}
|
|
387
|
+
if (!isStringArray(dependencies)) {
|
|
388
|
+
throw invalid(`piece "${name}" has a "dependencies" that is not a list of packages`);
|
|
389
|
+
}
|
|
390
|
+
parsed[name] = { files, requires, dependencies };
|
|
391
|
+
}
|
|
392
|
+
return {
|
|
393
|
+
platform: typeof platform === "string" ? platform : "",
|
|
394
|
+
pieces: parsed
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
async function fetchSharedManifest(platform, source) {
|
|
398
|
+
const manifestPath = getSharedManifestPath(platform);
|
|
399
|
+
const result = await readRegistryPath(source, manifestPath);
|
|
400
|
+
if (!result.ok) {
|
|
401
|
+
throw new RegistryError(
|
|
402
|
+
`Could not fetch the shared manifest ${manifestPath} from ${result.url}: ${result.reason}`
|
|
403
|
+
);
|
|
404
|
+
}
|
|
405
|
+
let raw;
|
|
406
|
+
try {
|
|
407
|
+
raw = JSON.parse(result.content);
|
|
408
|
+
} catch {
|
|
409
|
+
throw new RegistryError(`The shared manifest ${manifestPath} is not valid JSON.`);
|
|
410
|
+
}
|
|
411
|
+
return parseSharedManifest(raw, manifestPath);
|
|
412
|
+
}
|
|
413
|
+
function resolveSharedPieces(names, manifest, labels) {
|
|
414
|
+
const resolved = [];
|
|
415
|
+
const visited = /* @__PURE__ */ new Set();
|
|
416
|
+
const visit = (name, requiredBy) => {
|
|
417
|
+
if (visited.has(name)) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (!Object.prototype.hasOwnProperty.call(manifest.pieces, name)) {
|
|
421
|
+
throw new RegistryError(
|
|
422
|
+
requiredBy === null ? `Shared piece "${name}", listed in ${labels.componentManifest}, is not defined in ${labels.sharedManifest}.` : `Shared piece "${requiredBy}" requires "${name}", which is not defined in ${labels.sharedManifest}.`
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
visited.add(name);
|
|
426
|
+
const piece = manifest.pieces[name];
|
|
427
|
+
for (const requirement of piece.requires) {
|
|
428
|
+
visit(requirement, name);
|
|
429
|
+
}
|
|
430
|
+
resolved.push({ name, ...piece });
|
|
431
|
+
};
|
|
432
|
+
for (const name of names) {
|
|
433
|
+
visit(name, null);
|
|
434
|
+
}
|
|
435
|
+
return resolved;
|
|
436
|
+
}
|
|
437
|
+
async function fetchSharedFile(source, platform, filePath) {
|
|
438
|
+
return fetchFile(source, `shared/${platform}`, filePath);
|
|
222
439
|
}
|
|
223
440
|
|
|
224
441
|
// src/utils/transform.ts
|
|
225
|
-
|
|
442
|
+
var SHARED_ALIAS_IMPORT = /\b((?:from|import|require)\s*\(?\s*)(['"])~shared\/([^'"/]+)\/([^'"]+)\2/g;
|
|
443
|
+
function findSharedAliasImports(content) {
|
|
444
|
+
return Array.from(content.matchAll(SHARED_ALIAS_IMPORT), (match) => ({
|
|
445
|
+
platform: match[3],
|
|
446
|
+
piece: match[4]
|
|
447
|
+
}));
|
|
448
|
+
}
|
|
449
|
+
function transformImports(content, config, options = {}) {
|
|
226
450
|
let transformed = content;
|
|
451
|
+
const { installedPath } = options;
|
|
452
|
+
if (installedPath !== void 0) {
|
|
453
|
+
const fromDir = posixDirname(installedPath);
|
|
454
|
+
transformed = transformed.replace(
|
|
455
|
+
SHARED_ALIAS_IMPORT,
|
|
456
|
+
(_match, lead, quote, _platform, piece) => `${lead}${quote}${relativeImportPath(fromDir, `shared/${piece}`)}${quote}`
|
|
457
|
+
);
|
|
458
|
+
}
|
|
227
459
|
transformed = transformed.replace(/from(\s+)(['"])\.\.\/files\//g, "from$1$2../components/");
|
|
228
460
|
transformed = transformed.replace(/import(\s+)(['"])\.\.\/files\//g, "import$1$2../components/");
|
|
229
461
|
transformed = transformed.replace(/import\((['"])\.\.\/files\//g, "import($1../components/");
|
|
@@ -232,11 +464,30 @@ function transformImports(content, config) {
|
|
|
232
464
|
/from\s+["']@sublay\/react-native["']/g,
|
|
233
465
|
'from "@sublay/expo"'
|
|
234
466
|
);
|
|
467
|
+
transformed = transformed.replace(
|
|
468
|
+
/^(\s*\*\s+-\s+)@sublay\/react-native(?=\s|$)/gm,
|
|
469
|
+
"$1@sublay/expo"
|
|
470
|
+
);
|
|
235
471
|
}
|
|
236
472
|
if (config.aliases["@/components"]) {
|
|
237
473
|
}
|
|
238
474
|
return transformed;
|
|
239
475
|
}
|
|
476
|
+
function posixDirname(filePath) {
|
|
477
|
+
const index = filePath.lastIndexOf("/");
|
|
478
|
+
return index === -1 ? "" : filePath.slice(0, index);
|
|
479
|
+
}
|
|
480
|
+
function relativeImportPath(fromDir, target) {
|
|
481
|
+
const from = fromDir.split("/").filter(Boolean);
|
|
482
|
+
const to = target.split("/").filter(Boolean);
|
|
483
|
+
let common = 0;
|
|
484
|
+
while (common < from.length && common < to.length - 1 && from[common] === to[common]) {
|
|
485
|
+
common++;
|
|
486
|
+
}
|
|
487
|
+
const up = from.length - common;
|
|
488
|
+
const down = to.slice(common).join("/");
|
|
489
|
+
return up === 0 ? `./${down}` : `${"../".repeat(up)}${down}`;
|
|
490
|
+
}
|
|
240
491
|
|
|
241
492
|
// src/utils/dependencies.ts
|
|
242
493
|
import fs4 from "fs-extra";
|
|
@@ -244,6 +495,7 @@ import path4 from "path";
|
|
|
244
495
|
import chalk2 from "chalk";
|
|
245
496
|
import prompts2 from "prompts";
|
|
246
497
|
import { execa } from "execa";
|
|
498
|
+
import semver from "semver";
|
|
247
499
|
async function installDependencies(deps) {
|
|
248
500
|
try {
|
|
249
501
|
console.log(chalk2.blue(`
|
|
@@ -280,6 +532,55 @@ function parseDependencyName(dep) {
|
|
|
280
532
|
}
|
|
281
533
|
return parts[0];
|
|
282
534
|
}
|
|
535
|
+
function parseDependencySpec(dep) {
|
|
536
|
+
const name = parseDependencyName(dep);
|
|
537
|
+
const rest = dep.slice(name.length);
|
|
538
|
+
return { name, range: rest.startsWith("@") ? rest.slice(1) : "" };
|
|
539
|
+
}
|
|
540
|
+
function mergeDependencies(componentDependencies, sharedDependencies) {
|
|
541
|
+
const merged = [];
|
|
542
|
+
const seen = /* @__PURE__ */ new Set();
|
|
543
|
+
for (const dep of [...componentDependencies, ...sharedDependencies]) {
|
|
544
|
+
const name = parseDependencyName(dep);
|
|
545
|
+
if (seen.has(name)) {
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
seen.add(name);
|
|
549
|
+
merged.push(dep);
|
|
550
|
+
}
|
|
551
|
+
return merged;
|
|
552
|
+
}
|
|
553
|
+
function isDeclaredRangeBelow(declared, required) {
|
|
554
|
+
const options = { includePrerelease: true };
|
|
555
|
+
if (typeof declared !== "string" || !required || !semver.validRange(declared, options) || !semver.validRange(required, options)) {
|
|
556
|
+
return false;
|
|
557
|
+
}
|
|
558
|
+
if (semver.intersects(declared, required, options)) {
|
|
559
|
+
return false;
|
|
560
|
+
}
|
|
561
|
+
const declaredMin = semver.minVersion(declared);
|
|
562
|
+
const requiredMin = semver.minVersion(required);
|
|
563
|
+
if (!declaredMin || !requiredMin) {
|
|
564
|
+
return false;
|
|
565
|
+
}
|
|
566
|
+
return semver.lt(declaredMin, requiredMin);
|
|
567
|
+
}
|
|
568
|
+
function findDependencyStatus(dependencies, declaredDependencies) {
|
|
569
|
+
const missing = [];
|
|
570
|
+
const upgrades = [];
|
|
571
|
+
for (const spec of dependencies) {
|
|
572
|
+
const { name, range } = parseDependencySpec(spec);
|
|
573
|
+
const declared = declaredDependencies[name];
|
|
574
|
+
if (!declared) {
|
|
575
|
+
missing.push(spec);
|
|
576
|
+
continue;
|
|
577
|
+
}
|
|
578
|
+
if (name.startsWith("@sublay/") && typeof declared === "string" && isDeclaredRangeBelow(declared, range)) {
|
|
579
|
+
upgrades.push({ spec, declared });
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return { missing, upgrades };
|
|
583
|
+
}
|
|
283
584
|
async function checkComponentDependencies(dependencies) {
|
|
284
585
|
try {
|
|
285
586
|
const packageJsonPath = path4.join(process.cwd(), "package.json");
|
|
@@ -292,17 +593,22 @@ async function checkComponentDependencies(dependencies) {
|
|
|
292
593
|
...packageJson2.dependencies,
|
|
293
594
|
...packageJson2.devDependencies
|
|
294
595
|
};
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
if (missingDeps.length === 0) {
|
|
596
|
+
const { missing, upgrades } = findDependencyStatus(dependencies, allDeps);
|
|
597
|
+
if (missing.length === 0 && upgrades.length === 0) {
|
|
298
598
|
console.log(chalk2.green("\n\u2705 All required dependencies are installed"));
|
|
299
599
|
return;
|
|
300
600
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
(dep) =>
|
|
304
|
-
|
|
305
|
-
|
|
601
|
+
if (missing.length > 0) {
|
|
602
|
+
console.log(chalk2.yellow("\n\u26A0\uFE0F Missing required dependencies:"));
|
|
603
|
+
missing.forEach((dep) => console.log(chalk2.dim(` - ${dep}`)));
|
|
604
|
+
}
|
|
605
|
+
if (upgrades.length > 0) {
|
|
606
|
+
console.log(chalk2.yellow("\n\u26A0\uFE0F Dependencies that need an upgrade:"));
|
|
607
|
+
upgrades.forEach(
|
|
608
|
+
({ spec, declared }) => console.log(chalk2.dim(` - ${spec} (your project has ${declared})`))
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
const toInstall = [...missing, ...upgrades.map((upgrade) => upgrade.spec)];
|
|
306
612
|
let install = false;
|
|
307
613
|
if (process.stdin.isTTY) {
|
|
308
614
|
const answer = await prompts2({
|
|
@@ -314,12 +620,12 @@ async function checkComponentDependencies(dependencies) {
|
|
|
314
620
|
install = Boolean(answer.install);
|
|
315
621
|
}
|
|
316
622
|
if (install) {
|
|
317
|
-
await installDependencies(
|
|
623
|
+
await installDependencies(toInstall);
|
|
318
624
|
} else {
|
|
319
625
|
console.log(chalk2.dim("\nYou can install them later with:"));
|
|
320
626
|
const packageManager = await detectPackageManager();
|
|
321
627
|
const installCmd = packageManager === "npm" ? "npm install" : packageManager === "yarn" ? "yarn add" : "pnpm add";
|
|
322
|
-
console.log(chalk2.dim(` ${installCmd} ${
|
|
628
|
+
console.log(chalk2.dim(` ${installCmd} ${toInstall.join(" ")}
|
|
323
629
|
`));
|
|
324
630
|
}
|
|
325
631
|
} catch (error) {
|
|
@@ -356,7 +662,7 @@ async function stripTypes(code, filePath) {
|
|
|
356
662
|
retainLines: true
|
|
357
663
|
// Try to keep line numbers similar
|
|
358
664
|
});
|
|
359
|
-
if (!result ||
|
|
665
|
+
if (!result || typeof result.code !== "string") {
|
|
360
666
|
throw new Error("Babel transformation returned no code");
|
|
361
667
|
}
|
|
362
668
|
return result.code;
|
|
@@ -373,9 +679,27 @@ function convertFileExtension(filePath) {
|
|
|
373
679
|
}
|
|
374
680
|
|
|
375
681
|
// src/commands/add.ts
|
|
376
|
-
|
|
682
|
+
var STRINGS_FILE = "utils/strings.ts";
|
|
683
|
+
var DEFAULT_LANGUAGE = "en";
|
|
684
|
+
var ENV_FILE = "utils/env.ts";
|
|
685
|
+
var FETCH_CONCURRENCY = 8;
|
|
686
|
+
async function add(componentName, options = {}) {
|
|
377
687
|
const spinner = ora("Initializing...").start();
|
|
378
688
|
try {
|
|
689
|
+
let source;
|
|
690
|
+
try {
|
|
691
|
+
const nameProblem = validateComponentName(componentName);
|
|
692
|
+
if (nameProblem) {
|
|
693
|
+
throw new RegistryError(nameProblem);
|
|
694
|
+
}
|
|
695
|
+
source = resolveRegistrySource(options.ref);
|
|
696
|
+
} catch (error) {
|
|
697
|
+
if (error instanceof RegistryError) {
|
|
698
|
+
spinner.fail(error.message);
|
|
699
|
+
process.exit(1);
|
|
700
|
+
}
|
|
701
|
+
throw error;
|
|
702
|
+
}
|
|
379
703
|
const configPath = path5.join(process.cwd(), "sublay.json");
|
|
380
704
|
if (!await fs5.pathExists(configPath)) {
|
|
381
705
|
spinner.fail("No sublay.json found");
|
|
@@ -411,38 +735,74 @@ async function add(componentName) {
|
|
|
411
735
|
spinner.start(`Fetching ${componentName}...`);
|
|
412
736
|
}
|
|
413
737
|
spinner.text = `Fetching ${componentName}...`;
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
)
|
|
738
|
+
source = await pinRegistrySource(source, componentName, config);
|
|
739
|
+
const registry = await fetchRegistry(componentName, config, source);
|
|
740
|
+
registry.dependencies = registry.dependencies.map(
|
|
741
|
+
(dep) => renameForPlatform(dep, config)
|
|
742
|
+
);
|
|
743
|
+
const languageFile = resolveLanguageFile(registry, options.lang, componentName);
|
|
744
|
+
const frameworkFile = resolveFrameworkFile(
|
|
745
|
+
registry,
|
|
746
|
+
await detectFramework(config.platform),
|
|
747
|
+
componentName
|
|
748
|
+
);
|
|
749
|
+
const platform = getRegistryPlatform(config);
|
|
750
|
+
const location = getComponentLocation(componentName, config);
|
|
751
|
+
const componentManifestLabel = `${location}/registry.json`;
|
|
752
|
+
const sharedManifestLabel = getSharedManifestPath(platform);
|
|
753
|
+
const sharedNames = readSharedNames(registry, componentManifestLabel);
|
|
754
|
+
let sharedPieces = [];
|
|
755
|
+
if (sharedNames.length > 0) {
|
|
756
|
+
const sharedManifest = await fetchSharedManifest(platform, source);
|
|
757
|
+
sharedPieces = resolveSharedPieces(sharedNames, sharedManifest, {
|
|
758
|
+
componentManifest: componentManifestLabel,
|
|
759
|
+
sharedManifest: sharedManifestLabel
|
|
760
|
+
});
|
|
423
761
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
762
|
+
const dependencies = mergeDependencies(
|
|
763
|
+
registry.dependencies,
|
|
764
|
+
sharedPieces.flatMap((piece) => piece.dependencies).map((dep) => renameForPlatform(dep, config))
|
|
765
|
+
);
|
|
766
|
+
spinner.text = sharedPieces.length > 0 ? `Downloading files and ${sharedPieces.length} shared piece(s)...` : "Downloading files...";
|
|
767
|
+
const installedPieces = new Set(sharedPieces.map((piece) => piece.name));
|
|
768
|
+
const downloads = [
|
|
769
|
+
...registry.files.map((file) => ({ registryPath: file.path, shared: false })),
|
|
770
|
+
...sharedPieces.flatMap(
|
|
771
|
+
(piece) => piece.files.map((file) => ({ registryPath: file, shared: true }))
|
|
772
|
+
)
|
|
773
|
+
].filter(
|
|
774
|
+
// Skip development files
|
|
775
|
+
(download) => !shouldExcludeFile(download.registryPath)
|
|
776
|
+
);
|
|
777
|
+
const pendingFiles = await mapWithConcurrency(
|
|
778
|
+
downloads,
|
|
779
|
+
FETCH_CONCURRENCY,
|
|
780
|
+
async ({ registryPath, shared }) => {
|
|
781
|
+
const fileContent = shared ? await fetchSharedFile(source, platform, registryPath) : await fetchFile(
|
|
782
|
+
source,
|
|
783
|
+
location,
|
|
784
|
+
languageFile && registryPath === STRINGS_FILE ? languageFile : frameworkFile && registryPath === ENV_FILE ? frameworkFile : registryPath
|
|
785
|
+
);
|
|
786
|
+
for (const used of findSharedAliasImports(fileContent)) {
|
|
787
|
+
if (used.platform !== platform || !installedPieces.has(used.piece)) {
|
|
788
|
+
throw new RegistryError(
|
|
789
|
+
`${registryPath} imports the shared piece "${used.platform}/${used.piece}", which ${componentManifestLabel} does not list under "shared" for ${sharedManifestLabel}.`
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return prepareFile(
|
|
794
|
+
shared ? `shared/${registryPath}` : registryPath,
|
|
795
|
+
fileContent,
|
|
796
|
+
config,
|
|
797
|
+
componentName
|
|
798
|
+
);
|
|
441
799
|
}
|
|
442
|
-
|
|
443
|
-
|
|
800
|
+
);
|
|
801
|
+
let filesInstalled = 0;
|
|
802
|
+
for (const file of pendingFiles) {
|
|
803
|
+
const fullPath = path5.join(process.cwd(), file.targetPath);
|
|
444
804
|
await fs5.ensureDir(path5.dirname(fullPath));
|
|
445
|
-
await fs5.writeFile(fullPath,
|
|
805
|
+
await fs5.writeFile(fullPath, file.content, "utf-8");
|
|
446
806
|
filesInstalled++;
|
|
447
807
|
}
|
|
448
808
|
spinner.text = "Creating index file...";
|
|
@@ -462,34 +822,138 @@ async function add(componentName) {
|
|
|
462
822
|
)}`
|
|
463
823
|
)
|
|
464
824
|
);
|
|
465
|
-
|
|
825
|
+
if (sharedPieces.length > 0) {
|
|
826
|
+
console.log(
|
|
827
|
+
chalk3.dim(
|
|
828
|
+
`\u{1F9E9} Shared pieces in ${path5.join(
|
|
829
|
+
config.paths.components,
|
|
830
|
+
componentName,
|
|
831
|
+
"shared"
|
|
832
|
+
)}: ${sharedPieces.map((piece) => piece.name).join(", ")}`
|
|
833
|
+
)
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
await checkComponentDependencies(dependencies);
|
|
466
837
|
const componentInfo = getComponentInfo(registry);
|
|
467
|
-
const
|
|
838
|
+
const relativeImportPath2 = path5.relative(
|
|
468
839
|
process.cwd(),
|
|
469
840
|
path5.join(config.paths.components, componentName)
|
|
470
841
|
);
|
|
471
842
|
console.log(chalk3.bold("\n\u{1F4D6} Usage:"));
|
|
472
843
|
console.log(
|
|
473
844
|
chalk3.dim(
|
|
474
|
-
` import { ${componentInfo.mainComponent} } from './${
|
|
845
|
+
` import { ${componentInfo.mainComponent} } from './${relativeImportPath2}';`
|
|
475
846
|
)
|
|
476
847
|
);
|
|
477
848
|
if (componentInfo.typeExport) {
|
|
478
849
|
console.log(chalk3.dim(` // With types:`));
|
|
479
850
|
console.log(
|
|
480
851
|
chalk3.dim(
|
|
481
|
-
` import { ${componentInfo.mainComponent}, type ${componentInfo.typeExport} } from './${
|
|
852
|
+
` import { ${componentInfo.mainComponent}, type ${componentInfo.typeExport} } from './${relativeImportPath2}';`
|
|
482
853
|
)
|
|
483
854
|
);
|
|
484
855
|
}
|
|
485
|
-
printAppSetupNotes(
|
|
856
|
+
printAppSetupNotes(dependencies);
|
|
486
857
|
console.log();
|
|
487
858
|
} catch (error) {
|
|
859
|
+
if (error instanceof RegistryError) {
|
|
860
|
+
spinner.fail(error.message);
|
|
861
|
+
console.error(chalk3.dim("\nNothing was written to your project."));
|
|
862
|
+
process.exit(1);
|
|
863
|
+
}
|
|
488
864
|
spinner.fail("Failed to add component");
|
|
489
865
|
console.error(chalk3.red("\n\u274C Error:"), error);
|
|
490
866
|
process.exit(1);
|
|
491
867
|
}
|
|
492
868
|
}
|
|
869
|
+
async function prepareFile(filePath, fileContent, config, componentName) {
|
|
870
|
+
const componentDir = path5.join(config.paths.components, componentName);
|
|
871
|
+
const needsTypeStrip = !config.typescript && (filePath.endsWith(".ts") || filePath.endsWith(".tsx"));
|
|
872
|
+
const finalFilePath = needsTypeStrip ? convertFileExtension(filePath) : filePath;
|
|
873
|
+
const targetPath = getTargetPath(finalFilePath, config, componentName);
|
|
874
|
+
const installedPath = path5.relative(componentDir, targetPath).split(path5.sep).join("/");
|
|
875
|
+
if (installedPath === ".." || installedPath.startsWith("../") || path5.isAbsolute(installedPath)) {
|
|
876
|
+
throw new RegistryError(
|
|
877
|
+
`${filePath} would be installed outside ${componentDir}; refusing to write it.`
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
let transformed = transformImports(fileContent, config, { installedPath });
|
|
881
|
+
if (needsTypeStrip) {
|
|
882
|
+
transformed = await stripTypes(transformed, filePath);
|
|
883
|
+
}
|
|
884
|
+
return { targetPath, content: transformed };
|
|
885
|
+
}
|
|
886
|
+
function renameForPlatform(dep, config) {
|
|
887
|
+
return config.platform === "expo" && dep.startsWith("@sublay/react-native") ? dep.replace("@sublay/react-native", "@sublay/expo") : dep;
|
|
888
|
+
}
|
|
889
|
+
function resolveLanguageFile(registry, lang, componentName) {
|
|
890
|
+
if (lang === void 0 || lang === DEFAULT_LANGUAGE) {
|
|
891
|
+
return null;
|
|
892
|
+
}
|
|
893
|
+
const languages = registry.languages && typeof registry.languages === "object" ? registry.languages : {};
|
|
894
|
+
const available = [DEFAULT_LANGUAGE, ...Object.keys(languages)].join(", ");
|
|
895
|
+
const languageFile = Object.prototype.hasOwnProperty.call(languages, lang) ? languages[lang] : void 0;
|
|
896
|
+
if (languageFile === void 0) {
|
|
897
|
+
throw new RegistryError(
|
|
898
|
+
`${componentName} is not available in "${lang}". Available languages: ${available}.`
|
|
899
|
+
);
|
|
900
|
+
}
|
|
901
|
+
if (typeof languageFile !== "string" || !/^[A-Za-z0-9_][A-Za-z0-9._/-]*$/.test(languageFile) || languageFile.split("/").includes("..")) {
|
|
902
|
+
throw new RegistryError(
|
|
903
|
+
`The "${lang}" entry under "languages" for ${componentName} is not a usable file path.`
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
if (!registry.files.some((file) => file.path === STRINGS_FILE)) {
|
|
907
|
+
throw new RegistryError(
|
|
908
|
+
`${componentName} lists languages but has no ${STRINGS_FILE} to put them in.`
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
return languageFile;
|
|
912
|
+
}
|
|
913
|
+
function resolveFrameworkFile(registry, framework, componentName) {
|
|
914
|
+
const frameworks = registry.frameworks && typeof registry.frameworks === "object" ? registry.frameworks : {};
|
|
915
|
+
const frameworkFile = Object.prototype.hasOwnProperty.call(frameworks, framework) ? frameworks[framework] : void 0;
|
|
916
|
+
if (frameworkFile === void 0) {
|
|
917
|
+
return null;
|
|
918
|
+
}
|
|
919
|
+
if (typeof frameworkFile !== "string" || !/^[A-Za-z0-9_][A-Za-z0-9._/-]*$/.test(frameworkFile) || frameworkFile.split("/").includes("..")) {
|
|
920
|
+
throw new RegistryError(
|
|
921
|
+
`The "${framework}" entry under "frameworks" for ${componentName} is not a usable file path.`
|
|
922
|
+
);
|
|
923
|
+
}
|
|
924
|
+
if (!registry.files.some((file) => file.path === ENV_FILE)) {
|
|
925
|
+
throw new RegistryError(
|
|
926
|
+
`${componentName} lists frameworks but has no ${ENV_FILE} to put them in.`
|
|
927
|
+
);
|
|
928
|
+
}
|
|
929
|
+
return frameworkFile;
|
|
930
|
+
}
|
|
931
|
+
function readSharedNames(registry, manifestLabel) {
|
|
932
|
+
const { shared } = registry;
|
|
933
|
+
if (shared === void 0 || shared === null) {
|
|
934
|
+
return [];
|
|
935
|
+
}
|
|
936
|
+
if (!Array.isArray(shared) || shared.some((name) => typeof name !== "string")) {
|
|
937
|
+
throw new RegistryError(
|
|
938
|
+
`"shared" in ${manifestLabel} must be a list of shared piece names.`
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
return shared;
|
|
942
|
+
}
|
|
943
|
+
async function mapWithConcurrency(items, limit, task) {
|
|
944
|
+
const results = new Array(items.length);
|
|
945
|
+
let next = 0;
|
|
946
|
+
const worker = async () => {
|
|
947
|
+
while (next < items.length) {
|
|
948
|
+
const index = next++;
|
|
949
|
+
results[index] = await task(items[index]);
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
await Promise.all(
|
|
953
|
+
Array.from({ length: Math.min(limit, items.length) }, worker)
|
|
954
|
+
);
|
|
955
|
+
return results;
|
|
956
|
+
}
|
|
493
957
|
function isValidConfig(value) {
|
|
494
958
|
if (typeof value !== "object" || value === null) {
|
|
495
959
|
return false;
|
|
@@ -565,6 +1029,9 @@ function getTargetPath(filePath, config, componentName) {
|
|
|
565
1029
|
if (filePath.startsWith("context/")) {
|
|
566
1030
|
return path5.join(componentDir, "context", path5.basename(filePath));
|
|
567
1031
|
}
|
|
1032
|
+
if (filePath.startsWith("shared/")) {
|
|
1033
|
+
return path5.join(componentDir, "shared", filePath.substring(7));
|
|
1034
|
+
}
|
|
568
1035
|
if (filePath.startsWith("files/")) {
|
|
569
1036
|
const normalizedPath = filePath.substring(6);
|
|
570
1037
|
return path5.join(componentDir, "components", normalizedPath);
|
|
@@ -622,5 +1089,13 @@ var packageJson = require2("../package.json");
|
|
|
622
1089
|
var program = new Command();
|
|
623
1090
|
program.name("sublay").description("CLI for installing Sublay UI components").version(packageJson.version);
|
|
624
1091
|
program.command("init").description("Initialize Sublay configuration in your project").action(init);
|
|
625
|
-
program.command("add <component>").description("Add a component to your project").
|
|
1092
|
+
program.command("add <component>").description("Add a component to your project").option(
|
|
1093
|
+
"--ref <branch>",
|
|
1094
|
+
"read the component from this branch of the registry instead of the default"
|
|
1095
|
+
).option(
|
|
1096
|
+
"--lang <code>",
|
|
1097
|
+
"install the component's text in this language instead of English (for example: he)"
|
|
1098
|
+
).action(
|
|
1099
|
+
(component, options) => add(component, { ref: options.ref, lang: options.lang })
|
|
1100
|
+
);
|
|
626
1101
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sublay/cli",
|
|
3
|
-
"version": "7.0
|
|
3
|
+
"version": "7.2.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Sublay, maintained by Yanay Tsabary",
|
|
@@ -43,13 +43,15 @@
|
|
|
43
43
|
"fs-extra": "^11.3.4",
|
|
44
44
|
"node-fetch": "^3.3.2",
|
|
45
45
|
"ora": "^9.3.0",
|
|
46
|
-
"prompts": "^2.4.2"
|
|
46
|
+
"prompts": "^2.4.2",
|
|
47
|
+
"semver": "^7.7.3"
|
|
47
48
|
},
|
|
48
49
|
"devDependencies": {
|
|
49
50
|
"@types/babel__core": "^7.20.5",
|
|
50
51
|
"@types/fs-extra": "^11.0.4",
|
|
51
52
|
"@types/node": "^25.9.2",
|
|
52
53
|
"@types/prompts": "^2.4.9",
|
|
54
|
+
"@types/semver": "^7.7.1",
|
|
53
55
|
"tsup": "^8.5.1",
|
|
54
56
|
"typescript": "^6.0.3",
|
|
55
57
|
"vitest": "^1.6.1"
|