nobadfonts-cli 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/PUBLISHING.md +34 -0
- package/index.js +228 -0
- package/package.json +19 -0
- package/test_list.js +16 -0
package/PUBLISHING.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# How to Publish NoBadFonts CLI
|
|
2
|
+
|
|
3
|
+
You have the source code for the CLI tool in this folder. To make it available to everyone via `npx nobadfonts`, you need to publish it to npm.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
1. Create an account on [npmjs.com](https://www.npmjs.com/).
|
|
8
|
+
2. Login to npm in your terminal:
|
|
9
|
+
```bash
|
|
10
|
+
npm login
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## publishing
|
|
14
|
+
|
|
15
|
+
1. Navigate to this folder:
|
|
16
|
+
```bash
|
|
17
|
+
cd nobadfonts-cli
|
|
18
|
+
```
|
|
19
|
+
2. Update the `package.json` name if `nobadfonts-cli` is taken (it might be!). heavily recommended to scope it e.g. `@yourusername/nobadfonts` or just `nobadfonts-official`.
|
|
20
|
+
- If you change the name, update the `bin` command in `package.json` if you want the command to differ.
|
|
21
|
+
3. Publish:
|
|
22
|
+
```bash
|
|
23
|
+
npm publish --access public
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Testing Locally (Without Publishing)
|
|
27
|
+
|
|
28
|
+
You can test the CLI globally on your machine:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm link
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Now you can run `nobadfonts` anywhere in your terminal.
|
package/index.js
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fetch from 'node-fetch';
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import ora from 'ora';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
|
|
10
|
+
const SUPABASE_URL = 'https://wcegdxhvgwbeskaidlxr.supabase.co';
|
|
11
|
+
const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6IndjZWdkeGh2Z3diZXNrYWlkbHhyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Njk1MjI3OTQsImV4cCI6MjA4NTA5ODc5NH0.P_JY0RF6wVdPCDfWLlcor5l1CP3g4bLE5y4JWmZVOig';
|
|
12
|
+
|
|
13
|
+
const args = process.argv.slice(2);
|
|
14
|
+
const command = args[0];
|
|
15
|
+
const targetFont = args[1];
|
|
16
|
+
|
|
17
|
+
if (!command) {
|
|
18
|
+
console.log(chalk.yellow('Usage: npx nobadfonts add <font-name>'));
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (command === 'add') {
|
|
23
|
+
if (!targetFont) {
|
|
24
|
+
console.log(chalk.red('Please specify a font name. Example: npx nobadfonts add "Inter"'));
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
addFont(targetFont);
|
|
28
|
+
} else if (command === 'list') {
|
|
29
|
+
listFonts();
|
|
30
|
+
} else {
|
|
31
|
+
console.log(chalk.red(`Unknown command: ${command}`));
|
|
32
|
+
console.log(chalk.yellow('Available commands: add <font>, list'));
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function listFonts() {
|
|
37
|
+
const spinner = ora('Fetching available fonts...').start();
|
|
38
|
+
try {
|
|
39
|
+
const res = await fetch(`${SUPABASE_URL}/rest/v1/fonts?select=name,category&limit=50&order=downloads.desc`, {
|
|
40
|
+
headers: {
|
|
41
|
+
'apikey': SUPABASE_ANON_KEY,
|
|
42
|
+
'Authorization': `Bearer ${SUPABASE_ANON_KEY}`
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
if (!res.ok) throw new Error(res.statusText);
|
|
46
|
+
const data = await res.json();
|
|
47
|
+
spinner.stop();
|
|
48
|
+
|
|
49
|
+
console.log(chalk.bold.cyan('\nAvailable Fonts (Top 50):'));
|
|
50
|
+
data.forEach(f => {
|
|
51
|
+
console.log(`${chalk.green('•')} ${chalk.white(f.name)} ${chalk.gray(`(${f.category})`)}`);
|
|
52
|
+
});
|
|
53
|
+
console.log('\nTo install: ' + chalk.yellow('npx nobadfonts add "Font Name"'));
|
|
54
|
+
|
|
55
|
+
} catch (e) {
|
|
56
|
+
spinner.fail('Error fetching fonts: ' + e.message);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function addFont(fontName) {
|
|
61
|
+
const spinner = ora(`Searching for font: ${fontName}...`).start();
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
// Search for font by name or slug
|
|
65
|
+
const searchUrl = `${SUPABASE_URL}/rest/v1/fonts?or=(name.ilike.*${fontName}*,slug.ilike.*${fontName}*)&select=*,font_variants(*)&limit=1`;
|
|
66
|
+
|
|
67
|
+
const response = await fetch(searchUrl, {
|
|
68
|
+
headers: {
|
|
69
|
+
'apikey': SUPABASE_ANON_KEY,
|
|
70
|
+
'Authorization': `Bearer ${SUPABASE_ANON_KEY}`
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
throw new Error(`Failed to fetch font data: ${response.statusText}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const data = await response.json();
|
|
79
|
+
|
|
80
|
+
if (!data || data.length === 0) {
|
|
81
|
+
spinner.fail(chalk.red(`Font "${fontName}" not found.`));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const font = data[0];
|
|
86
|
+
spinner.succeed(chalk.green(`Found font: ${font.name}`));
|
|
87
|
+
|
|
88
|
+
// download files
|
|
89
|
+
const downloadDir = path.join(process.cwd(), 'public', 'fonts', font.slug);
|
|
90
|
+
await fs.ensureDir(downloadDir);
|
|
91
|
+
|
|
92
|
+
const cssContent = [];
|
|
93
|
+
const cssPath = path.join(process.cwd(), 'src', 'index.css'); // Default assumption, maybe configure later? Or creates dedicated file.
|
|
94
|
+
let imports = '';
|
|
95
|
+
|
|
96
|
+
// Function to download file
|
|
97
|
+
const downloadFile = async (url, filename) => {
|
|
98
|
+
if (!url) return null;
|
|
99
|
+
try {
|
|
100
|
+
const res = await fetch(url);
|
|
101
|
+
if (!res.ok) throw new Error(`Failed to download ${url}`);
|
|
102
|
+
const buffer = await res.buffer();
|
|
103
|
+
const filePath = path.join(downloadDir, filename);
|
|
104
|
+
await fs.writeFile(filePath, buffer);
|
|
105
|
+
return filePath;
|
|
106
|
+
} catch (e) {
|
|
107
|
+
console.error(chalk.red(`Error downloading ${filename}: ${e.message}`));
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
spinner.start('Downloading font files...');
|
|
113
|
+
|
|
114
|
+
// Download Main Font
|
|
115
|
+
// We prefer woff2 -> woff -> ttf -> otf
|
|
116
|
+
const mainFiles = [
|
|
117
|
+
{ url: font.woff2_url, name: `${font.slug}.woff2`, format: 'woff2' },
|
|
118
|
+
{ url: font.woff_url, name: `${font.slug}.woff`, format: 'woff' },
|
|
119
|
+
{ url: font.ttf_url, name: `${font.slug}.ttf`, format: 'truetype' },
|
|
120
|
+
{ url: font.otf_url, name: `${font.slug}.otf`, format: 'opentype' }
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
let mainFileDownloaded = false;
|
|
124
|
+
let mainCssSrc = [];
|
|
125
|
+
|
|
126
|
+
// Prioritize formats for main font
|
|
127
|
+
for (const f of mainFiles) {
|
|
128
|
+
if (f.url) {
|
|
129
|
+
await downloadFile(f.url, f.name);
|
|
130
|
+
mainCssSrc.push(`url('/fonts/${font.slug}/${f.name}') format('${f.format}')`);
|
|
131
|
+
mainFileDownloaded = true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (mainFileDownloaded) {
|
|
136
|
+
cssContent.push(`
|
|
137
|
+
@font-face {
|
|
138
|
+
font-family: '${font.name}';
|
|
139
|
+
src: ${mainCssSrc.join(',\n ')};
|
|
140
|
+
font-weight: normal;
|
|
141
|
+
font-style: normal;
|
|
142
|
+
font-display: swap;
|
|
143
|
+
}
|
|
144
|
+
`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Download Variants
|
|
148
|
+
if (font.font_variants && font.font_variants.length > 0) {
|
|
149
|
+
for (const variant of font.font_variants) {
|
|
150
|
+
const vFiles = [
|
|
151
|
+
{ url: variant.woff2_url, name: `${font.slug}-${variant.variant_name}.woff2`, format: 'woff2' },
|
|
152
|
+
{ url: variant.woff_url, name: `${font.slug}-${variant.variant_name}.woff`, format: 'woff' },
|
|
153
|
+
{ url: variant.ttf_url, name: `${font.slug}-${variant.variant_name}.ttf`, format: 'truetype' },
|
|
154
|
+
{ url: variant.otf_url, name: `${font.slug}-${variant.variant_name}.otf`, format: 'opentype' }
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
let vCssSrc = [];
|
|
158
|
+
// Determine weight/style from name (simple heuristic)
|
|
159
|
+
let weight = 400;
|
|
160
|
+
let style = 'normal';
|
|
161
|
+
const nameLower = variant.variant_name.toLowerCase();
|
|
162
|
+
|
|
163
|
+
if (nameLower.includes('bold')) weight = 700;
|
|
164
|
+
if (nameLower.includes('light')) weight = 300;
|
|
165
|
+
if (nameLower.includes('medium')) weight = 500;
|
|
166
|
+
if (nameLower.includes('black')) weight = 900;
|
|
167
|
+
if (nameLower.includes('thin')) weight = 100;
|
|
168
|
+
if (nameLower.includes('extra')) weight = 800; // heuristic
|
|
169
|
+
|
|
170
|
+
if (nameLower.includes('italic')) style = 'italic';
|
|
171
|
+
|
|
172
|
+
for (const f of vFiles) {
|
|
173
|
+
if (f.url) {
|
|
174
|
+
await downloadFile(f.url, f.name);
|
|
175
|
+
vCssSrc.push(`url('/fonts/${font.slug}/${f.name}') format('${f.format}')`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (vCssSrc.length > 0) {
|
|
180
|
+
cssContent.push(`
|
|
181
|
+
@font-face {
|
|
182
|
+
font-family: '${font.name}';
|
|
183
|
+
src: ${vCssSrc.join(',\n ')};
|
|
184
|
+
font-weight: ${weight};
|
|
185
|
+
font-style: ${style};
|
|
186
|
+
font-display: swap;
|
|
187
|
+
}
|
|
188
|
+
`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
spinner.succeed('Files downloaded successfully.');
|
|
194
|
+
|
|
195
|
+
// Write CSS
|
|
196
|
+
// Create specific CSS file
|
|
197
|
+
const outputCssPath = path.join(process.cwd(), 'src', 'fonts.css');
|
|
198
|
+
|
|
199
|
+
// Check if file exists to append or create
|
|
200
|
+
// If we append, we check if font-family is already defined to avoid duplicates?
|
|
201
|
+
// For simplicity, just append.
|
|
202
|
+
|
|
203
|
+
spinner.start('Updating CSS...');
|
|
204
|
+
|
|
205
|
+
let existingCss = '';
|
|
206
|
+
if (await fs.pathExists(outputCssPath)) {
|
|
207
|
+
existingCss = await fs.readFile(outputCssPath, 'utf8');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const newCssBlock = `
|
|
211
|
+
/* Font: ${font.name} (Added via NoBadFonts CLI) */
|
|
212
|
+
${cssContent.join('\n')}
|
|
213
|
+
`;
|
|
214
|
+
|
|
215
|
+
if (!existingCss.includes(`Font: ${font.name}`)) {
|
|
216
|
+
await fs.outputFile(outputCssPath, existingCss + newCssBlock);
|
|
217
|
+
spinner.succeed(chalk.green(`Updated src/fonts.css with @font-face definitions.`));
|
|
218
|
+
console.log(chalk.blue(`\nSuccess! To use the font, add this to your CSS:`));
|
|
219
|
+
console.log(chalk.cyan(`font-family: '${font.name}', sans-serif;`));
|
|
220
|
+
console.log(chalk.gray(`(Make sure to import './fonts.css' in your main execution file)`));
|
|
221
|
+
} else {
|
|
222
|
+
spinner.info(chalk.yellow(`Font ${font.name} definitions already exist in src/fonts.css`));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
} catch (error) {
|
|
226
|
+
spinner.fail(chalk.red(`Error: ${error.message}`));
|
|
227
|
+
}
|
|
228
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nobadfonts-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI tool to instantly install fonts from NoBadFonts",
|
|
5
|
+
"bin": {
|
|
6
|
+
"nobadfonts": "index.js"
|
|
7
|
+
},
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"chalk": "^4.1.2",
|
|
10
|
+
"node-fetch": "^2.6.7",
|
|
11
|
+
"ora": "^5.4.1",
|
|
12
|
+
"fs-extra": "^10.0.0"
|
|
13
|
+
},
|
|
14
|
+
"main": "index.js",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"scripts": {
|
|
17
|
+
"start": "node index.js"
|
|
18
|
+
}
|
|
19
|
+
}
|
package/test_list.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
|
|
2
|
+
import fetch from 'node-fetch';
|
|
3
|
+
|
|
4
|
+
const SUPABASE_URL = 'https://wcegdxhvgwbeskaidlxr.supabase.co';
|
|
5
|
+
const SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6IndjZWdkeGh2Z3diZXNrYWlkbHhyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Njk1MjI3OTQsImV4cCI6MjA4NTA5ODc5NH0.P_JY0RF6wVdPCDfWLlcor5l1CP3g4bLE5y4JWmZVOig';
|
|
6
|
+
|
|
7
|
+
(async () => {
|
|
8
|
+
const res = await fetch(`${SUPABASE_URL}/rest/v1/fonts?select=name&limit=10`, {
|
|
9
|
+
headers: {
|
|
10
|
+
'apikey': SUPABASE_ANON_KEY,
|
|
11
|
+
'Authorization': `Bearer ${SUPABASE_ANON_KEY}`
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
const data = await res.json();
|
|
15
|
+
console.log(JSON.stringify(data, null, 2));
|
|
16
|
+
})();
|