devfolio-page 0.2.0 → 0.2.1

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.
@@ -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 { renderCommand } from './render.js';
7
- import fs from 'fs/promises';
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,14 +18,17 @@ 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
- await renderCommand('portfolio.yaml', {
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('✓') + ' Rebuild complete!');
31
+ console.log(chalk.green('✓') + ` Rebuilt ${result.fileList.length} files in memory`);
28
32
  console.log();
29
33
  }
30
34
  catch (error) {
@@ -41,31 +45,23 @@ async function rebuild(file, options) {
41
45
  }
42
46
  }
43
47
  }
44
- function startServer(outputDir, port) {
48
+ function startServer(port) {
45
49
  const server = http.createServer(async (req, res) => {
46
50
  try {
47
51
  // 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)) {
52
+ let filePath = req.url === '/' ? 'index.html' : (req.url || 'index.html');
53
+ // Remove leading slash and query string
54
+ filePath = filePath.replace(/^\//, '').split('?')[0];
55
+ // Check if file exists in cache
56
+ if (!siteCache.has(filePath)) {
61
57
  res.writeHead(404);
62
58
  res.end('Not found');
63
59
  return;
64
60
  }
65
- // Read file
66
- const content = await fs.readFile(fullPath);
67
- // Set content type
68
- const ext = path.extname(fullPath);
61
+ // Get file from cache
62
+ const content = siteCache.get(filePath);
63
+ // Set content type based on file extension
64
+ const ext = path.extname(filePath);
69
65
  const contentTypes = {
70
66
  '.html': 'text/html',
71
67
  '.css': 'text/css',
@@ -96,7 +92,6 @@ function startServer(outputDir, port) {
96
92
  }
97
93
  export async function devCommand(file = 'portfolio.yaml', options = {}) {
98
94
  const port = options.port || 3000;
99
- const outputDir = path.resolve(options.output || './site');
100
95
  // Check if portfolio.yaml exists
101
96
  if (!existsSync(file)) {
102
97
  console.error(chalk.red('✗') + ` File not found: ${file}`);
@@ -111,13 +106,15 @@ export async function devCommand(file = 'portfolio.yaml', options = {}) {
111
106
  console.log(chalk.cyan('└─────────────────────────────────────────────────────────┘'));
112
107
  console.log();
113
108
  // Initial build
114
- console.log(chalk.bold('Building initial site...'));
109
+ console.log(chalk.bold('Building initial site in memory...'));
115
110
  console.log();
116
111
  try {
117
- await renderCommand(file, {
112
+ const portfolio = validatePortfolio(file);
113
+ const result = await buildStaticSiteInMemory(portfolio, {
118
114
  theme: options.theme,
119
- output: outputDir,
120
115
  });
116
+ siteCache = result.files;
117
+ console.log(chalk.green(' ✓') + ` Built ${result.fileList.length} files`);
121
118
  }
122
119
  catch (error) {
123
120
  console.error(chalk.red('✗') + ' Initial build failed:');
@@ -125,12 +122,12 @@ export async function devCommand(file = 'portfolio.yaml', options = {}) {
125
122
  process.exit(1);
126
123
  }
127
124
  // Start server
128
- const server = startServer(outputDir, port);
125
+ const server = startServer(port);
129
126
  console.log();
130
127
  console.log(chalk.green('✓') + ' Dev server started!');
131
128
  console.log();
132
129
  console.log(' ' + chalk.bold('Local:') + ' ' + chalk.cyan(`http://localhost:${port}`));
133
- console.log(' ' + chalk.bold('Output:') + ' ' + chalk.dim(outputDir));
130
+ console.log(' ' + chalk.bold('Mode:') + ' ' + chalk.dim('In-memory (no site/ folder)'));
134
131
  console.log();
135
132
  console.log(chalk.dim('Watching for changes...'));
136
133
  console.log(chalk.dim('Press Ctrl+C to stop'));
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.0');
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,254 @@ 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.sections.projects?.length ?? 0) > 0,
649
+ hasSkills: portfolio.sections.skills && Object.keys(portfolio.sections.skills).length > 0,
650
+ hasWriting: (portfolio.sections.writing?.length ?? 0) > 0,
651
+ hasEducation: (portfolio.sections.education?.length ?? 0) > 0,
652
+ });
653
+ files.set('index.html', Buffer.from(html, 'utf-8'));
654
+ fileList.push('index.html');
655
+ return fileList;
656
+ }
657
+ async function generateMultiPageSiteInMemory(portfolio, themePath, theme, files) {
658
+ const fileList = [];
659
+ // Check if theme has multi-page templates
660
+ const hasMultiPageTemplates = await fileExists(path.join(themePath, 'templates/homepage.html'));
661
+ if (!hasMultiPageTemplates) {
662
+ return generateSinglePageSiteInMemory(portfolio, themePath, theme, files);
663
+ }
664
+ // Generate homepage
665
+ const homepageFiles = await generateHomepageInMemory(portfolio, themePath, theme, files);
666
+ fileList.push(...homepageFiles);
667
+ // Generate project pages
668
+ const projectFiles = await generateProjectPagesInMemory(portfolio, themePath, theme, files);
669
+ fileList.push(...projectFiles);
670
+ // Generate projects index
671
+ const projectIndexFiles = await generateProjectsIndexInMemory(portfolio, themePath, theme, files);
672
+ fileList.push(...projectIndexFiles);
673
+ // Generate experiments index
674
+ const experimentsIndexFiles = await generateExperimentsIndexInMemory(portfolio, themePath, theme, files);
675
+ fileList.push(...experimentsIndexFiles);
676
+ // Generate writing index
677
+ const writingIndexFiles = await generateWritingIndexInMemory(portfolio, themePath, theme, files);
678
+ fileList.push(...writingIndexFiles);
679
+ // Copy user images
680
+ await copyUserImagesInMemory(portfolio, files);
681
+ return fileList;
682
+ }
683
+ async function generateHomepageInMemory(portfolio, themePath, theme, files) {
684
+ const templatePath = path.join(themePath, 'templates/homepage.html');
685
+ if (!(await fileExists(templatePath))) {
686
+ return [];
687
+ }
688
+ const template = await fs.readFile(templatePath, 'utf-8');
689
+ const settings = portfolio.settings || {};
690
+ const defaultColorScheme = theme === 'dark-academia' ? 'light' : 'dark';
691
+ const data = {
692
+ name: portfolio.meta.name,
693
+ title: portfolio.meta.title,
694
+ location: portfolio.meta.location,
695
+ timezone: portfolio.meta.timezone,
696
+ email: portfolio.contact.email,
697
+ website: portfolio.contact.website,
698
+ github: portfolio.contact.github,
699
+ linkedin: portfolio.contact.linkedin,
700
+ twitter: portfolio.contact.twitter,
701
+ bio: portfolio.bio,
702
+ bio_html: parseBio(portfolio.bio),
703
+ about_short: portfolio.about?.short || '',
704
+ featured_projects: portfolio.projects?.filter((p) => p.featured) || [],
705
+ featured_writing: portfolio.sections.writing?.filter((w) => 'featured' in w && w.featured) || [],
706
+ show_experiments: portfolio.layout?.show_experiments,
707
+ experiments: portfolio.experiments?.slice(0, 4) || [],
708
+ colorScheme: settings.color_scheme || defaultColorScheme,
709
+ showGrid: settings.show_grid || false,
710
+ enableHotkeys: settings.enable_hotkeys !== false,
711
+ animate: settings.animate || 'subtle',
712
+ hasProjects: (portfolio.projects?.length ?? 0) > 0,
713
+ hasExperiments: (portfolio.experiments?.length ?? 0) > 0,
714
+ hasWriting: (portfolio.sections.writing?.length ?? 0) > 0,
715
+ };
716
+ const html = Mustache.render(template, data);
717
+ files.set('index.html', Buffer.from(html, 'utf-8'));
718
+ return ['index.html'];
719
+ }
720
+ async function generateProjectPagesInMemory(portfolio, themePath, theme, files) {
721
+ if (!portfolio.projects || portfolio.projects.length === 0) {
722
+ return [];
723
+ }
724
+ const templatePath = path.join(themePath, 'templates/project.html');
725
+ if (!(await fileExists(templatePath))) {
726
+ return [];
727
+ }
728
+ const template = await fs.readFile(templatePath, 'utf-8');
729
+ const partials = await loadProjectPartials(themePath);
730
+ const settings = portfolio.settings || {};
731
+ const defaultColorScheme = theme === 'dark-academia' ? 'light' : 'dark';
732
+ const fileList = [];
733
+ for (const project of portfolio.projects) {
734
+ const sectionsHtml = project.sections
735
+ .map((section) => renderContentSection(section, partials))
736
+ .join('\n');
737
+ const data = {
738
+ ...project,
739
+ sections_html: sectionsHtml,
740
+ site_name: portfolio.meta.name,
741
+ nav_links: generateNavLinks(portfolio, true, 'project'),
742
+ colorScheme: settings.color_scheme || defaultColorScheme,
743
+ showGrid: settings.show_grid || false,
744
+ enableHotkeys: settings.enable_hotkeys !== false,
745
+ };
746
+ const html = Mustache.render(template, data);
747
+ const filename = `projects/${project.id}.html`;
748
+ files.set(filename, Buffer.from(html, 'utf-8'));
749
+ fileList.push(filename);
750
+ }
751
+ return fileList;
752
+ }
753
+ async function generateProjectsIndexInMemory(portfolio, themePath, theme, files) {
754
+ const templatePath = path.join(themePath, 'templates/projects-index.html');
755
+ if (!(await fileExists(templatePath))) {
756
+ return [];
757
+ }
758
+ const template = await fs.readFile(templatePath, 'utf-8');
759
+ const settings = portfolio.settings || {};
760
+ const defaultColorScheme = theme === 'dark-academia' ? 'light' : 'dark';
761
+ const data = {
762
+ site_name: portfolio.meta.name,
763
+ projects: portfolio.projects || [],
764
+ colorScheme: settings.color_scheme || defaultColorScheme,
765
+ nav_links: generateNavLinks(portfolio, true, 'projects'),
766
+ };
767
+ const html = Mustache.render(template, data);
768
+ files.set('projects/index.html', Buffer.from(html, 'utf-8'));
769
+ return ['projects/index.html'];
770
+ }
771
+ async function generateExperimentsIndexInMemory(portfolio, themePath, theme, files) {
772
+ if (!portfolio.experiments || portfolio.experiments.length === 0) {
773
+ return [];
774
+ }
775
+ const templatePath = path.join(themePath, 'templates/experiments-index.html');
776
+ if (!(await fileExists(templatePath))) {
777
+ return [];
778
+ }
779
+ const template = await fs.readFile(templatePath, 'utf-8');
780
+ const settings = portfolio.settings || {};
781
+ const defaultColorScheme = theme === 'dark-academia' ? 'light' : 'dark';
782
+ const data = {
783
+ site_name: portfolio.meta.name,
784
+ experiments: portfolio.experiments || [],
785
+ colorScheme: settings.color_scheme || defaultColorScheme,
786
+ nav_links: generateNavLinks(portfolio, true, 'experiments'),
787
+ };
788
+ const html = Mustache.render(template, data);
789
+ files.set('experiments/index.html', Buffer.from(html, 'utf-8'));
790
+ return ['experiments/index.html'];
791
+ }
792
+ async function generateWritingIndexInMemory(portfolio, themePath, theme, files) {
793
+ if (!portfolio.sections.writing || portfolio.sections.writing.length === 0) {
794
+ return [];
795
+ }
796
+ const templatePath = path.join(themePath, 'templates/writing-index.html');
797
+ if (!(await fileExists(templatePath))) {
798
+ return [];
799
+ }
800
+ const template = await fs.readFile(templatePath, 'utf-8');
801
+ const settings = portfolio.settings || {};
802
+ const defaultColorScheme = theme === 'dark-academia' ? 'light' : 'dark';
803
+ const data = {
804
+ site_name: portfolio.meta.name,
805
+ writing: portfolio.sections.writing || [],
806
+ colorScheme: settings.color_scheme || defaultColorScheme,
807
+ nav_links: generateNavLinks(portfolio, true, 'writing'),
808
+ };
809
+ const html = Mustache.render(template, data);
810
+ files.set('writing/index.html', Buffer.from(html, 'utf-8'));
811
+ return ['writing/index.html'];
812
+ }
813
+ async function copyUserImagesInMemory(portfolio, files) {
814
+ const imagePaths = extractAllImagePaths(portfolio);
815
+ for (const imgPath of imagePaths) {
816
+ if (!imgPath) {
817
+ continue;
818
+ }
819
+ // Skip absolute URLs
820
+ if (imgPath.startsWith('http://') || imgPath.startsWith('https://')) {
821
+ continue;
822
+ }
823
+ const srcPath = imgPath.startsWith('/')
824
+ ? path.join(process.cwd(), imgPath)
825
+ : path.join(process.cwd(), imgPath);
826
+ try {
827
+ const content = await fs.readFile(srcPath);
828
+ files.set(imgPath, content);
829
+ }
830
+ catch {
831
+ // Image doesn't exist - that's okay, it might be a placeholder
832
+ console.warn(`Warning: Could not copy image ${imgPath}`);
833
+ }
834
+ }
835
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devfolio-page",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Your portfolio as code. Version control it like software.",
5
5
  "type": "module",
6
6
  "bin": {