devfolio-page 0.2.1 → 0.2.2
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/dist/cli/commands/dev.js +110 -11
- package/dist/cli/commands/init.js +78 -28
- package/dist/generator/builder.js +2 -1
- package/dist/generator/themes/dark-academia/styles.css +1 -1
- package/dist/generator/themes/modern/styles.css +1 -1
- package/dist/generator/themes/srcl/styles.css +1 -1
- package/package.json +1 -1
- package/src/generator/themes/dark-academia/styles.css +1 -1
- package/src/generator/themes/modern/styles.css +1 -1
- package/src/generator/themes/srcl/styles.css +1 -1
package/dist/cli/commands/dev.js
CHANGED
|
@@ -29,11 +29,37 @@ async function rebuild(file, options) {
|
|
|
29
29
|
siteCache = result.files;
|
|
30
30
|
console.log();
|
|
31
31
|
console.log(chalk.green('✓') + ` Rebuilt ${result.fileList.length} files in memory`);
|
|
32
|
+
// Show available pages (HTML only)
|
|
33
|
+
const htmlFiles = Array.from(siteCache.keys()).filter(key => key.endsWith('.html'));
|
|
34
|
+
if (htmlFiles.length > 0) {
|
|
35
|
+
console.log(chalk.dim(' Available pages:'));
|
|
36
|
+
htmlFiles.forEach((key) => {
|
|
37
|
+
// Convert file path to route
|
|
38
|
+
let route = '/' + key.replace(/\.html$/, '').replace(/\/index$/, '');
|
|
39
|
+
if (route === '/')
|
|
40
|
+
route = '/ (home)';
|
|
41
|
+
console.log(chalk.dim(` ${route}`));
|
|
42
|
+
});
|
|
43
|
+
}
|
|
32
44
|
console.log();
|
|
33
45
|
}
|
|
34
46
|
catch (error) {
|
|
35
47
|
console.error(chalk.red('✗') + ' Build failed:');
|
|
36
|
-
|
|
48
|
+
// Show detailed validation errors if available
|
|
49
|
+
if (error instanceof Error && 'errors' in error) {
|
|
50
|
+
const validationError = error;
|
|
51
|
+
console.error(chalk.red(error.message));
|
|
52
|
+
console.log();
|
|
53
|
+
validationError.errors?.forEach((err) => {
|
|
54
|
+
console.error(chalk.yellow(` ${err.path}`) + `: ${err.message}`);
|
|
55
|
+
if (err.hint) {
|
|
56
|
+
console.error(chalk.dim(` Example: ${err.hint}`));
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
62
|
+
}
|
|
37
63
|
console.log();
|
|
38
64
|
}
|
|
39
65
|
finally {
|
|
@@ -52,16 +78,62 @@ function startServer(port) {
|
|
|
52
78
|
let filePath = req.url === '/' ? 'index.html' : (req.url || 'index.html');
|
|
53
79
|
// Remove leading slash and query string
|
|
54
80
|
filePath = filePath.replace(/^\//, '').split('?')[0];
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
81
|
+
// Debug: log the request
|
|
82
|
+
console.log(chalk.dim(`\n Request: ${req.url} → normalized: ${filePath}`));
|
|
83
|
+
// Try different path variations to find the file
|
|
84
|
+
let content;
|
|
85
|
+
let matchedPath;
|
|
86
|
+
// 1. Try exact path
|
|
87
|
+
const tryExact = filePath;
|
|
88
|
+
console.log(chalk.dim(` [1] Exact: ${tryExact} ${siteCache.has(tryExact) ? chalk.green('✓') : chalk.red('✗')}`));
|
|
89
|
+
if (siteCache.has(tryExact)) {
|
|
90
|
+
content = siteCache.get(tryExact);
|
|
91
|
+
matchedPath = tryExact;
|
|
92
|
+
}
|
|
93
|
+
// 2. Try adding .html extension
|
|
94
|
+
if (!content) {
|
|
95
|
+
const tryHtml = filePath + '.html';
|
|
96
|
+
console.log(chalk.dim(` [2] +.html: ${tryHtml} ${siteCache.has(tryHtml) ? chalk.green('✓') : chalk.red('✗')}`));
|
|
97
|
+
if (siteCache.has(tryHtml)) {
|
|
98
|
+
content = siteCache.get(tryHtml);
|
|
99
|
+
matchedPath = tryHtml;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// 3. Try as directory with index.html
|
|
103
|
+
if (!content) {
|
|
104
|
+
const tryDir = filePath + '/index.html';
|
|
105
|
+
console.log(chalk.dim(` [3] +/index.html: ${tryDir} ${siteCache.has(tryDir) ? chalk.green('✓') : chalk.red('✗')}`));
|
|
106
|
+
if (siteCache.has(tryDir)) {
|
|
107
|
+
content = siteCache.get(tryDir);
|
|
108
|
+
matchedPath = tryDir;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// 4. Try removing trailing slash and adding index.html
|
|
112
|
+
if (!content && filePath.endsWith('/')) {
|
|
113
|
+
const trySlash = filePath + 'index.html';
|
|
114
|
+
console.log(chalk.dim(` [4] trailing/: ${trySlash} ${siteCache.has(trySlash) ? chalk.green('✓') : chalk.red('✗')}`));
|
|
115
|
+
if (siteCache.has(trySlash)) {
|
|
116
|
+
content = siteCache.get(trySlash);
|
|
117
|
+
matchedPath = trySlash;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// If still not found, return 404
|
|
121
|
+
if (!content) {
|
|
122
|
+
console.log(chalk.dim(` 404: ${req.url}`));
|
|
123
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
58
124
|
res.end('Not found');
|
|
59
125
|
return;
|
|
60
126
|
}
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
127
|
+
// Log successful requests (excluding assets)
|
|
128
|
+
if (matchedPath && matchedPath.endsWith('.html')) {
|
|
129
|
+
console.log(chalk.dim(` 200: ${req.url}`) + chalk.green(` → ${matchedPath}`));
|
|
130
|
+
}
|
|
131
|
+
// Determine content type from original request or file path
|
|
132
|
+
let ext = path.extname(req.url || '');
|
|
133
|
+
// If no extension in URL, check if we're serving HTML
|
|
134
|
+
if (!ext && content) {
|
|
135
|
+
ext = '.html';
|
|
136
|
+
}
|
|
65
137
|
const contentTypes = {
|
|
66
138
|
'.html': 'text/html',
|
|
67
139
|
'.css': 'text/css',
|
|
@@ -82,8 +154,8 @@ function startServer(port) {
|
|
|
82
154
|
res.end(content);
|
|
83
155
|
}
|
|
84
156
|
catch (error) {
|
|
85
|
-
console.error('Server error:', error);
|
|
86
|
-
res.writeHead(500);
|
|
157
|
+
console.error(chalk.red('✗') + ' Server error:', error);
|
|
158
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
87
159
|
res.end('Internal server error');
|
|
88
160
|
}
|
|
89
161
|
});
|
|
@@ -115,10 +187,37 @@ export async function devCommand(file = 'portfolio.yaml', options = {}) {
|
|
|
115
187
|
});
|
|
116
188
|
siteCache = result.files;
|
|
117
189
|
console.log(chalk.green(' ✓') + ` Built ${result.fileList.length} files`);
|
|
190
|
+
console.log();
|
|
191
|
+
// Show available pages (HTML only)
|
|
192
|
+
const htmlFiles = Array.from(siteCache.keys()).filter(key => key.endsWith('.html'));
|
|
193
|
+
if (htmlFiles.length > 0) {
|
|
194
|
+
console.log(chalk.dim(' Available pages:'));
|
|
195
|
+
htmlFiles.forEach((key) => {
|
|
196
|
+
// Convert file path to route
|
|
197
|
+
let route = '/' + key.replace(/\.html$/, '').replace(/\/index$/, '');
|
|
198
|
+
if (route === '/')
|
|
199
|
+
route = '/ (home)';
|
|
200
|
+
console.log(chalk.dim(` ${route}`));
|
|
201
|
+
});
|
|
202
|
+
}
|
|
118
203
|
}
|
|
119
204
|
catch (error) {
|
|
120
205
|
console.error(chalk.red('✗') + ' Initial build failed:');
|
|
121
|
-
|
|
206
|
+
// Show detailed validation errors if available
|
|
207
|
+
if (error instanceof Error && 'errors' in error) {
|
|
208
|
+
const validationError = error;
|
|
209
|
+
console.error(chalk.red(error.message));
|
|
210
|
+
console.log();
|
|
211
|
+
validationError.errors?.forEach((err) => {
|
|
212
|
+
console.error(chalk.yellow(` ${err.path}`) + `: ${err.message}`);
|
|
213
|
+
if (err.hint) {
|
|
214
|
+
console.error(chalk.dim(` Example: ${err.hint}`));
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
220
|
+
}
|
|
122
221
|
process.exit(1);
|
|
123
222
|
}
|
|
124
223
|
// Start server
|
|
@@ -35,6 +35,47 @@ function question(rl, prompt) {
|
|
|
35
35
|
});
|
|
36
36
|
});
|
|
37
37
|
}
|
|
38
|
+
function isValidEmail(email) {
|
|
39
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
40
|
+
return emailRegex.test(email);
|
|
41
|
+
}
|
|
42
|
+
function isValidUrl(url) {
|
|
43
|
+
try {
|
|
44
|
+
const parsed = new URL(url);
|
|
45
|
+
return parsed.protocol === 'http:' || parsed.protocol === 'https:';
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function isValidUsername(username) {
|
|
52
|
+
// Basic validation: alphanumeric, hyphens, underscores
|
|
53
|
+
const usernameRegex = /^[a-zA-Z0-9_-]+$/;
|
|
54
|
+
return usernameRegex.test(username);
|
|
55
|
+
}
|
|
56
|
+
async function questionWithValidation(rl, prompt, validator, optional = false) {
|
|
57
|
+
while (true) {
|
|
58
|
+
const answer = await question(rl, prompt);
|
|
59
|
+
// Allow empty if optional
|
|
60
|
+
if (!answer && optional) {
|
|
61
|
+
return '';
|
|
62
|
+
}
|
|
63
|
+
// Require value if not optional
|
|
64
|
+
if (!answer && !optional) {
|
|
65
|
+
console.log(chalk.red(' ✗ This field is required'));
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
// Validate if validator provided
|
|
69
|
+
if (answer && validator) {
|
|
70
|
+
const result = validator(answer);
|
|
71
|
+
if (!result.valid) {
|
|
72
|
+
console.log(chalk.red(` ✗ ${result.error}`));
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return answer;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
38
79
|
function printHeader() {
|
|
39
80
|
console.log();
|
|
40
81
|
console.log(chalk.cyan('┌────────────────────────────────────────┐'));
|
|
@@ -195,26 +236,15 @@ export async function initCommand() {
|
|
|
195
236
|
rl.close();
|
|
196
237
|
process.exit(1);
|
|
197
238
|
}
|
|
198
|
-
const name = await
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
const location = await question(rl, chalk.cyan('? ') + 'Where are you located? ');
|
|
209
|
-
if (!location) {
|
|
210
|
-
console.log(chalk.red('✗') + ' Location is required');
|
|
211
|
-
process.exit(1);
|
|
212
|
-
}
|
|
213
|
-
const email = await question(rl, chalk.cyan('? ') + 'What\'s your email? ');
|
|
214
|
-
if (!email) {
|
|
215
|
-
console.log(chalk.red('✗') + ' Email is required');
|
|
216
|
-
process.exit(1);
|
|
217
|
-
}
|
|
239
|
+
const name = await questionWithValidation(rl, chalk.cyan('? ') + 'What\'s your name? ');
|
|
240
|
+
const title = await questionWithValidation(rl, chalk.cyan('? ') + 'What\'s your title/role? ');
|
|
241
|
+
const location = await questionWithValidation(rl, chalk.cyan('? ') + 'Where are you located? ');
|
|
242
|
+
const email = await questionWithValidation(rl, chalk.cyan('? ') + 'What\'s your email? ', (value) => {
|
|
243
|
+
if (!isValidEmail(value)) {
|
|
244
|
+
return { valid: false, error: 'Invalid email format (example: you@example.com)' };
|
|
245
|
+
}
|
|
246
|
+
return { valid: true };
|
|
247
|
+
});
|
|
218
248
|
// Theme selection
|
|
219
249
|
console.log();
|
|
220
250
|
console.log(chalk.cyan('? ') + 'Which theme would you like?');
|
|
@@ -236,10 +266,30 @@ export async function initCommand() {
|
|
|
236
266
|
console.log(chalk.yellow(' Invalid choice, using SRCL'));
|
|
237
267
|
}
|
|
238
268
|
console.log(chalk.dim('\n Optional fields (press Enter to skip)'));
|
|
239
|
-
const github = await
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
269
|
+
const github = await questionWithValidation(rl, chalk.cyan('? ') + 'GitHub username? ' + chalk.dim('(optional) '), (value) => {
|
|
270
|
+
if (!isValidUsername(value)) {
|
|
271
|
+
return { valid: false, error: 'Invalid username format (letters, numbers, hyphens, underscores only)' };
|
|
272
|
+
}
|
|
273
|
+
return { valid: true };
|
|
274
|
+
}, true);
|
|
275
|
+
const linkedin = await questionWithValidation(rl, chalk.cyan('? ') + 'LinkedIn username? ' + chalk.dim('(optional) '), (value) => {
|
|
276
|
+
if (!isValidUsername(value)) {
|
|
277
|
+
return { valid: false, error: 'Invalid username format (letters, numbers, hyphens, underscores only)' };
|
|
278
|
+
}
|
|
279
|
+
return { valid: true };
|
|
280
|
+
}, true);
|
|
281
|
+
const twitter = await questionWithValidation(rl, chalk.cyan('? ') + 'Twitter username? ' + chalk.dim('(optional) '), (value) => {
|
|
282
|
+
if (!isValidUsername(value)) {
|
|
283
|
+
return { valid: false, error: 'Invalid username format (letters, numbers, hyphens, underscores only)' };
|
|
284
|
+
}
|
|
285
|
+
return { valid: true };
|
|
286
|
+
}, true);
|
|
287
|
+
const website = await questionWithValidation(rl, chalk.cyan('? ') + 'Website URL? ' + chalk.dim('(optional) '), (value) => {
|
|
288
|
+
if (!isValidUrl(value)) {
|
|
289
|
+
return { valid: false, error: 'Invalid URL format (must start with http:// or https://)' };
|
|
290
|
+
}
|
|
291
|
+
return { valid: true };
|
|
292
|
+
}, true);
|
|
243
293
|
rl.close();
|
|
244
294
|
const input = {
|
|
245
295
|
name,
|
|
@@ -267,12 +317,12 @@ export async function initCommand() {
|
|
|
267
317
|
console.log(chalk.dim(' └── ') + chalk.cyan('site/') + chalk.dim(' (generated after render)'));
|
|
268
318
|
console.log();
|
|
269
319
|
console.log(chalk.bold('Next steps:'));
|
|
270
|
-
console.log(chalk.dim(' 1.') + ' Edit ' + chalk.cyan(`${folderName}/portfolio.yaml`) + ' and add your
|
|
320
|
+
console.log(chalk.dim(' 1.') + ' Edit ' + chalk.cyan(`${folderName}/portfolio.yaml`) + ' and add your content');
|
|
271
321
|
console.log(chalk.dim(' 2.') + ' Add images to ' + chalk.cyan(`${folderName}/images/`));
|
|
272
|
-
console.log(chalk.dim(' 3.') + ' Run: ' + chalk.cyan(`cd ${folderName} && devfolio-page
|
|
273
|
-
console.log(chalk.dim(' 4.') + ' Open: ' + chalk.cyan(
|
|
322
|
+
console.log(chalk.dim(' 3.') + ' Run: ' + chalk.cyan(`cd ${folderName} && devfolio-page dev`));
|
|
323
|
+
console.log(chalk.dim(' 4.') + ' Open: ' + chalk.cyan(`http://localhost:3000`) + ' in your browser');
|
|
274
324
|
console.log();
|
|
275
|
-
console.log('Tip: Check ' + chalk.cyan('https://devfolio.page/
|
|
325
|
+
console.log('Tip: Check ' + chalk.cyan('https://devfolio.page/docs') + ' for full documentation');
|
|
276
326
|
console.log();
|
|
277
327
|
}
|
|
278
328
|
catch (err) {
|
|
@@ -645,7 +645,8 @@ async function generateSinglePageSiteInMemory(portfolio, themePath, theme, files
|
|
|
645
645
|
enableHotkeys: settings.enable_hotkeys !== false,
|
|
646
646
|
animate: settings.animate || 'subtle',
|
|
647
647
|
hasExperience: (portfolio.sections.experience?.length ?? 0) > 0,
|
|
648
|
-
hasProjects: (portfolio.sections.projects?.length ?? 0) > 0,
|
|
648
|
+
hasProjects: ((portfolio.projects?.length ?? 0) > 0) || ((portfolio.sections.projects?.length ?? 0) > 0),
|
|
649
|
+
hasExperiments: (portfolio.experiments?.length ?? 0) > 0,
|
|
649
650
|
hasSkills: portfolio.sections.skills && Object.keys(portfolio.sections.skills).length > 0,
|
|
650
651
|
hasWriting: (portfolio.sections.writing?.length ?? 0) > 0,
|
|
651
652
|
hasEducation: (portfolio.sections.education?.length ?? 0) > 0,
|
package/package.json
CHANGED