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.
- package/LICENSE +21 -0
- package/README.md +5 -0
- package/bin.mjs +2 -0
- package/dist/index.js +2179 -0
- package/package.json +70 -0
- package/pnpm-lock.yaml +3185 -0
- package/schema.json +28 -0
- package/src/blocks/types/result.ts +736 -0
- package/src/blocks/utilities/array-sum.ts +31 -0
- package/src/blocks/utilities/lines.ts +73 -0
- package/src/blocks/utilities/map-to-array.ts +32 -0
- package/src/blocks/utilities/pad.ts +85 -0
- package/src/blocks/utilities/strip-ansi.ts +25 -0
- package/src/commands/add.ts +495 -0
- package/src/commands/build.ts +79 -0
- package/src/commands/diff.ts +218 -0
- package/src/commands/index.ts +7 -0
- package/src/commands/init.ts +117 -0
- package/src/commands/test.ts +369 -0
- package/src/config/index.ts +31 -0
- package/src/index.ts +40 -0
- package/src/utils/build.ts +189 -0
- package/src/utils/context.ts +19 -0
- package/src/utils/diff.ts +184 -0
- package/src/utils/get-installed-blocks.ts +38 -0
- package/src/utils/get-watermark.ts +7 -0
- package/src/utils/git-providers.ts +140 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/language-support.ts +198 -0
- package/src/utils/package.ts +16 -0
- package/src/utils/prompts.ts +72 -0
|
@@ -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 };
|