jsrepo 1.0.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.
@@ -0,0 +1,495 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { cancel, confirm, isCancel, multiselect, outro, spinner } from '@clack/prompts';
4
+ import color from 'chalk';
5
+ import { Command, program } from 'commander';
6
+ import { execa } from 'execa';
7
+ import type { ResolvedCommand } from 'package-manager-detector';
8
+ import { resolveCommand } from 'package-manager-detector/commands';
9
+ import { detect } from 'package-manager-detector/detect';
10
+ import * as v from 'valibot';
11
+ import { context } from '..';
12
+ import { mapToArray } from '../blocks/utilities/map-to-array';
13
+ import { getConfig } from '../config';
14
+ import { type Block, isTestFile } from '../utils/build';
15
+ import { getInstalledBlocks } from '../utils/get-installed-blocks';
16
+ import { getWatermark } from '../utils/get-watermark';
17
+ import * as gitProviders from '../utils/git-providers';
18
+ import { INFO } from '../utils/index';
19
+ import { OUTPUT_FILE } from '../utils/index';
20
+ import { languages } from '../utils/language-support';
21
+ import { type Task, intro, nextSteps, runTasks } from '../utils/prompts';
22
+
23
+ const schema = v.object({
24
+ yes: v.boolean(),
25
+ verbose: v.boolean(),
26
+ repo: v.optional(v.string()),
27
+ allow: v.boolean(),
28
+ });
29
+
30
+ type Options = v.InferInput<typeof schema>;
31
+
32
+ const add = new Command('add')
33
+ .argument('[blocks...]', 'Whichever block you want to add to your project.')
34
+ .option('-y, --yes', 'Add and install any required dependencies.', false)
35
+ .option('-A, --allow', 'Allow jsrepo to download code from the provided repo.', false)
36
+ .option('--repo <repo>', 'Repository to download the blocks from')
37
+ .option('--verbose', 'Include debug logs.', false)
38
+ .action(async (blockNames, opts) => {
39
+ const options = v.parse(schema, opts);
40
+
41
+ await _add(blockNames, options);
42
+ });
43
+
44
+ type RemoteBlock = Block & { sourceRepo: gitProviders.Info };
45
+
46
+ const _add = async (blockNames: string[], options: Options) => {
47
+ intro(context.package.version);
48
+
49
+ const verbose = (msg: string) => {
50
+ if (options.verbose) {
51
+ console.info(`${INFO} ${msg}`);
52
+ }
53
+ };
54
+
55
+ verbose(`Attempting to add ${JSON.stringify(blockNames)}`);
56
+
57
+ const loading = spinner();
58
+
59
+ const config = getConfig().match(
60
+ (val) => val,
61
+ (err) => program.error(color.red(err))
62
+ );
63
+
64
+ const blocksMap: Map<string, RemoteBlock> = new Map();
65
+
66
+ let repoPaths = config.repos;
67
+
68
+ // we just want to override all others if supplied via the CLI
69
+ if (options.repo) repoPaths = [options.repo];
70
+
71
+ if (!options.allow && options.repo) {
72
+ const result = await confirm({
73
+ message: `Allow ${color.cyan('jsrepo')} to download and run code from ${color.cyan(options.repo)}?`,
74
+ initialValue: true,
75
+ });
76
+
77
+ if (isCancel(result) || !result) {
78
+ cancel('Canceled!');
79
+ process.exit(0);
80
+ }
81
+ }
82
+
83
+ verbose(`Fetching blocks from ${color.cyan(repoPaths.join(', '))}`);
84
+
85
+ if (!options.verbose) loading.start(`Fetching blocks from ${color.cyan(repoPaths.join(', '))}`);
86
+
87
+ for (const repo of repoPaths) {
88
+ const providerInfo: gitProviders.Info = (await gitProviders.getProviderInfo(repo)).match(
89
+ (info) => info,
90
+ (err) => {
91
+ loading.stop(`Failed fetching blocks from ${color.cyan(repo)}`);
92
+ program.error(color.red(err));
93
+ }
94
+ );
95
+
96
+ const manifestUrl = await providerInfo.provider.resolveRaw(providerInfo, OUTPUT_FILE);
97
+
98
+ verbose(`Got info for provider ${color.cyan(providerInfo.name)}`);
99
+
100
+ const categories = (await gitProviders.getManifest(manifestUrl)).match(
101
+ (val) => val,
102
+ (err) => {
103
+ loading.stop(`Failed fetching blocks from ${color.cyan(repo)}`);
104
+ program.error(color.red(err));
105
+ }
106
+ );
107
+
108
+ for (const category of categories) {
109
+ for (const block of category.blocks) {
110
+ blocksMap.set(
111
+ `${providerInfo.name}/${providerInfo.owner}/${providerInfo.repoName}/${category.name}/${block.name}`,
112
+ {
113
+ ...block,
114
+ sourceRepo: providerInfo,
115
+ }
116
+ );
117
+ }
118
+ }
119
+ }
120
+
121
+ verbose(`Retrieved blocks from ${color.cyan(repoPaths.join(', '))}`);
122
+
123
+ if (!options.verbose) loading.stop(`Retrieved blocks from ${color.cyan(repoPaths.join(', '))}`);
124
+
125
+ const installedBlocks = getInstalledBlocks(blocksMap, config).map((val) => val.specifier);
126
+
127
+ let installingBlockNames = blockNames;
128
+
129
+ if (installingBlockNames.length === 0) {
130
+ const promptResult = await multiselect({
131
+ message: 'Select which blocks to add.',
132
+ options: Array.from(blocksMap.entries()).map(([key, value]) => {
133
+ const shortName = `${value.category}/${value.name}`;
134
+
135
+ const blockExists =
136
+ installedBlocks.findIndex((block) => block === shortName) !== -1;
137
+
138
+ let label: string;
139
+
140
+ // show the full repo if there are multiple repos
141
+ if (repoPaths.length > 1) {
142
+ label = `${color.cyan(
143
+ `${value.sourceRepo.name}/${value.sourceRepo.owner}/${value.sourceRepo.repoName}/${value.category}`
144
+ )}/${value.name}`;
145
+ } else {
146
+ label = `${color.cyan(value.category)}/${value.name}`;
147
+ }
148
+
149
+ return {
150
+ label: blockExists ? color.gray(label) : label,
151
+ value: key,
152
+ // show hint for `Installed` if block is already installed
153
+ hint: blockExists ? 'Installed' : undefined,
154
+ };
155
+ }),
156
+ required: true,
157
+ });
158
+
159
+ if (isCancel(promptResult)) {
160
+ cancel('Canceled!');
161
+ process.exit(0);
162
+ }
163
+
164
+ installingBlockNames = promptResult as string[];
165
+ }
166
+
167
+ verbose(`Installing blocks ${color.cyan(installingBlockNames.join(', '))}`);
168
+
169
+ if (options.verbose) console.log('Blocks map: ', blocksMap);
170
+
171
+ const installingBlocks = await getBlocks(installingBlockNames, blocksMap, repoPaths);
172
+
173
+ const pm = (await detect({ cwd: process.cwd() }))?.agent ?? 'npm';
174
+
175
+ const tasks: Task[] = [];
176
+
177
+ const devDeps: Set<string> = new Set<string>();
178
+ const deps: Set<string> = new Set<string>();
179
+
180
+ for (const { name: specifier, block } of installingBlocks) {
181
+ const watermark = getWatermark(context.package.version, block.sourceRepo.url);
182
+
183
+ const providerInfo = block.sourceRepo;
184
+
185
+ verbose(`Attempting to add ${specifier}`);
186
+
187
+ const directory = path.join(config.path, block.category);
188
+
189
+ verbose(`Creating directory ${color.bold(directory)}`);
190
+
191
+ const blockExists =
192
+ (!block.subdirectory && fs.existsSync(path.join(directory, block.files[0]))) ||
193
+ (block.subdirectory && fs.existsSync(path.join(directory, block.name)));
194
+
195
+ if (blockExists && !options.yes) {
196
+ const result = await confirm({
197
+ message: `${color.bold(block.name)} already exists in your project would you like to overwrite it?`,
198
+ initialValue: false,
199
+ });
200
+
201
+ if (isCancel(result) || !result) {
202
+ cancel('Canceled!');
203
+ process.exit(0);
204
+ }
205
+ }
206
+
207
+ tasks.push({
208
+ loadingMessage: `Adding ${specifier}`,
209
+ completedMessage: `Added ${specifier}`,
210
+ run: async () => {
211
+ // in case the directory didn't already exist
212
+ fs.mkdirSync(directory, { recursive: true });
213
+
214
+ const files: { content: string; destPath: string }[] = [];
215
+
216
+ const getSourceFile = async (filePath: string) => {
217
+ const rawUrl = await providerInfo.provider.resolveRaw(providerInfo, filePath);
218
+
219
+ const response = await fetch(rawUrl);
220
+
221
+ if (!response.ok) {
222
+ loading.stop(color.red(`Error fetching ${color.bold(rawUrl.href)}`));
223
+ program.error(color.red(`There was an error trying to get ${specifier}`));
224
+ }
225
+
226
+ return await response.text();
227
+ };
228
+
229
+ for (const sourceFile of block.files) {
230
+ if (!config.includeTests && isTestFile(sourceFile)) continue;
231
+
232
+ const sourcePath = path.join(block.directory, sourceFile);
233
+
234
+ let destPath: string;
235
+ if (block.subdirectory) {
236
+ destPath = path.join(config.path, block.category, block.name, sourceFile);
237
+ } else {
238
+ destPath = path.join(config.path, block.category, sourceFile);
239
+ }
240
+
241
+ const content = await getSourceFile(sourcePath);
242
+
243
+ fs.mkdirSync(destPath.slice(0, destPath.length - sourceFile.length), {
244
+ recursive: true,
245
+ });
246
+
247
+ files.push({ content, destPath });
248
+ }
249
+
250
+ for (const file of files) {
251
+ let content: string = file.content;
252
+
253
+ if (config.watermark) {
254
+ const lang = languages.find((lang) => lang.matches(file.destPath));
255
+
256
+ if (lang) {
257
+ const comment = lang.comment(watermark);
258
+
259
+ content = `${comment}\n\n${content}`;
260
+ }
261
+ }
262
+
263
+ fs.writeFileSync(file.destPath, content);
264
+ }
265
+
266
+ if (config.includeTests) {
267
+ verbose('Trying to include tests');
268
+
269
+ const { devDependencies } = JSON.parse(
270
+ fs.readFileSync('package.json').toString()
271
+ );
272
+
273
+ if (devDependencies.vitest === undefined) {
274
+ devDeps.add('vitest');
275
+ }
276
+ }
277
+
278
+ for (const dep of block.devDependencies) {
279
+ devDeps.add(dep);
280
+ }
281
+
282
+ for (const dep of block.dependencies) {
283
+ deps.add(dep);
284
+ }
285
+ },
286
+ });
287
+ }
288
+
289
+ await runTasks(tasks, { verbose: options.verbose });
290
+
291
+ const installDependencies = async (deps: string[], dev: boolean) => {
292
+ if (!options.verbose) loading.start(`Installing dependencies with ${color.cyan(pm)}`);
293
+
294
+ let add: ResolvedCommand | null;
295
+ if (dev) {
296
+ add = resolveCommand(pm, 'install', [...deps, '-D']);
297
+ } else {
298
+ add = resolveCommand(pm, 'install', [...deps]);
299
+ }
300
+
301
+ if (add == null) {
302
+ program.error(color.red(`Could not resolve add command for '${pm}'.`));
303
+ }
304
+
305
+ try {
306
+ await execa(add.command, [...add.args], { cwd: process.cwd() });
307
+ } catch {
308
+ program.error(
309
+ color.red(
310
+ `Failed to install ${color.bold('vitest')}! Failed while running '${color.bold(
311
+ `${add.command} ${add.args.join(' ')}`
312
+ )}'`
313
+ )
314
+ );
315
+ }
316
+
317
+ if (!options.verbose) loading.stop(`Installed ${color.cyan(deps.join(', '))}`);
318
+ };
319
+
320
+ const hasDependencies = deps.size > 0 || devDeps.size > 0;
321
+
322
+ if (hasDependencies) {
323
+ let install = options.yes;
324
+ if (!options.yes) {
325
+ const result = await confirm({
326
+ message: 'Would you like to install dependencies?',
327
+ initialValue: true,
328
+ });
329
+
330
+ if (isCancel(result)) {
331
+ cancel('Canceled!');
332
+ process.exit(0);
333
+ }
334
+
335
+ install = result;
336
+ }
337
+
338
+ if (install) {
339
+ if (deps.size > 0) {
340
+ await installDependencies(Array.from(deps), false);
341
+ }
342
+
343
+ if (devDeps.size > 0) {
344
+ await installDependencies(Array.from(devDeps), true);
345
+ }
346
+ }
347
+
348
+ // next steps if they didn't install dependencies
349
+ let steps = [];
350
+
351
+ if (!install) {
352
+ if (deps.size > 0) {
353
+ const cmd = resolveCommand(pm, 'install', [...deps]);
354
+
355
+ steps.push(
356
+ `Install dependencies \`${color.cyan(`${cmd?.command} ${cmd?.args.join(' ')}`)}\``
357
+ );
358
+ }
359
+
360
+ if (devDeps.size > 0) {
361
+ const cmd = resolveCommand(pm, 'install', [...devDeps, '-D']);
362
+
363
+ steps.push(
364
+ `Install dev dependencies \`${color.cyan(`${cmd?.command} ${cmd?.args.join(' ')}`)}\``
365
+ );
366
+ }
367
+ }
368
+
369
+ // put steps with numbers above here
370
+ steps = steps.map((step, i) => `${i + 1}. ${step}`);
371
+
372
+ if (!install) {
373
+ steps.push('');
374
+ }
375
+
376
+ steps.push(`Import the blocks from \`${color.cyan(config.path)}\``);
377
+
378
+ const next = nextSteps(steps);
379
+
380
+ process.stdout.write(next);
381
+ }
382
+
383
+ outro(color.green('All done!'));
384
+ };
385
+
386
+ type InstallingBlock = {
387
+ name: string;
388
+ subDependency: boolean;
389
+ block: RemoteBlock;
390
+ };
391
+
392
+ const getBlocks = async (
393
+ blockSpecifiers: string[],
394
+ blocksMap: Map<string, RemoteBlock>,
395
+ repoPaths: string[]
396
+ ): Promise<InstallingBlock[]> => {
397
+ const blocks = new Map<string, InstallingBlock>();
398
+
399
+ for (const blockSpecifier of blockSpecifiers) {
400
+ let block: RemoteBlock | undefined = undefined;
401
+
402
+ // if the block starts with github (or another provider) we know it has been resolved
403
+ if (!blockSpecifier.startsWith('github')) {
404
+ if (repoPaths.length === 0) {
405
+ program.error(
406
+ color.red(
407
+ `If your config doesn't repos then you must provide the repo in the block specifier ex: \`${color.bold(
408
+ `github/<owner>/<name>/${blockSpecifier}`
409
+ )}\`!`
410
+ )
411
+ );
412
+ }
413
+
414
+ for (const repo of repoPaths) {
415
+ // we unwrap because we already checked this
416
+ const providerInfo = (await gitProviders.getProviderInfo(repo)).unwrap();
417
+
418
+ const tempBlock = blocksMap.get(
419
+ `${providerInfo.name}/${providerInfo.owner}/${providerInfo.repoName}/${blockSpecifier}`
420
+ );
421
+
422
+ if (tempBlock === undefined) continue;
423
+
424
+ block = tempBlock;
425
+
426
+ break;
427
+ }
428
+ } else {
429
+ if (repoPaths.length === 0) {
430
+ const [providerName, owner, repoName, ...rest] = blockSpecifier.split('/');
431
+
432
+ let repo: string;
433
+ // if rest is greater than 2 it isn't the block specifier so it is part of the path
434
+ if (rest.length > 2) {
435
+ repo = `${providerName}/${owner}/${repoName}/${rest.join('/')}`;
436
+ } else {
437
+ repo = `${providerName}/${owner}/${repoName}`;
438
+ }
439
+
440
+ const providerInfo = (await gitProviders.getProviderInfo(repo)).match(
441
+ (val) => val,
442
+ (err) => program.error(color.red(err))
443
+ );
444
+
445
+ const manifestUrl = await providerInfo.provider.resolveRaw(
446
+ providerInfo,
447
+ OUTPUT_FILE
448
+ );
449
+
450
+ const categories = (await gitProviders.getManifest(manifestUrl)).match(
451
+ (val) => val,
452
+ (err) => program.error(color.red(err))
453
+ );
454
+
455
+ for (const category of categories) {
456
+ for (const block of category.blocks) {
457
+ blocksMap.set(
458
+ `${providerInfo.name}/${providerInfo.owner}/${providerInfo.repoName}/${category.name}/${block.name}`,
459
+ {
460
+ ...block,
461
+ sourceRepo: providerInfo,
462
+ }
463
+ );
464
+ }
465
+ }
466
+ }
467
+
468
+ block = blocksMap.get(blockSpecifier);
469
+ }
470
+
471
+ if (!block) {
472
+ program.error(
473
+ color.red(`Invalid block! ${color.bold(blockSpecifier)} does not exist!`)
474
+ );
475
+ }
476
+
477
+ blocks.set(blockSpecifier, { name: blockSpecifier, subDependency: false, block });
478
+
479
+ if (block.localDependencies && block.localDependencies.length > 0) {
480
+ const subDeps = await getBlocks(
481
+ block.localDependencies.filter((dep) => blocks.has(dep)),
482
+ blocksMap,
483
+ repoPaths
484
+ );
485
+
486
+ for (const dep of subDeps) {
487
+ blocks.set(dep.name, dep);
488
+ }
489
+ }
490
+ }
491
+
492
+ return mapToArray(blocks, (_, val) => val);
493
+ };
494
+
495
+ export { add };
@@ -0,0 +1,79 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { outro, spinner } from '@clack/prompts';
4
+ import color from 'chalk';
5
+ import { Command } from 'commander';
6
+ import * as v from 'valibot';
7
+ import { context } from '..';
8
+ import { OUTPUT_FILE } from '../utils';
9
+ import { type Category, buildBlocksDirectory } from '../utils/build';
10
+ import { intro } from '../utils/prompts';
11
+
12
+ const schema = v.object({
13
+ verbose: v.boolean(),
14
+ output: v.boolean(),
15
+ dirs: v.array(v.string()),
16
+ cwd: v.string(),
17
+ });
18
+
19
+ type Options = v.InferInput<typeof schema>;
20
+
21
+ const build = new Command('build')
22
+ .description(`Builds the provided --dirs in the project root into a \`${OUTPUT_FILE}\` file.`)
23
+ .option('--dirs [dirs...]', 'The directories containing the blocks.', ['./blocks'])
24
+ .option('--no-output', `Do not output a \`${OUTPUT_FILE}\` file.`)
25
+ .option('--verbose', 'Include debug logs.', false)
26
+ .option('--cwd <cwd>', 'The current working directory', process.cwd())
27
+ .action(async (opts) => {
28
+ const options = v.parse(schema, opts);
29
+
30
+ await _build(options);
31
+ });
32
+
33
+ const _build = async (options: Options) => {
34
+ intro(context.package.version);
35
+
36
+ const loading = spinner();
37
+
38
+ const categories: Category[] = [];
39
+
40
+ const outFile = path.join(options.cwd, OUTPUT_FILE);
41
+
42
+ for (const dir of options.dirs) {
43
+ const dirPath = path.join(options.cwd, dir);
44
+
45
+ loading.start(`Building ${color.cyan(dirPath)}`);
46
+
47
+ if (options.output && fs.existsSync(outFile)) fs.rmSync(outFile);
48
+
49
+ categories.push(...buildBlocksDirectory(dirPath, options.cwd));
50
+
51
+ loading.stop(`Built ${color.cyan(dirPath)}`);
52
+ }
53
+
54
+ const categoriesMap = new Map<string, Category>();
55
+
56
+ for (const category of categories) {
57
+ const cat = categoriesMap.get(category.name);
58
+
59
+ if (!cat) {
60
+ categoriesMap.set(category.name, category);
61
+ continue;
62
+ }
63
+
64
+ // we aren't going to merge blocks hopefully people are smart enough not to overlap names
65
+ cat.blocks = [...cat.blocks, ...category.blocks];
66
+
67
+ categoriesMap.set(cat.name, cat);
68
+ }
69
+
70
+ if (options.output) {
71
+ fs.writeFileSync(outFile, JSON.stringify(categories, null, '\t'));
72
+ } else {
73
+ loading.stop('Built successfully!');
74
+ }
75
+
76
+ outro(color.green('All done!'));
77
+ };
78
+
79
+ export { build };