faang 0.0.0-stage → 0.1.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 +17 -2
- package/index.mjs +265 -0
- package/package.json +31 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mertcan Dundar Esmergul (Faang)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
-
#
|
|
1
|
+
# faang
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Add [Faang](https://faang.vira.ia.br) components to your project as source.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx faang@latest list
|
|
7
|
+
npx faang@latest add button
|
|
8
|
+
npx faang@latest add data-table stat-cards
|
|
9
|
+
npx faang@latest add --all
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Each item declares the other items it imports and the npm packages it needs, so
|
|
13
|
+
`add` pulls the whole graph in order and prints the one `npm install` line the
|
|
14
|
+
host project is missing.
|
|
15
|
+
|
|
16
|
+
Options: `--cwd <dir>`, `--registry <url>`, `--overwrite`, `--dry-run`, `--json`.
|
|
17
|
+
`FAANG_REGISTRY` sets the registry for CI. Default registry:
|
|
18
|
+
`https://faang.vira.ia.br/r`.
|
package/index.mjs
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* faang — install Faang components into a project as source.
|
|
4
|
+
*
|
|
5
|
+
* npx faang@latest list
|
|
6
|
+
* npx faang@latest add button
|
|
7
|
+
* npx faang@latest add data-table stat-cards
|
|
8
|
+
* npx faang@latest add --all
|
|
9
|
+
*
|
|
10
|
+
* The components come from Faang's registry (a JSON index plus one file per
|
|
11
|
+
* item, served by the site). Every item declares its own npm dependencies and
|
|
12
|
+
* the other items it imports, so installing one pulls its whole graph in the
|
|
13
|
+
* right order and prints the packages the host project still needs.
|
|
14
|
+
*/
|
|
15
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
17
|
+
|
|
18
|
+
const DEFAULT_REGISTRY = process.env.FAANG_REGISTRY ?? "https://faang.vira.ia.br/r";
|
|
19
|
+
const VERSION = "0.1.0";
|
|
20
|
+
|
|
21
|
+
const HELP = `
|
|
22
|
+
faang v${VERSION} — add Faang components to your project
|
|
23
|
+
|
|
24
|
+
Usage
|
|
25
|
+
faang list every component in the registry
|
|
26
|
+
faang add <name...> install one or more components
|
|
27
|
+
faang add --all install the whole registry
|
|
28
|
+
faang add all same thing
|
|
29
|
+
faang add <name> --dry-run show what would be written
|
|
30
|
+
|
|
31
|
+
Options
|
|
32
|
+
--cwd <dir> project to write into (default: current directory)
|
|
33
|
+
--registry <url> registry to read from (default: ${DEFAULT_REGISTRY})
|
|
34
|
+
--overwrite replace files that already exist
|
|
35
|
+
--dry-run print the plan without touching disk
|
|
36
|
+
--json machine-readable output for \`list\`
|
|
37
|
+
-h, --help this text
|
|
38
|
+
|
|
39
|
+
Registry
|
|
40
|
+
FAANG_REGISTRY same as --registry, for CI
|
|
41
|
+
`;
|
|
42
|
+
|
|
43
|
+
const color = process.env.NO_COLOR || !process.stdout.isTTY
|
|
44
|
+
? { dim: (s) => s, bold: (s) => s, green: (s) => s, red: (s) => s, cyan: (s) => s }
|
|
45
|
+
: {
|
|
46
|
+
dim: (s) => `\u001b[2m${s}\u001b[0m`,
|
|
47
|
+
bold: (s) => `\u001b[1m${s}\u001b[0m`,
|
|
48
|
+
green: (s) => `\u001b[32m${s}\u001b[0m`,
|
|
49
|
+
red: (s) => `\u001b[31m${s}\u001b[0m`,
|
|
50
|
+
cyan: (s) => `\u001b[36m${s}\u001b[0m`,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function parseArgs(argv) {
|
|
54
|
+
const args = { _: [], names: [] };
|
|
55
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
56
|
+
const arg = argv[i];
|
|
57
|
+
switch (arg) {
|
|
58
|
+
case "--cwd":
|
|
59
|
+
args.cwd = argv[++i];
|
|
60
|
+
break;
|
|
61
|
+
case "--registry":
|
|
62
|
+
args.registry = argv[++i];
|
|
63
|
+
break;
|
|
64
|
+
case "--overwrite":
|
|
65
|
+
args.overwrite = true;
|
|
66
|
+
break;
|
|
67
|
+
case "--dry-run":
|
|
68
|
+
args.dryRun = true;
|
|
69
|
+
break;
|
|
70
|
+
case "--all":
|
|
71
|
+
args.all = true;
|
|
72
|
+
break;
|
|
73
|
+
case "--json":
|
|
74
|
+
args.json = true;
|
|
75
|
+
break;
|
|
76
|
+
case "-h":
|
|
77
|
+
case "--help":
|
|
78
|
+
args.help = true;
|
|
79
|
+
break;
|
|
80
|
+
case "-v":
|
|
81
|
+
case "--version":
|
|
82
|
+
args.version = true;
|
|
83
|
+
break;
|
|
84
|
+
default:
|
|
85
|
+
if (arg.startsWith("-")) {
|
|
86
|
+
console.error(color.red(`Unknown option: ${arg}`));
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
args.names.push(arg);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return args;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function getJson(url) {
|
|
96
|
+
const response = await fetch(url);
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
throw new Error(`${response.status} ${response.statusText} — ${url}`);
|
|
99
|
+
}
|
|
100
|
+
return response.json();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function registryUrl(args) {
|
|
104
|
+
return (args.registry ?? DEFAULT_REGISTRY).replace(/\/+$/, "");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function loadIndex(args) {
|
|
108
|
+
try {
|
|
109
|
+
return await getJson(`${registryUrl(args)}/index.json`);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.error(color.red(`Could not read the registry at ${registryUrl(args)}`));
|
|
112
|
+
console.error(color.dim(String(error.message ?? error)));
|
|
113
|
+
console.error(color.dim("Use --registry <url> or set FAANG_REGISTRY."));
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** "Button", "buttons/button" and "button" all resolve to the item named button. */
|
|
119
|
+
function resolveName(index, wanted) {
|
|
120
|
+
const term = wanted.toLowerCase().replace(/^@faang\//, "");
|
|
121
|
+
return (
|
|
122
|
+
index.items.find((item) => item.name === term) ??
|
|
123
|
+
index.items.find((item) => item.name === term.split("/").pop()) ??
|
|
124
|
+
index.items.find((item) => item.title.toLowerCase() === term)
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function cmdList(args) {
|
|
129
|
+
const index = await loadIndex(args);
|
|
130
|
+
if (args.json) {
|
|
131
|
+
console.log(JSON.stringify(index.items.map(({ name, title, type }) => ({ name, title, type })), null, 2));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const width = Math.max(...index.items.map((item) => item.name.length));
|
|
135
|
+
for (const item of index.items) {
|
|
136
|
+
console.log(` ${item.name.padEnd(width)} ${color.dim(item.title)}`);
|
|
137
|
+
}
|
|
138
|
+
console.log(color.dim(`\n ${index.items.length} items — ${registryUrl(args)}`));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function cmdAdd(args) {
|
|
142
|
+
if (!args.all && args.names.length === 0) {
|
|
143
|
+
console.error(color.red("Name at least one component, or pass --all."));
|
|
144
|
+
console.error(color.dim("faang list"));
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const cwd = isAbsolute(args.cwd ?? "") ? args.cwd : resolve(process.cwd(), args.cwd ?? ".");
|
|
149
|
+
const index = await loadIndex(args);
|
|
150
|
+
|
|
151
|
+
let wanted;
|
|
152
|
+
if (args.all) {
|
|
153
|
+
wanted = index.items;
|
|
154
|
+
} else {
|
|
155
|
+
wanted = args.names.map((name) => {
|
|
156
|
+
const item = resolveName(index, name);
|
|
157
|
+
if (!item) {
|
|
158
|
+
console.error(color.red(`Not in the registry: ${name}`));
|
|
159
|
+
process.exit(1);
|
|
160
|
+
}
|
|
161
|
+
return item;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Items are summaries in the index; fetch each one for its file contents.
|
|
166
|
+
const detail = async (item) => getJson(`${registryUrl(args)}/${item.path ?? `${item.name}.json`}`);
|
|
167
|
+
const resolved = [];
|
|
168
|
+
for (const item of wanted) resolved.push(await detail(item));
|
|
169
|
+
|
|
170
|
+
// Resolve registry dependencies to their full items before installing.
|
|
171
|
+
async function expand(item, acc, seen) {
|
|
172
|
+
if (seen.has(item.name)) return;
|
|
173
|
+
seen.add(item.name);
|
|
174
|
+
for (const name of item.registryDependencies ?? []) {
|
|
175
|
+
const summary = index.items.find((entry) => entry.name === name);
|
|
176
|
+
if (!summary) continue;
|
|
177
|
+
await expand(await detail(summary), acc, seen);
|
|
178
|
+
}
|
|
179
|
+
acc.push(item);
|
|
180
|
+
}
|
|
181
|
+
const ordered = [];
|
|
182
|
+
const seen = new Set();
|
|
183
|
+
for (const item of resolved) await expand(item, ordered, seen);
|
|
184
|
+
|
|
185
|
+
const dependencies = new Set();
|
|
186
|
+
const plan = [];
|
|
187
|
+
for (const item of ordered) {
|
|
188
|
+
for (const dep of item.dependencies ?? []) dependencies.add(dep);
|
|
189
|
+
for (const file of item.files ?? []) plan.push({ ...file, item: item.name });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const written = [];
|
|
193
|
+
const skipped = [];
|
|
194
|
+
|
|
195
|
+
for (const file of plan) {
|
|
196
|
+
const target = join(cwd, file.path);
|
|
197
|
+
const exists = existsSync(target);
|
|
198
|
+
if (exists && !args.overwrite) {
|
|
199
|
+
skipped.push(target);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (!args.dryRun) {
|
|
203
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
204
|
+
writeFileSync(target, file.content ?? "", "utf8");
|
|
205
|
+
}
|
|
206
|
+
written.push(target);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const label = args.dryRun ? "would write" : "wrote";
|
|
210
|
+
console.log("");
|
|
211
|
+
for (const item of ordered) {
|
|
212
|
+
console.log(`${color.green("✔")} ${color.bold(item.name)}`);
|
|
213
|
+
}
|
|
214
|
+
console.log("");
|
|
215
|
+
for (const file of written) {
|
|
216
|
+
console.log(` ${color.dim(label)} ${file.replace(`${cwd}/`, "")}`);
|
|
217
|
+
}
|
|
218
|
+
for (const file of skipped) {
|
|
219
|
+
console.log(` ${color.dim("skipped (exists)")} ${file.replace(`${cwd}/`, "")}`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (dependencies.size > 0) {
|
|
223
|
+
console.log(`\n ${color.bold("npm packages")}\n`);
|
|
224
|
+
console.log(` npm install ${[...dependencies].join(" ")}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (skipped.length > 0) {
|
|
228
|
+
console.log(color.dim("\n Existing files were left alone; pass --overwrite to replace them."));
|
|
229
|
+
}
|
|
230
|
+
if (args.dryRun) {
|
|
231
|
+
console.log(color.dim("\n Dry run: nothing was written."));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function main() {
|
|
236
|
+
const args = parseArgs(process.argv.slice(2));
|
|
237
|
+
if (args.names.includes("all")) {
|
|
238
|
+
args.all = true;
|
|
239
|
+
args.names = args.names.filter((name) => name !== "all");
|
|
240
|
+
}
|
|
241
|
+
if (args.version) {
|
|
242
|
+
console.log(VERSION);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (args.help || args._.length === 0 && args.names.length === 0 && !args.all) {
|
|
246
|
+
console.log(HELP);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const command = args.names[0] ?? args._[0];
|
|
251
|
+
const rest = args.names.slice(1);
|
|
252
|
+
|
|
253
|
+
if (command === "list") return cmdList(args);
|
|
254
|
+
if (command === "add") return cmdAdd({ ...args, names: rest });
|
|
255
|
+
if (command === "help") return console.log(HELP);
|
|
256
|
+
|
|
257
|
+
console.error(color.red(`Unknown command: ${command}`));
|
|
258
|
+
console.log(HELP);
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
main().catch((error) => {
|
|
263
|
+
console.error(color.red(String(error?.message ?? error)));
|
|
264
|
+
process.exit(1);
|
|
265
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "faang",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Add Faang components to your project.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"faang": "index.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.mjs",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18"
|
|
16
|
+
},
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"homepage": "https://faang.vira.ia.br",
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/ferforastieri/faang.git",
|
|
22
|
+
"directory": "cli"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"react",
|
|
26
|
+
"design-system",
|
|
27
|
+
"tailwind",
|
|
28
|
+
"nextjs",
|
|
29
|
+
"components",
|
|
30
|
+
"cli",
|
|
31
|
+
"registry"
|
|
32
|
+
]
|
|
33
|
+
}
|