devfolio-page 0.1.3 → 0.2.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/README.md CHANGED
@@ -18,11 +18,11 @@ devfolio-page init
18
18
  cd my-portfolio
19
19
  nano portfolio.yaml
20
20
 
21
- # Generate the static site
22
- devfolio-page render
21
+ # Start development server with auto-rebuild
22
+ devfolio-page dev
23
23
 
24
- # Open in browser
25
- open site/index.html
24
+ # Or generate the static site once
25
+ devfolio-page render
26
26
  ```
27
27
 
28
28
  ## Folder Structure
@@ -48,6 +48,27 @@ Create a new portfolio folder with config and images directory.
48
48
  devfolio-page init
49
49
  ```
50
50
 
51
+ ### `devfolio-page dev`
52
+
53
+ Start development server with file watching and auto-rebuild.
54
+
55
+ ```bash
56
+ devfolio-page dev # Start server on port 3000
57
+ devfolio-page dev --port 8080 # Use custom port
58
+ devfolio-page dev --theme modern # Use a specific theme
59
+ ```
60
+
61
+ **Options:**
62
+
63
+ - `-p, --port <port>` - Port for dev server (default: 3000)
64
+ - `-t, --theme <theme>` - Theme to use (srcl, modern, dark-academia)
65
+ - `-o, --output <dir>` - Output directory (default: ./site)
66
+
67
+ The dev server will:
68
+ - Watch `portfolio.yaml` and `images/` for changes
69
+ - Automatically rebuild your site when files change
70
+ - Serve your site at `http://localhost:3000`
71
+
51
72
  ### `devfolio-page render`
52
73
 
53
74
  Generate a static website. Run from inside your portfolio folder.
@@ -0,0 +1,165 @@
1
+ import chalk from 'chalk';
2
+ import chokidar from 'chokidar';
3
+ import { existsSync } from 'fs';
4
+ import http from 'http';
5
+ import path from 'path';
6
+ import { renderCommand } from './render.js';
7
+ import fs from 'fs/promises';
8
+ let isBuilding = false;
9
+ let buildQueued = false;
10
+ async function rebuild(file, options) {
11
+ // If already building, queue another build
12
+ if (isBuilding) {
13
+ buildQueued = true;
14
+ return;
15
+ }
16
+ isBuilding = true;
17
+ try {
18
+ console.log();
19
+ console.log(chalk.dim(`[${new Date().toLocaleTimeString()}]`) + ' File changed: ' + chalk.cyan(file));
20
+ console.log(chalk.dim('Rebuilding...'));
21
+ console.log();
22
+ await renderCommand('portfolio.yaml', {
23
+ theme: options.theme,
24
+ output: options.output || './site',
25
+ });
26
+ console.log();
27
+ console.log(chalk.green('✓') + ' Rebuild complete!');
28
+ console.log();
29
+ }
30
+ catch (error) {
31
+ console.error(chalk.red('✗') + ' Build failed:');
32
+ console.error(error instanceof Error ? error.message : String(error));
33
+ console.log();
34
+ }
35
+ finally {
36
+ isBuilding = false;
37
+ // If a build was queued while we were building, run it now
38
+ if (buildQueued) {
39
+ buildQueued = false;
40
+ setTimeout(() => rebuild(file, options), 100);
41
+ }
42
+ }
43
+ }
44
+ function startServer(outputDir, port) {
45
+ const server = http.createServer(async (req, res) => {
46
+ try {
47
+ // Default to index.html
48
+ let filePath = req.url === '/' ? '/index.html' : req.url || '/index.html';
49
+ // Remove query string
50
+ filePath = filePath.split('?')[0];
51
+ const fullPath = path.join(outputDir, filePath);
52
+ // Security: prevent directory traversal
53
+ const normalizedPath = path.normalize(fullPath);
54
+ if (!normalizedPath.startsWith(path.normalize(outputDir))) {
55
+ res.writeHead(403);
56
+ res.end('Forbidden');
57
+ return;
58
+ }
59
+ // Check if file exists
60
+ if (!existsSync(fullPath)) {
61
+ res.writeHead(404);
62
+ res.end('Not found');
63
+ return;
64
+ }
65
+ // Read file
66
+ const content = await fs.readFile(fullPath);
67
+ // Set content type
68
+ const ext = path.extname(fullPath);
69
+ const contentTypes = {
70
+ '.html': 'text/html',
71
+ '.css': 'text/css',
72
+ '.js': 'application/javascript',
73
+ '.json': 'application/json',
74
+ '.png': 'image/png',
75
+ '.jpg': 'image/jpeg',
76
+ '.jpeg': 'image/jpeg',
77
+ '.gif': 'image/gif',
78
+ '.svg': 'image/svg+xml',
79
+ '.ico': 'image/x-icon',
80
+ '.woff': 'font/woff',
81
+ '.woff2': 'font/woff2',
82
+ '.ttf': 'font/ttf',
83
+ '.otf': 'font/otf',
84
+ };
85
+ res.writeHead(200, { 'Content-Type': contentTypes[ext] || 'application/octet-stream' });
86
+ res.end(content);
87
+ }
88
+ catch (error) {
89
+ console.error('Server error:', error);
90
+ res.writeHead(500);
91
+ res.end('Internal server error');
92
+ }
93
+ });
94
+ server.listen(port);
95
+ return server;
96
+ }
97
+ export async function devCommand(file = 'portfolio.yaml', options = {}) {
98
+ const port = options.port || 3000;
99
+ const outputDir = path.resolve(options.output || './site');
100
+ // Check if portfolio.yaml exists
101
+ if (!existsSync(file)) {
102
+ console.error(chalk.red('✗') + ` File not found: ${file}`);
103
+ console.log();
104
+ console.log('Make sure you are in a portfolio directory with a portfolio.yaml file.');
105
+ console.log('Run ' + chalk.cyan('devfolio-page init') + ' to create a new portfolio.');
106
+ process.exit(1);
107
+ }
108
+ console.log();
109
+ console.log(chalk.cyan('┌─────────────────────────────────────────────────────────┐'));
110
+ console.log(chalk.cyan('│') + ' ' + chalk.bold('devfolio.page') + ' - Development Mode ' + chalk.cyan('│'));
111
+ console.log(chalk.cyan('└─────────────────────────────────────────────────────────┘'));
112
+ console.log();
113
+ // Initial build
114
+ console.log(chalk.bold('Building initial site...'));
115
+ console.log();
116
+ try {
117
+ await renderCommand(file, {
118
+ theme: options.theme,
119
+ output: outputDir,
120
+ });
121
+ }
122
+ catch (error) {
123
+ console.error(chalk.red('✗') + ' Initial build failed:');
124
+ console.error(error instanceof Error ? error.message : String(error));
125
+ process.exit(1);
126
+ }
127
+ // Start server
128
+ const server = startServer(outputDir, port);
129
+ console.log();
130
+ console.log(chalk.green('✓') + ' Dev server started!');
131
+ console.log();
132
+ console.log(' ' + chalk.bold('Local:') + ' ' + chalk.cyan(`http://localhost:${port}`));
133
+ console.log(' ' + chalk.bold('Output:') + ' ' + chalk.dim(outputDir));
134
+ console.log();
135
+ console.log(chalk.dim('Watching for changes...'));
136
+ console.log(chalk.dim('Press Ctrl+C to stop'));
137
+ console.log();
138
+ // Watch for file changes
139
+ const watcher = chokidar.watch([file, 'images/**/*'], {
140
+ persistent: true,
141
+ ignoreInitial: true,
142
+ awaitWriteFinish: {
143
+ stabilityThreshold: 100,
144
+ pollInterval: 100,
145
+ },
146
+ });
147
+ watcher.on('change', (changedFile) => {
148
+ rebuild(changedFile, options);
149
+ });
150
+ watcher.on('add', (changedFile) => {
151
+ rebuild(changedFile, options);
152
+ });
153
+ watcher.on('unlink', (changedFile) => {
154
+ rebuild(changedFile, options);
155
+ });
156
+ // Handle process exit
157
+ process.on('SIGINT', () => {
158
+ console.log();
159
+ console.log();
160
+ console.log(chalk.dim('Shutting down...'));
161
+ watcher.close();
162
+ server.close();
163
+ process.exit(0);
164
+ });
165
+ }
package/dist/cli/index.js CHANGED
@@ -5,11 +5,12 @@ import { initCommand } from './commands/init.js';
5
5
  import { validateCommand } from './commands/validate.js';
6
6
  import { renderCommand } from './commands/render.js';
7
7
  import { themesCommand } from './commands/themes.js';
8
+ import { devCommand } from './commands/dev.js';
8
9
  const program = new Command();
9
10
  program
10
11
  .name('devfolio-page')
11
12
  .description('Your portfolio as code. Version control it like software.')
12
- .version('0.1.3');
13
+ .version('0.2.0');
13
14
  program
14
15
  .command('init')
15
16
  .description('Create a new portfolio.yaml with interactive prompts')
@@ -28,6 +29,13 @@ program
28
29
  .command('themes')
29
30
  .description('List available themes with examples')
30
31
  .action(themesCommand);
32
+ program
33
+ .command('dev [file]')
34
+ .description('Start development server with file watching')
35
+ .option('-p, --port <port>', 'Port for dev server', '3000')
36
+ .option('-t, --theme <theme>', 'Theme to use (defaults to YAML theme or srcl)')
37
+ .option('-o, --output <dir>', 'Output directory', './site')
38
+ .action((file, options) => devCommand(file || 'portfolio.yaml', options));
31
39
  // Show welcome message if no arguments provided
32
40
  if (process.argv.length === 2) {
33
41
  console.log();
@@ -38,6 +46,7 @@ if (process.argv.length === 2) {
38
46
  console.log(chalk.bold('Commands:'));
39
47
  console.log();
40
48
  console.log(' ' + chalk.cyan('devfolio-page init') + ' Create a new portfolio folder');
49
+ console.log(' ' + chalk.cyan('devfolio-page dev') + ' Start dev server with auto-rebuild');
41
50
  console.log(' ' + chalk.cyan('devfolio-page render') + ' Generate site (uses portfolio.yaml)');
42
51
  console.log(' ' + chalk.cyan('devfolio-page themes') + ' List available themes');
43
52
  console.log(' ' + chalk.cyan('devfolio-page validate') + ' <file> Validate a portfolio YAML file');
@@ -11,6 +11,7 @@ log();
11
11
  log(chalk.bold('Available commands:'));
12
12
  log();
13
13
  log(' ' + chalk.cyan('devfolio-page init') + ' Create a new portfolio folder');
14
+ log(' ' + chalk.cyan('devfolio-page dev') + ' Start dev server with auto-rebuild');
14
15
  log(' ' + chalk.cyan('devfolio-page render') + ' Generate static site from YAML');
15
16
  log(' ' + chalk.cyan('devfolio-page validate') + ' <file> Validate a portfolio YAML file');
16
17
  log(' ' + chalk.cyan('devfolio-page --help') + ' Show all options');
@@ -474,6 +474,10 @@ async function copyFonts(outputDir) {
474
474
  async function copyUserImages(portfolio, outputDir) {
475
475
  const imagePaths = extractAllImagePaths(portfolio);
476
476
  for (const imgPath of imagePaths) {
477
+ // Skip undefined or empty paths
478
+ if (!imgPath) {
479
+ continue;
480
+ }
477
481
  // Skip absolute URLs
478
482
  if (imgPath.startsWith('http://') || imgPath.startsWith('https://')) {
479
483
  continue;
@@ -503,7 +507,8 @@ function extractAllImagePaths(portfolio) {
503
507
  paths.push(meta.hero_image);
504
508
  // Extract from rich projects
505
509
  portfolio.projects?.forEach((project) => {
506
- paths.push(project.thumbnail);
510
+ if (project.thumbnail)
511
+ paths.push(project.thumbnail);
507
512
  if (project.hero)
508
513
  paths.push(project.hero);
509
514
  project.sections.forEach((section) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devfolio-page",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "Your portfolio as code. Version control it like software.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -41,6 +41,7 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "chalk": "^5.6.2",
44
+ "chokidar": "^5.0.0",
44
45
  "commander": "^14.0.2",
45
46
  "js-yaml": "^4.1.1",
46
47
  "marked": "^17.0.1",