devfolio-page 0.2.0 → 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 +130 -34
- package/dist/cli/commands/init.js +78 -28
- package/dist/cli/index.js +1 -1
- package/dist/generator/builder.js +280 -0
- 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
|
@@ -3,10 +3,11 @@ import chokidar from 'chokidar';
|
|
|
3
3
|
import { existsSync } from 'fs';
|
|
4
4
|
import http from 'http';
|
|
5
5
|
import path from 'path';
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
6
|
+
import { buildStaticSiteInMemory } from '../../generator/builder.js';
|
|
7
|
+
import { validatePortfolio } from '../helpers/validate.js';
|
|
8
8
|
let isBuilding = false;
|
|
9
9
|
let buildQueued = false;
|
|
10
|
+
let siteCache = new Map();
|
|
10
11
|
async function rebuild(file, options) {
|
|
11
12
|
// If already building, queue another build
|
|
12
13
|
if (isBuilding) {
|
|
@@ -17,19 +18,48 @@ async function rebuild(file, options) {
|
|
|
17
18
|
try {
|
|
18
19
|
console.log();
|
|
19
20
|
console.log(chalk.dim(`[${new Date().toLocaleTimeString()}]`) + ' File changed: ' + chalk.cyan(file));
|
|
20
|
-
console.log(chalk.dim('Rebuilding...'));
|
|
21
|
+
console.log(chalk.dim('Rebuilding in memory...'));
|
|
21
22
|
console.log();
|
|
22
|
-
|
|
23
|
+
// Validate and build in memory
|
|
24
|
+
const portfolio = validatePortfolio('portfolio.yaml');
|
|
25
|
+
const result = await buildStaticSiteInMemory(portfolio, {
|
|
23
26
|
theme: options.theme,
|
|
24
|
-
output: options.output || './site',
|
|
25
27
|
});
|
|
28
|
+
// Update cache
|
|
29
|
+
siteCache = result.files;
|
|
26
30
|
console.log();
|
|
27
|
-
console.log(chalk.green('✓') +
|
|
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
|
+
}
|
|
28
44
|
console.log();
|
|
29
45
|
}
|
|
30
46
|
catch (error) {
|
|
31
47
|
console.error(chalk.red('✗') + ' Build failed:');
|
|
32
|
-
|
|
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
|
+
}
|
|
33
63
|
console.log();
|
|
34
64
|
}
|
|
35
65
|
finally {
|
|
@@ -41,31 +71,69 @@ async function rebuild(file, options) {
|
|
|
41
71
|
}
|
|
42
72
|
}
|
|
43
73
|
}
|
|
44
|
-
function startServer(
|
|
74
|
+
function startServer(port) {
|
|
45
75
|
const server = http.createServer(async (req, res) => {
|
|
46
76
|
try {
|
|
47
77
|
// Default to index.html
|
|
48
|
-
let filePath = req.url === '/' ? '
|
|
49
|
-
// Remove query string
|
|
50
|
-
filePath = filePath.split('?')[0];
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
78
|
+
let filePath = req.url === '/' ? 'index.html' : (req.url || 'index.html');
|
|
79
|
+
// Remove leading slash and query string
|
|
80
|
+
filePath = filePath.replace(/^\//, '').split('?')[0];
|
|
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
|
+
}
|
|
58
119
|
}
|
|
59
|
-
//
|
|
60
|
-
if (!
|
|
61
|
-
|
|
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' });
|
|
62
124
|
res.end('Not found');
|
|
63
125
|
return;
|
|
64
126
|
}
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
+
}
|
|
69
137
|
const contentTypes = {
|
|
70
138
|
'.html': 'text/html',
|
|
71
139
|
'.css': 'text/css',
|
|
@@ -86,8 +154,8 @@ function startServer(outputDir, port) {
|
|
|
86
154
|
res.end(content);
|
|
87
155
|
}
|
|
88
156
|
catch (error) {
|
|
89
|
-
console.error('Server error:', error);
|
|
90
|
-
res.writeHead(500);
|
|
157
|
+
console.error(chalk.red('✗') + ' Server error:', error);
|
|
158
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
91
159
|
res.end('Internal server error');
|
|
92
160
|
}
|
|
93
161
|
});
|
|
@@ -96,7 +164,6 @@ function startServer(outputDir, port) {
|
|
|
96
164
|
}
|
|
97
165
|
export async function devCommand(file = 'portfolio.yaml', options = {}) {
|
|
98
166
|
const port = options.port || 3000;
|
|
99
|
-
const outputDir = path.resolve(options.output || './site');
|
|
100
167
|
// Check if portfolio.yaml exists
|
|
101
168
|
if (!existsSync(file)) {
|
|
102
169
|
console.error(chalk.red('✗') + ` File not found: ${file}`);
|
|
@@ -111,26 +178,55 @@ export async function devCommand(file = 'portfolio.yaml', options = {}) {
|
|
|
111
178
|
console.log(chalk.cyan('└─────────────────────────────────────────────────────────┘'));
|
|
112
179
|
console.log();
|
|
113
180
|
// Initial build
|
|
114
|
-
console.log(chalk.bold('Building initial site...'));
|
|
181
|
+
console.log(chalk.bold('Building initial site in memory...'));
|
|
115
182
|
console.log();
|
|
116
183
|
try {
|
|
117
|
-
|
|
184
|
+
const portfolio = validatePortfolio(file);
|
|
185
|
+
const result = await buildStaticSiteInMemory(portfolio, {
|
|
118
186
|
theme: options.theme,
|
|
119
|
-
output: outputDir,
|
|
120
187
|
});
|
|
188
|
+
siteCache = result.files;
|
|
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
|
+
}
|
|
121
203
|
}
|
|
122
204
|
catch (error) {
|
|
123
205
|
console.error(chalk.red('✗') + ' Initial build failed:');
|
|
124
|
-
|
|
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
|
+
}
|
|
125
221
|
process.exit(1);
|
|
126
222
|
}
|
|
127
223
|
// Start server
|
|
128
|
-
const server = startServer(
|
|
224
|
+
const server = startServer(port);
|
|
129
225
|
console.log();
|
|
130
226
|
console.log(chalk.green('✓') + ' Dev server started!');
|
|
131
227
|
console.log();
|
|
132
228
|
console.log(' ' + chalk.bold('Local:') + ' ' + chalk.cyan(`http://localhost:${port}`));
|
|
133
|
-
console.log(' ' + chalk.bold('
|
|
229
|
+
console.log(' ' + chalk.bold('Mode:') + ' ' + chalk.dim('In-memory (no site/ folder)'));
|
|
134
230
|
console.log();
|
|
135
231
|
console.log(chalk.dim('Watching for changes...'));
|
|
136
232
|
console.log(chalk.dim('Press Ctrl+C to stop'));
|
|
@@ -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) {
|
package/dist/cli/index.js
CHANGED
|
@@ -10,7 +10,7 @@ const program = new Command();
|
|
|
10
10
|
program
|
|
11
11
|
.name('devfolio-page')
|
|
12
12
|
.description('Your portfolio as code. Version control it like software.')
|
|
13
|
-
.version('0.2.
|
|
13
|
+
.version('0.2.1');
|
|
14
14
|
program
|
|
15
15
|
.command('init')
|
|
16
16
|
.description('Create a new portfolio.yaml with interactive prompts')
|
|
@@ -33,6 +33,34 @@ export async function buildStaticSite(portfolio, options) {
|
|
|
33
33
|
return { outputDir, files };
|
|
34
34
|
}
|
|
35
35
|
// =============================================================================
|
|
36
|
+
// In-Memory Build Function (for dev mode)
|
|
37
|
+
// =============================================================================
|
|
38
|
+
export async function buildStaticSiteInMemory(portfolio, options) {
|
|
39
|
+
const theme = options.theme || portfolio.theme || 'srcl';
|
|
40
|
+
const themePath = path.join(import.meta.dirname, 'themes', theme);
|
|
41
|
+
const files = new Map();
|
|
42
|
+
const fileList = [];
|
|
43
|
+
// 1. Copy theme assets (CSS, JS)
|
|
44
|
+
const assetFiles = await copyThemeAssetsInMemory(themePath, portfolio.settings, files);
|
|
45
|
+
fileList.push(...assetFiles);
|
|
46
|
+
// 2. Copy fonts
|
|
47
|
+
const fontFiles = await copyFontsInMemory(files);
|
|
48
|
+
fileList.push(...fontFiles);
|
|
49
|
+
// 3. Generate pages based on portfolio structure
|
|
50
|
+
const hasRichProjects = (portfolio.projects?.length ?? 0) > 0;
|
|
51
|
+
if (hasRichProjects) {
|
|
52
|
+
// Multi-page site with project case studies
|
|
53
|
+
const pageFiles = await generateMultiPageSiteInMemory(portfolio, themePath, theme, files);
|
|
54
|
+
fileList.push(...pageFiles);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
// Single-page site (backwards compatible)
|
|
58
|
+
const pageFiles = await generateSinglePageSiteInMemory(portfolio, themePath, theme, files);
|
|
59
|
+
fileList.push(...pageFiles);
|
|
60
|
+
}
|
|
61
|
+
return { files, fileList };
|
|
62
|
+
}
|
|
63
|
+
// =============================================================================
|
|
36
64
|
// Directory Structure
|
|
37
65
|
// =============================================================================
|
|
38
66
|
async function createDirectoryStructure(outputDir) {
|
|
@@ -554,3 +582,255 @@ async function fileExists(filePath) {
|
|
|
554
582
|
return false;
|
|
555
583
|
}
|
|
556
584
|
}
|
|
585
|
+
// =============================================================================
|
|
586
|
+
// In-Memory Build Helpers
|
|
587
|
+
// =============================================================================
|
|
588
|
+
async function copyThemeAssetsInMemory(themePath, settings, files) {
|
|
589
|
+
const fileList = [];
|
|
590
|
+
// Copy CSS
|
|
591
|
+
const cssPath = path.join(themePath, 'styles.css');
|
|
592
|
+
if (await fileExists(cssPath)) {
|
|
593
|
+
const content = await fs.readFile(cssPath);
|
|
594
|
+
files.set('assets/styles.css', content);
|
|
595
|
+
fileList.push('assets/styles.css');
|
|
596
|
+
}
|
|
597
|
+
// Copy JS
|
|
598
|
+
const jsPath = path.join(themePath, 'script.js');
|
|
599
|
+
if (await fileExists(jsPath)) {
|
|
600
|
+
const content = await fs.readFile(jsPath);
|
|
601
|
+
files.set('assets/script.js', content);
|
|
602
|
+
fileList.push('assets/script.js');
|
|
603
|
+
}
|
|
604
|
+
return fileList;
|
|
605
|
+
}
|
|
606
|
+
async function copyFontsInMemory(files) {
|
|
607
|
+
const fileList = [];
|
|
608
|
+
const srcFontDir = '/Users/louanne/www-sacred/public/fonts';
|
|
609
|
+
try {
|
|
610
|
+
const fontFiles = await fs.readdir(srcFontDir);
|
|
611
|
+
for (const file of fontFiles) {
|
|
612
|
+
if (file.endsWith('.woff') ||
|
|
613
|
+
file.endsWith('.woff2') ||
|
|
614
|
+
file.endsWith('.ttf') ||
|
|
615
|
+
file.endsWith('.otf') ||
|
|
616
|
+
file.endsWith('.css')) {
|
|
617
|
+
const content = await fs.readFile(path.join(srcFontDir, file));
|
|
618
|
+
files.set(`assets/fonts/${file}`, content);
|
|
619
|
+
fileList.push(`assets/fonts/${file}`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
catch {
|
|
624
|
+
// Fonts directory doesn't exist or isn't accessible - that's fine
|
|
625
|
+
}
|
|
626
|
+
return fileList;
|
|
627
|
+
}
|
|
628
|
+
async function generateSinglePageSiteInMemory(portfolio, themePath, theme, files) {
|
|
629
|
+
const fileList = [];
|
|
630
|
+
// Load template and partials
|
|
631
|
+
const template = await fs.readFile(path.join(themePath, 'template.html'), 'utf-8');
|
|
632
|
+
const partials = await loadPartials(themePath);
|
|
633
|
+
// Prepare template data
|
|
634
|
+
const templateData = prepareTemplateData(portfolio);
|
|
635
|
+
// Render each partial
|
|
636
|
+
const renderedPartials = renderPartials(partials, templateData);
|
|
637
|
+
// Render main template with settings
|
|
638
|
+
const settings = portfolio.settings || {};
|
|
639
|
+
const defaultColorScheme = theme === 'dark-academia' ? 'light' : 'dark';
|
|
640
|
+
const html = Mustache.render(template, {
|
|
641
|
+
...templateData,
|
|
642
|
+
...renderedPartials,
|
|
643
|
+
colorScheme: settings.color_scheme || defaultColorScheme,
|
|
644
|
+
showGrid: settings.show_grid || false,
|
|
645
|
+
enableHotkeys: settings.enable_hotkeys !== false,
|
|
646
|
+
animate: settings.animate || 'subtle',
|
|
647
|
+
hasExperience: (portfolio.sections.experience?.length ?? 0) > 0,
|
|
648
|
+
hasProjects: ((portfolio.projects?.length ?? 0) > 0) || ((portfolio.sections.projects?.length ?? 0) > 0),
|
|
649
|
+
hasExperiments: (portfolio.experiments?.length ?? 0) > 0,
|
|
650
|
+
hasSkills: portfolio.sections.skills && Object.keys(portfolio.sections.skills).length > 0,
|
|
651
|
+
hasWriting: (portfolio.sections.writing?.length ?? 0) > 0,
|
|
652
|
+
hasEducation: (portfolio.sections.education?.length ?? 0) > 0,
|
|
653
|
+
});
|
|
654
|
+
files.set('index.html', Buffer.from(html, 'utf-8'));
|
|
655
|
+
fileList.push('index.html');
|
|
656
|
+
return fileList;
|
|
657
|
+
}
|
|
658
|
+
async function generateMultiPageSiteInMemory(portfolio, themePath, theme, files) {
|
|
659
|
+
const fileList = [];
|
|
660
|
+
// Check if theme has multi-page templates
|
|
661
|
+
const hasMultiPageTemplates = await fileExists(path.join(themePath, 'templates/homepage.html'));
|
|
662
|
+
if (!hasMultiPageTemplates) {
|
|
663
|
+
return generateSinglePageSiteInMemory(portfolio, themePath, theme, files);
|
|
664
|
+
}
|
|
665
|
+
// Generate homepage
|
|
666
|
+
const homepageFiles = await generateHomepageInMemory(portfolio, themePath, theme, files);
|
|
667
|
+
fileList.push(...homepageFiles);
|
|
668
|
+
// Generate project pages
|
|
669
|
+
const projectFiles = await generateProjectPagesInMemory(portfolio, themePath, theme, files);
|
|
670
|
+
fileList.push(...projectFiles);
|
|
671
|
+
// Generate projects index
|
|
672
|
+
const projectIndexFiles = await generateProjectsIndexInMemory(portfolio, themePath, theme, files);
|
|
673
|
+
fileList.push(...projectIndexFiles);
|
|
674
|
+
// Generate experiments index
|
|
675
|
+
const experimentsIndexFiles = await generateExperimentsIndexInMemory(portfolio, themePath, theme, files);
|
|
676
|
+
fileList.push(...experimentsIndexFiles);
|
|
677
|
+
// Generate writing index
|
|
678
|
+
const writingIndexFiles = await generateWritingIndexInMemory(portfolio, themePath, theme, files);
|
|
679
|
+
fileList.push(...writingIndexFiles);
|
|
680
|
+
// Copy user images
|
|
681
|
+
await copyUserImagesInMemory(portfolio, files);
|
|
682
|
+
return fileList;
|
|
683
|
+
}
|
|
684
|
+
async function generateHomepageInMemory(portfolio, themePath, theme, files) {
|
|
685
|
+
const templatePath = path.join(themePath, 'templates/homepage.html');
|
|
686
|
+
if (!(await fileExists(templatePath))) {
|
|
687
|
+
return [];
|
|
688
|
+
}
|
|
689
|
+
const template = await fs.readFile(templatePath, 'utf-8');
|
|
690
|
+
const settings = portfolio.settings || {};
|
|
691
|
+
const defaultColorScheme = theme === 'dark-academia' ? 'light' : 'dark';
|
|
692
|
+
const data = {
|
|
693
|
+
name: portfolio.meta.name,
|
|
694
|
+
title: portfolio.meta.title,
|
|
695
|
+
location: portfolio.meta.location,
|
|
696
|
+
timezone: portfolio.meta.timezone,
|
|
697
|
+
email: portfolio.contact.email,
|
|
698
|
+
website: portfolio.contact.website,
|
|
699
|
+
github: portfolio.contact.github,
|
|
700
|
+
linkedin: portfolio.contact.linkedin,
|
|
701
|
+
twitter: portfolio.contact.twitter,
|
|
702
|
+
bio: portfolio.bio,
|
|
703
|
+
bio_html: parseBio(portfolio.bio),
|
|
704
|
+
about_short: portfolio.about?.short || '',
|
|
705
|
+
featured_projects: portfolio.projects?.filter((p) => p.featured) || [],
|
|
706
|
+
featured_writing: portfolio.sections.writing?.filter((w) => 'featured' in w && w.featured) || [],
|
|
707
|
+
show_experiments: portfolio.layout?.show_experiments,
|
|
708
|
+
experiments: portfolio.experiments?.slice(0, 4) || [],
|
|
709
|
+
colorScheme: settings.color_scheme || defaultColorScheme,
|
|
710
|
+
showGrid: settings.show_grid || false,
|
|
711
|
+
enableHotkeys: settings.enable_hotkeys !== false,
|
|
712
|
+
animate: settings.animate || 'subtle',
|
|
713
|
+
hasProjects: (portfolio.projects?.length ?? 0) > 0,
|
|
714
|
+
hasExperiments: (portfolio.experiments?.length ?? 0) > 0,
|
|
715
|
+
hasWriting: (portfolio.sections.writing?.length ?? 0) > 0,
|
|
716
|
+
};
|
|
717
|
+
const html = Mustache.render(template, data);
|
|
718
|
+
files.set('index.html', Buffer.from(html, 'utf-8'));
|
|
719
|
+
return ['index.html'];
|
|
720
|
+
}
|
|
721
|
+
async function generateProjectPagesInMemory(portfolio, themePath, theme, files) {
|
|
722
|
+
if (!portfolio.projects || portfolio.projects.length === 0) {
|
|
723
|
+
return [];
|
|
724
|
+
}
|
|
725
|
+
const templatePath = path.join(themePath, 'templates/project.html');
|
|
726
|
+
if (!(await fileExists(templatePath))) {
|
|
727
|
+
return [];
|
|
728
|
+
}
|
|
729
|
+
const template = await fs.readFile(templatePath, 'utf-8');
|
|
730
|
+
const partials = await loadProjectPartials(themePath);
|
|
731
|
+
const settings = portfolio.settings || {};
|
|
732
|
+
const defaultColorScheme = theme === 'dark-academia' ? 'light' : 'dark';
|
|
733
|
+
const fileList = [];
|
|
734
|
+
for (const project of portfolio.projects) {
|
|
735
|
+
const sectionsHtml = project.sections
|
|
736
|
+
.map((section) => renderContentSection(section, partials))
|
|
737
|
+
.join('\n');
|
|
738
|
+
const data = {
|
|
739
|
+
...project,
|
|
740
|
+
sections_html: sectionsHtml,
|
|
741
|
+
site_name: portfolio.meta.name,
|
|
742
|
+
nav_links: generateNavLinks(portfolio, true, 'project'),
|
|
743
|
+
colorScheme: settings.color_scheme || defaultColorScheme,
|
|
744
|
+
showGrid: settings.show_grid || false,
|
|
745
|
+
enableHotkeys: settings.enable_hotkeys !== false,
|
|
746
|
+
};
|
|
747
|
+
const html = Mustache.render(template, data);
|
|
748
|
+
const filename = `projects/${project.id}.html`;
|
|
749
|
+
files.set(filename, Buffer.from(html, 'utf-8'));
|
|
750
|
+
fileList.push(filename);
|
|
751
|
+
}
|
|
752
|
+
return fileList;
|
|
753
|
+
}
|
|
754
|
+
async function generateProjectsIndexInMemory(portfolio, themePath, theme, files) {
|
|
755
|
+
const templatePath = path.join(themePath, 'templates/projects-index.html');
|
|
756
|
+
if (!(await fileExists(templatePath))) {
|
|
757
|
+
return [];
|
|
758
|
+
}
|
|
759
|
+
const template = await fs.readFile(templatePath, 'utf-8');
|
|
760
|
+
const settings = portfolio.settings || {};
|
|
761
|
+
const defaultColorScheme = theme === 'dark-academia' ? 'light' : 'dark';
|
|
762
|
+
const data = {
|
|
763
|
+
site_name: portfolio.meta.name,
|
|
764
|
+
projects: portfolio.projects || [],
|
|
765
|
+
colorScheme: settings.color_scheme || defaultColorScheme,
|
|
766
|
+
nav_links: generateNavLinks(portfolio, true, 'projects'),
|
|
767
|
+
};
|
|
768
|
+
const html = Mustache.render(template, data);
|
|
769
|
+
files.set('projects/index.html', Buffer.from(html, 'utf-8'));
|
|
770
|
+
return ['projects/index.html'];
|
|
771
|
+
}
|
|
772
|
+
async function generateExperimentsIndexInMemory(portfolio, themePath, theme, files) {
|
|
773
|
+
if (!portfolio.experiments || portfolio.experiments.length === 0) {
|
|
774
|
+
return [];
|
|
775
|
+
}
|
|
776
|
+
const templatePath = path.join(themePath, 'templates/experiments-index.html');
|
|
777
|
+
if (!(await fileExists(templatePath))) {
|
|
778
|
+
return [];
|
|
779
|
+
}
|
|
780
|
+
const template = await fs.readFile(templatePath, 'utf-8');
|
|
781
|
+
const settings = portfolio.settings || {};
|
|
782
|
+
const defaultColorScheme = theme === 'dark-academia' ? 'light' : 'dark';
|
|
783
|
+
const data = {
|
|
784
|
+
site_name: portfolio.meta.name,
|
|
785
|
+
experiments: portfolio.experiments || [],
|
|
786
|
+
colorScheme: settings.color_scheme || defaultColorScheme,
|
|
787
|
+
nav_links: generateNavLinks(portfolio, true, 'experiments'),
|
|
788
|
+
};
|
|
789
|
+
const html = Mustache.render(template, data);
|
|
790
|
+
files.set('experiments/index.html', Buffer.from(html, 'utf-8'));
|
|
791
|
+
return ['experiments/index.html'];
|
|
792
|
+
}
|
|
793
|
+
async function generateWritingIndexInMemory(portfolio, themePath, theme, files) {
|
|
794
|
+
if (!portfolio.sections.writing || portfolio.sections.writing.length === 0) {
|
|
795
|
+
return [];
|
|
796
|
+
}
|
|
797
|
+
const templatePath = path.join(themePath, 'templates/writing-index.html');
|
|
798
|
+
if (!(await fileExists(templatePath))) {
|
|
799
|
+
return [];
|
|
800
|
+
}
|
|
801
|
+
const template = await fs.readFile(templatePath, 'utf-8');
|
|
802
|
+
const settings = portfolio.settings || {};
|
|
803
|
+
const defaultColorScheme = theme === 'dark-academia' ? 'light' : 'dark';
|
|
804
|
+
const data = {
|
|
805
|
+
site_name: portfolio.meta.name,
|
|
806
|
+
writing: portfolio.sections.writing || [],
|
|
807
|
+
colorScheme: settings.color_scheme || defaultColorScheme,
|
|
808
|
+
nav_links: generateNavLinks(portfolio, true, 'writing'),
|
|
809
|
+
};
|
|
810
|
+
const html = Mustache.render(template, data);
|
|
811
|
+
files.set('writing/index.html', Buffer.from(html, 'utf-8'));
|
|
812
|
+
return ['writing/index.html'];
|
|
813
|
+
}
|
|
814
|
+
async function copyUserImagesInMemory(portfolio, files) {
|
|
815
|
+
const imagePaths = extractAllImagePaths(portfolio);
|
|
816
|
+
for (const imgPath of imagePaths) {
|
|
817
|
+
if (!imgPath) {
|
|
818
|
+
continue;
|
|
819
|
+
}
|
|
820
|
+
// Skip absolute URLs
|
|
821
|
+
if (imgPath.startsWith('http://') || imgPath.startsWith('https://')) {
|
|
822
|
+
continue;
|
|
823
|
+
}
|
|
824
|
+
const srcPath = imgPath.startsWith('/')
|
|
825
|
+
? path.join(process.cwd(), imgPath)
|
|
826
|
+
: path.join(process.cwd(), imgPath);
|
|
827
|
+
try {
|
|
828
|
+
const content = await fs.readFile(srcPath);
|
|
829
|
+
files.set(imgPath, content);
|
|
830
|
+
}
|
|
831
|
+
catch {
|
|
832
|
+
// Image doesn't exist - that's okay, it might be a placeholder
|
|
833
|
+
console.warn(`Warning: Could not copy image ${imgPath}`);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
package/package.json
CHANGED