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,31 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import * as v from 'valibot';
|
|
3
|
+
import { Err, Ok, type Result } from '../blocks/types/result';
|
|
4
|
+
|
|
5
|
+
const CONFIG_NAME = 'blocks.json';
|
|
6
|
+
|
|
7
|
+
const schema = v.object({
|
|
8
|
+
$schema: v.string(),
|
|
9
|
+
repos: v.optional(v.array(v.string()), []),
|
|
10
|
+
includeTests: v.boolean(),
|
|
11
|
+
path: v.pipe(v.string(), v.minLength(1)),
|
|
12
|
+
watermark: v.optional(v.boolean(), true),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const getConfig = (): Result<Config, string> => {
|
|
16
|
+
if (!fs.existsSync(CONFIG_NAME)) {
|
|
17
|
+
return Err('Could not find your configuration file! Please run `npx jsrepo init`.');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const config = v.safeParse(schema, JSON.parse(fs.readFileSync(CONFIG_NAME).toString()));
|
|
21
|
+
|
|
22
|
+
if (!config.success) {
|
|
23
|
+
return Err('There was an error reading your `blocks.json` file!');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return Ok(config.output);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type Config = v.InferOutput<typeof schema>;
|
|
30
|
+
|
|
31
|
+
export { type Config, CONFIG_NAME, getConfig, schema };
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { program } from 'commander';
|
|
5
|
+
import * as commands from './commands';
|
|
6
|
+
import type { CLIContext } from './utils/context';
|
|
7
|
+
|
|
8
|
+
const resolveRelativeToRoot = (p: string): string => {
|
|
9
|
+
const dirname = fileURLToPath(import.meta.url);
|
|
10
|
+
return path.join(dirname, '../..', p);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// get version from package.json
|
|
14
|
+
const { version, name, description, repository } = JSON.parse(
|
|
15
|
+
fs.readFileSync(resolveRelativeToRoot('package.json'), 'utf-8')
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const context: CLIContext = {
|
|
19
|
+
package: {
|
|
20
|
+
name,
|
|
21
|
+
description,
|
|
22
|
+
version,
|
|
23
|
+
repository,
|
|
24
|
+
},
|
|
25
|
+
resolveRelativeToRoot,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
program
|
|
29
|
+
.name(name)
|
|
30
|
+
.description(description)
|
|
31
|
+
.version(version)
|
|
32
|
+
.addCommand(commands.add)
|
|
33
|
+
.addCommand(commands.init)
|
|
34
|
+
.addCommand(commands.test)
|
|
35
|
+
.addCommand(commands.build)
|
|
36
|
+
.addCommand(commands.diff);
|
|
37
|
+
|
|
38
|
+
program.parse();
|
|
39
|
+
|
|
40
|
+
export { context };
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import color from 'chalk';
|
|
4
|
+
import { program } from 'commander';
|
|
5
|
+
import * as v from 'valibot';
|
|
6
|
+
import { WARN } from '.';
|
|
7
|
+
import { languages } from './language-support';
|
|
8
|
+
|
|
9
|
+
export const blockSchema = v.object({
|
|
10
|
+
name: v.string(),
|
|
11
|
+
category: v.string(),
|
|
12
|
+
localDependencies: v.array(v.string()),
|
|
13
|
+
dependencies: v.array(v.string()),
|
|
14
|
+
devDependencies: v.array(v.string()),
|
|
15
|
+
tests: v.boolean(),
|
|
16
|
+
/** Where to find the block relative to root */
|
|
17
|
+
directory: v.string(),
|
|
18
|
+
subdirectory: v.boolean(),
|
|
19
|
+
files: v.array(v.string()),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const categorySchema = v.object({
|
|
23
|
+
name: v.string(),
|
|
24
|
+
blocks: v.array(blockSchema),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export type Category = v.InferInput<typeof categorySchema>;
|
|
28
|
+
|
|
29
|
+
export type Block = v.InferInput<typeof blockSchema>;
|
|
30
|
+
|
|
31
|
+
const TEST_SUFFIXES = ['.test.ts', '_test.ts', '.test.js', '_test.js'] as const;
|
|
32
|
+
|
|
33
|
+
const isTestFile = (file: string): boolean =>
|
|
34
|
+
TEST_SUFFIXES.find((suffix) => file.endsWith(suffix)) !== undefined;
|
|
35
|
+
|
|
36
|
+
/** Using the provided path to the blocks folder builds the blocks into categories and also resolves dependencies
|
|
37
|
+
*
|
|
38
|
+
* @param blocksPath
|
|
39
|
+
* @returns
|
|
40
|
+
*/
|
|
41
|
+
const buildBlocksDirectory = (blocksPath: string, cwd: string): Category[] => {
|
|
42
|
+
let paths: string[];
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
paths = fs.readdirSync(blocksPath);
|
|
46
|
+
} catch {
|
|
47
|
+
program.error(color.red(`Couldn't read the ${color.bold(blocksPath)} directory.`));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const categories: Category[] = [];
|
|
51
|
+
|
|
52
|
+
for (const categoryPath of paths) {
|
|
53
|
+
const categoryDir = path.join(blocksPath, categoryPath);
|
|
54
|
+
|
|
55
|
+
if (fs.statSync(categoryDir).isFile()) continue;
|
|
56
|
+
|
|
57
|
+
const categoryName = path.basename(categoryPath);
|
|
58
|
+
|
|
59
|
+
const category: Category = {
|
|
60
|
+
name: categoryName,
|
|
61
|
+
blocks: [],
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const files = fs.readdirSync(categoryDir);
|
|
65
|
+
|
|
66
|
+
for (const file of files) {
|
|
67
|
+
const blockDir = path.join(categoryDir, file);
|
|
68
|
+
|
|
69
|
+
if (fs.statSync(blockDir).isFile()) {
|
|
70
|
+
if (isTestFile(file)) continue;
|
|
71
|
+
|
|
72
|
+
const lang = languages.find((resolver) => resolver.matches(file));
|
|
73
|
+
|
|
74
|
+
if (!lang) {
|
|
75
|
+
console.warn(
|
|
76
|
+
`${WARN} Skipped \`${color.bold(blockDir)}\` \`${color.bold(
|
|
77
|
+
path.parse(file).ext
|
|
78
|
+
)}\` files are not currently supported!`
|
|
79
|
+
);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const name = path.parse(path.basename(file)).name;
|
|
84
|
+
|
|
85
|
+
// tries to find a test file with the same name as the file
|
|
86
|
+
const testsPath = files.find((f) =>
|
|
87
|
+
TEST_SUFFIXES.find((suffix) => f === `${name}${suffix}`)
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const { dependencies, devDependencies, local } = lang
|
|
91
|
+
.resolveDependencies(blockDir, categoryName, false)
|
|
92
|
+
.match(
|
|
93
|
+
(val) => val,
|
|
94
|
+
(err) => {
|
|
95
|
+
program.error(color.red(err));
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const block: Block = {
|
|
100
|
+
name,
|
|
101
|
+
directory: path.relative(cwd, categoryDir),
|
|
102
|
+
category: categoryName,
|
|
103
|
+
tests: testsPath !== undefined,
|
|
104
|
+
subdirectory: false,
|
|
105
|
+
files: [file],
|
|
106
|
+
localDependencies: local,
|
|
107
|
+
dependencies,
|
|
108
|
+
devDependencies,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
if (testsPath !== undefined) {
|
|
112
|
+
block.files.push(testsPath);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
category.blocks.push(block);
|
|
116
|
+
} else {
|
|
117
|
+
const blockName = file;
|
|
118
|
+
|
|
119
|
+
const blockFiles = fs.readdirSync(blockDir);
|
|
120
|
+
|
|
121
|
+
const hasTests = blockFiles.findIndex((f) => isTestFile(f)) !== -1;
|
|
122
|
+
|
|
123
|
+
const localDepsSet = new Set<string>();
|
|
124
|
+
const depsSet = new Set<string>();
|
|
125
|
+
const devDepsSet = new Set<string>();
|
|
126
|
+
|
|
127
|
+
// if it is a directory
|
|
128
|
+
for (const f of blockFiles) {
|
|
129
|
+
if (isTestFile(f)) continue;
|
|
130
|
+
|
|
131
|
+
const lang = languages.find((resolver) => resolver.matches(f));
|
|
132
|
+
|
|
133
|
+
if (!lang) {
|
|
134
|
+
console.warn(
|
|
135
|
+
`${WARN} Skipped \`${color.bold(path.join(blockDir, f))}\` \`${color.bold(
|
|
136
|
+
path.parse(file).ext
|
|
137
|
+
)}\` files are not currently supported!`
|
|
138
|
+
);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const { local, dependencies, devDependencies } = lang
|
|
143
|
+
.resolveDependencies(path.join(blockDir, f), categoryName, true)
|
|
144
|
+
.match(
|
|
145
|
+
(val) => val,
|
|
146
|
+
(err) => {
|
|
147
|
+
program.error(color.red(err));
|
|
148
|
+
}
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
for (const dep of local) {
|
|
152
|
+
localDepsSet.add(dep);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for (const dep of dependencies) {
|
|
156
|
+
depsSet.add(dep);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
for (const dep of devDependencies) {
|
|
160
|
+
devDepsSet.add(dep);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const block: Block = {
|
|
165
|
+
name: blockName,
|
|
166
|
+
directory: path.relative(cwd, blockDir),
|
|
167
|
+
category: categoryName,
|
|
168
|
+
tests: hasTests,
|
|
169
|
+
subdirectory: true,
|
|
170
|
+
files: [...blockFiles],
|
|
171
|
+
localDependencies: Array.from(localDepsSet.keys()),
|
|
172
|
+
dependencies: Array.from(depsSet.keys()),
|
|
173
|
+
devDependencies: Array.from(devDepsSet.keys()),
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
category.blocks.push(block);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
categories.push(category);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return categories;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const readCategories = (outputFilePath: string): Category[] =>
|
|
187
|
+
v.parse(v.array(categorySchema), JSON.parse(fs.readFileSync(outputFilePath).toString()));
|
|
188
|
+
|
|
189
|
+
export { buildBlocksDirectory, readCategories, isTestFile };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Block, Category } from './build';
|
|
2
|
+
|
|
3
|
+
export interface CLIContext {
|
|
4
|
+
/** The package.json of the CLI */
|
|
5
|
+
package: {
|
|
6
|
+
name: string;
|
|
7
|
+
version: string;
|
|
8
|
+
description: string;
|
|
9
|
+
repository: {
|
|
10
|
+
url: string;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
/** Resolves the path relative to the root of the application
|
|
14
|
+
*
|
|
15
|
+
* @param path
|
|
16
|
+
* @returns
|
|
17
|
+
*/
|
|
18
|
+
resolveRelativeToRoot: (path: string) => string;
|
|
19
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import color from 'chalk';
|
|
2
|
+
import { type Change, diffChars } from 'diff';
|
|
3
|
+
import { arraySum } from '../blocks/utilities/array-sum';
|
|
4
|
+
import * as lines from '../blocks/utilities/lines';
|
|
5
|
+
import { leftPadMin } from '../blocks/utilities/pad';
|
|
6
|
+
|
|
7
|
+
type Options = {
|
|
8
|
+
/** The source file */
|
|
9
|
+
from: string;
|
|
10
|
+
/** The destination file */
|
|
11
|
+
to: string;
|
|
12
|
+
/** The changes to the file */
|
|
13
|
+
changes: Change[];
|
|
14
|
+
/** Expands all lines to show the entire file */
|
|
15
|
+
expand: boolean;
|
|
16
|
+
/** Maximum lines to show before collapsing */
|
|
17
|
+
maxUnchanged: number;
|
|
18
|
+
/** Color the removed lines */
|
|
19
|
+
colorRemoved?: (line: string) => string;
|
|
20
|
+
/** Color the added lines */
|
|
21
|
+
colorAdded?: (line: string) => string;
|
|
22
|
+
/** Prefixes each line with the string returned from this function. */
|
|
23
|
+
prefix: () => string;
|
|
24
|
+
intro: (options: Options) => string;
|
|
25
|
+
onUnchanged: (options: Options) => string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const formatDiff = ({
|
|
29
|
+
from,
|
|
30
|
+
to,
|
|
31
|
+
changes,
|
|
32
|
+
expand = false,
|
|
33
|
+
maxUnchanged = 5,
|
|
34
|
+
colorRemoved = color.red,
|
|
35
|
+
colorAdded = color.green,
|
|
36
|
+
prefix,
|
|
37
|
+
onUnchanged,
|
|
38
|
+
intro,
|
|
39
|
+
}: Options): string => {
|
|
40
|
+
let result = '';
|
|
41
|
+
|
|
42
|
+
const length = arraySum(changes, (change) => change.count ?? 0).toString().length + 1;
|
|
43
|
+
|
|
44
|
+
let lineOffset = 0;
|
|
45
|
+
|
|
46
|
+
if (changes.length === 1 && !changes[0].added && !changes[0].removed) {
|
|
47
|
+
return onUnchanged({
|
|
48
|
+
from,
|
|
49
|
+
to,
|
|
50
|
+
changes,
|
|
51
|
+
expand,
|
|
52
|
+
maxUnchanged,
|
|
53
|
+
colorAdded,
|
|
54
|
+
colorRemoved,
|
|
55
|
+
prefix,
|
|
56
|
+
onUnchanged,
|
|
57
|
+
intro,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
result += intro({
|
|
62
|
+
from,
|
|
63
|
+
to,
|
|
64
|
+
changes,
|
|
65
|
+
expand,
|
|
66
|
+
maxUnchanged,
|
|
67
|
+
colorAdded,
|
|
68
|
+
colorRemoved,
|
|
69
|
+
prefix,
|
|
70
|
+
onUnchanged,
|
|
71
|
+
intro,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
/** Provides the line number prefix */
|
|
75
|
+
const linePrefix = (line: number): string =>
|
|
76
|
+
color.gray(`${prefix?.() ?? ''}${leftPadMin(`${line + 1 + lineOffset} `, length)} `);
|
|
77
|
+
|
|
78
|
+
for (let i = 0; i < changes.length; i++) {
|
|
79
|
+
const change = changes[i];
|
|
80
|
+
|
|
81
|
+
const hasPreviousChange = changes[i - 1]?.added || changes[i - 1]?.removed;
|
|
82
|
+
const hasNextChange = changes[i + 1]?.added || changes[i + 1]?.removed;
|
|
83
|
+
|
|
84
|
+
if (!change.added && !change.removed) {
|
|
85
|
+
// show collapsed
|
|
86
|
+
if (!expand && change.count !== undefined && change.count > maxUnchanged) {
|
|
87
|
+
const prevLineOffset = lineOffset;
|
|
88
|
+
const ls = lines.get(change.value.trimEnd());
|
|
89
|
+
|
|
90
|
+
let shownLines = 0;
|
|
91
|
+
|
|
92
|
+
if (hasNextChange) shownLines += maxUnchanged;
|
|
93
|
+
if (hasPreviousChange) shownLines += maxUnchanged;
|
|
94
|
+
|
|
95
|
+
// just show all if we are going to show more than we have
|
|
96
|
+
if (shownLines >= ls.length) {
|
|
97
|
+
result += `${lines.join(ls, {
|
|
98
|
+
prefix: linePrefix,
|
|
99
|
+
})}\n`;
|
|
100
|
+
lineOffset += ls.length;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// this writes the top few lines
|
|
105
|
+
if (hasPreviousChange) {
|
|
106
|
+
result += `${lines.join(ls.slice(0, maxUnchanged), {
|
|
107
|
+
prefix: linePrefix,
|
|
108
|
+
})}\n`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (ls.length > shownLines) {
|
|
112
|
+
const count = ls.length - shownLines;
|
|
113
|
+
result += `${lines.join(
|
|
114
|
+
lines.get(
|
|
115
|
+
color.gray(
|
|
116
|
+
`+ ${count} more unchanged (${color.italic('-E to expand')})`
|
|
117
|
+
)
|
|
118
|
+
),
|
|
119
|
+
{
|
|
120
|
+
prefix: () => `${prefix?.() ?? ''}${leftPadMin(' ', length)} `,
|
|
121
|
+
}
|
|
122
|
+
)}\n`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (hasNextChange) {
|
|
126
|
+
lineOffset = lineOffset + ls.length - maxUnchanged;
|
|
127
|
+
result += `${lines.join(ls.slice(ls.length - maxUnchanged), {
|
|
128
|
+
prefix: linePrefix,
|
|
129
|
+
})}\n`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// resets the line offset
|
|
133
|
+
lineOffset = prevLineOffset + change.count;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// show expanded
|
|
138
|
+
|
|
139
|
+
result += `${lines.join(lines.get(change.value.trimEnd()), {
|
|
140
|
+
prefix: linePrefix,
|
|
141
|
+
})}\n`;
|
|
142
|
+
lineOffset += change.count ?? 0;
|
|
143
|
+
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const colorChange = (change: Change) => {
|
|
148
|
+
if (change.added) {
|
|
149
|
+
return colorAdded(change.value.trimEnd());
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (change.removed) {
|
|
153
|
+
return colorRemoved(change.value.trimEnd());
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return change.value;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
if (change.removed && change.count === 1) {
|
|
160
|
+
// single line change
|
|
161
|
+
const diffedChars = diffChars(change.value, changes[i + 1].value);
|
|
162
|
+
|
|
163
|
+
const sentence = diffedChars.map((chg) => colorChange(chg)).join('');
|
|
164
|
+
|
|
165
|
+
result += `${linePrefix(0)}${sentence}`;
|
|
166
|
+
|
|
167
|
+
lineOffset += 1;
|
|
168
|
+
|
|
169
|
+
i++;
|
|
170
|
+
} else {
|
|
171
|
+
result += `${lines.join(lines.get(colorChange(change)), {
|
|
172
|
+
prefix: linePrefix,
|
|
173
|
+
})}\n`;
|
|
174
|
+
|
|
175
|
+
if (!change.removed) {
|
|
176
|
+
lineOffset += change.count ?? 0;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return result;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
export { formatDiff };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import type { Config } from '../config';
|
|
4
|
+
import type { Block } from './build';
|
|
5
|
+
|
|
6
|
+
type InstalledBlock = {
|
|
7
|
+
specifier: `${string}/${string}`;
|
|
8
|
+
path: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/** Finds installed blocks and returns them as `<category>/<name>`
|
|
12
|
+
*
|
|
13
|
+
* @param blocks
|
|
14
|
+
* @param config
|
|
15
|
+
* @returns
|
|
16
|
+
*/
|
|
17
|
+
const getInstalledBlocks = (blocks: Map<string, Block>, config: Config): InstalledBlock[] => {
|
|
18
|
+
const installedBlocks: InstalledBlock[] = [];
|
|
19
|
+
|
|
20
|
+
for (const [_, block] of blocks) {
|
|
21
|
+
const baseDir = path.join(config.path, block.category);
|
|
22
|
+
|
|
23
|
+
let blockPath = path.join(baseDir, block.files[0]);
|
|
24
|
+
if (block.subdirectory) {
|
|
25
|
+
blockPath = path.join(baseDir, block.name);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (fs.existsSync(blockPath))
|
|
29
|
+
installedBlocks.push({
|
|
30
|
+
specifier: `${block.category}/${block.name}`,
|
|
31
|
+
path: blockPath,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return installedBlocks;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export { getInstalledBlocks };
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { Octokit } from 'octokit';
|
|
2
|
+
import * as v from 'valibot';
|
|
3
|
+
import { Err, Ok, type Result } from '../blocks/types/result';
|
|
4
|
+
import { type Category, categorySchema } from './build';
|
|
5
|
+
import { OUTPUT_FILE } from './index';
|
|
6
|
+
|
|
7
|
+
const octokit = new Octokit({});
|
|
8
|
+
|
|
9
|
+
export type Info = {
|
|
10
|
+
refs: 'tags' | 'heads';
|
|
11
|
+
url: string;
|
|
12
|
+
name: string;
|
|
13
|
+
repoName: string;
|
|
14
|
+
owner: string;
|
|
15
|
+
ref: string;
|
|
16
|
+
provider: Provider;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export interface Provider {
|
|
20
|
+
/** Get the name of the provider
|
|
21
|
+
*
|
|
22
|
+
* @returns the name of the provider
|
|
23
|
+
*/
|
|
24
|
+
name: () => string;
|
|
25
|
+
/** Returns a URL to the raw path of the resource provided in the resourcePath
|
|
26
|
+
*
|
|
27
|
+
* @param repoPath
|
|
28
|
+
* @param resourcePath
|
|
29
|
+
* @returns
|
|
30
|
+
*/
|
|
31
|
+
resolveRaw: (repoPath: string | Info, resourcePath: string) => Promise<URL>;
|
|
32
|
+
/** Parses the url and gives info about the repo
|
|
33
|
+
*
|
|
34
|
+
* @param repoPath
|
|
35
|
+
* @returns
|
|
36
|
+
*/
|
|
37
|
+
info: (repoPath: string) => Promise<Info>;
|
|
38
|
+
/** Returns true if this provider matches the provided url
|
|
39
|
+
*
|
|
40
|
+
* @param repoPath
|
|
41
|
+
* @returns
|
|
42
|
+
*/
|
|
43
|
+
matches: (repoPath: string) => boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Valid paths
|
|
47
|
+
*
|
|
48
|
+
* `https://github.com/<owner>/<repo>/[tree]/[ref]`
|
|
49
|
+
*
|
|
50
|
+
* `github/<owner>/<repo>/[tree]/[ref]`
|
|
51
|
+
*/
|
|
52
|
+
const github: Provider = {
|
|
53
|
+
name: () => 'github',
|
|
54
|
+
resolveRaw: async (repoPath, resourcePath) => {
|
|
55
|
+
let info: Info;
|
|
56
|
+
if (typeof repoPath === 'string') {
|
|
57
|
+
info = await github.info(repoPath);
|
|
58
|
+
} else {
|
|
59
|
+
info = repoPath;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return new URL(
|
|
63
|
+
resourcePath,
|
|
64
|
+
`https://raw.githubusercontent.com/${info.owner}/${info.repoName}/refs/${info.refs}/${info.ref}/`
|
|
65
|
+
);
|
|
66
|
+
},
|
|
67
|
+
info: async (repoPath) => {
|
|
68
|
+
const repo = repoPath.replaceAll(/(https:\/\/github.com\/)|(github\/)/g, '');
|
|
69
|
+
|
|
70
|
+
const [owner, repoName, ...rest] = repo.split('/');
|
|
71
|
+
|
|
72
|
+
let ref = 'main';
|
|
73
|
+
|
|
74
|
+
if (rest[0] === 'tree') {
|
|
75
|
+
ref = rest[1];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// checks if the type of the ref is tags or heads
|
|
79
|
+
let refs: 'heads' | 'tags' = 'heads';
|
|
80
|
+
// no need to check if ref is main
|
|
81
|
+
if (ref !== 'main') {
|
|
82
|
+
try {
|
|
83
|
+
const { data: tags } = await octokit.rest.git.listMatchingRefs({
|
|
84
|
+
owner,
|
|
85
|
+
repo: repoName,
|
|
86
|
+
ref: 'tags',
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (tags.some((tag) => tag.ref === `refs/tags/${ref}`)) {
|
|
90
|
+
refs = 'tags';
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
refs = 'heads';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
refs,
|
|
99
|
+
url: repoPath,
|
|
100
|
+
name: github.name(),
|
|
101
|
+
repoName,
|
|
102
|
+
owner,
|
|
103
|
+
ref: ref,
|
|
104
|
+
provider: github,
|
|
105
|
+
};
|
|
106
|
+
},
|
|
107
|
+
matches: (repoPath) =>
|
|
108
|
+
repoPath.toLowerCase().startsWith('https://github.com') ||
|
|
109
|
+
repoPath.toLowerCase().startsWith('github'),
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const getProviderInfo = async (repo: string): Promise<Result<Info, string>> => {
|
|
113
|
+
if (github.matches(repo)) {
|
|
114
|
+
return Ok(await github.info(repo));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return Err('Only GitHub repositories are supported at this time!');
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const getManifest = async (url: URL): Promise<Result<Category[], string>> => {
|
|
121
|
+
try {
|
|
122
|
+
const response = await fetch(url);
|
|
123
|
+
|
|
124
|
+
if (!response.ok) {
|
|
125
|
+
return Err(
|
|
126
|
+
`There was an error fetching the \`${OUTPUT_FILE}\` from the repository \`${url.href}\` make sure the target repository has a \`${OUTPUT_FILE}\` in its root?`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const categories = v.parse(v.array(categorySchema), await response.json());
|
|
131
|
+
|
|
132
|
+
return Ok(categories);
|
|
133
|
+
} catch {
|
|
134
|
+
return Err(
|
|
135
|
+
`There was an error fetching the \`${OUTPUT_FILE}\` from the repository \`${url.href}\` make sure the target repository has a \`${OUTPUT_FILE}\` in its root?`
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export { github, getProviderInfo, getManifest };
|