uniweb 0.3.2 → 0.3.4

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
@@ -13,6 +13,20 @@ pnpm install
13
13
  pnpm dev
14
14
  ```
15
15
 
16
+ Open http://localhost:5173 to see your site. Edit files in `site/pages/` and `foundation/src/components/` to see changes instantly.
17
+
18
+ ### Development Commands
19
+
20
+ Run these from the **project root** (where `pnpm-workspace.yaml` is):
21
+
22
+ ```bash
23
+ pnpm dev # Start development server
24
+ pnpm build # Build foundation + site for production
25
+ pnpm preview # Preview the production build
26
+ ```
27
+
28
+ The `build` command generates static HTML in `site/dist/`. Open those files to verify your output before deploying.
29
+
16
30
  The `marketing` template includes real components (Hero, Features, Pricing, Testimonials, FAQ, and more) with sample content—a working site you can explore and modify.
17
31
 
18
32
  **Other templates:**
@@ -92,7 +106,16 @@ role: Lead Architect
92
106
  ```
93
107
  ````
94
108
 
95
- Components receive validated, localized data. Natural content stays in markdown; structured data goes in tagged blocks (YAML or JSON).
109
+ Access the parsed data via `content.data`:
110
+
111
+ ```jsx
112
+ function TeamCard({ content }) {
113
+ const member = content.data['team-member']
114
+ return <div>{member.name} — {member.role}</div>
115
+ }
116
+ ```
117
+
118
+ Natural content stays in markdown; structured data goes in tagged blocks (YAML or JSON).
96
119
 
97
120
  ### Components as React
98
121
 
@@ -634,6 +657,76 @@ Yes. Content is pre-embedded in the initial HTML—no fetch waterfalls, no layou
634
657
 
635
658
  Pages can define data sources that auto-generate subroutes. A `/blog` page can have an index and a `[slug]` template that renders each post.
636
659
 
660
+ ## Common Gotchas
661
+
662
+ ### Homepage Configuration
663
+
664
+ Set your homepage with `index:` in `site.yml`:
665
+
666
+ ```yaml
667
+ # site.yml
668
+ index: home # The page folder that becomes /
669
+ ```
670
+
671
+ The `index:` option tells the build which page folder becomes the root route (`/`). The page still exists in `pages/home/`, but visitors access it at `/`.
672
+
673
+ Don't confuse this with `pages:` (which explicitly lists pages and hides any not listed).
674
+
675
+ ### Content Shapes
676
+
677
+ Lists and items in markdown become **objects**, not strings. A bullet list:
678
+
679
+ ```markdown
680
+ - First item
681
+ - Second item
682
+ ```
683
+
684
+ Becomes:
685
+
686
+ ```js
687
+ content.items = [
688
+ { title: "First item", paragraphs: [], links: [], imgs: [] },
689
+ { title: "Second item", paragraphs: [], links: [], imgs: [] }
690
+ ]
691
+ ```
692
+
693
+ Each item has the same structure as a content block. See [Content Structure](./docs/content-structure.md) for the full shape.
694
+
695
+ ### Tagged Data Blocks
696
+
697
+ Tagged code blocks like:
698
+
699
+ ````markdown
700
+ ```yaml:team-member
701
+ name: Sarah Chen
702
+ role: Lead Architect
703
+ ```
704
+ ````
705
+
706
+ Are accessible via `content.data`:
707
+
708
+ ```jsx
709
+ function TeamMember({ content }) {
710
+ const member = content.data['team-member']
711
+ return <div>{member.name} - {member.role}</div>
712
+ }
713
+ ```
714
+
715
+ The tag name (after the colon) becomes the key in `content.data`.
716
+
717
+ ### Root vs Package Commands
718
+
719
+ In a Uniweb workspace, commands run differently at different levels:
720
+
721
+ | Location | Command | What it does |
722
+ |----------|---------|--------------|
723
+ | Project root | `pnpm build` | Builds all packages (foundation + site) |
724
+ | Project root | `pnpm dev` | Starts dev server for site |
725
+ | `foundation/` | `uniweb build` | Builds just the foundation |
726
+ | `site/` | `uniweb build` | Builds just the site |
727
+
728
+ For day-to-day development, run `pnpm dev` from the project root. The workspace scripts handle the rest.
729
+
637
730
  ## Related Packages
638
731
 
639
732
  - [`@uniweb/build`](https://github.com/uniweb/build) — Foundation build tooling
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uniweb",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Create structured Vite + React sites with content/code separation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -37,8 +37,8 @@
37
37
  "js-yaml": "^4.1.0",
38
38
  "prompts": "^2.4.2",
39
39
  "tar": "^7.0.0",
40
- "@uniweb/build": "0.2.1",
41
- "@uniweb/core": "0.2.1",
40
+ "@uniweb/build": "0.2.2",
41
+ "@uniweb/core": "0.2.2",
42
42
  "@uniweb/runtime": "0.3.0",
43
43
  "@uniweb/kit": "0.2.1"
44
44
  }
@@ -10,7 +10,7 @@
10
10
  * uniweb build --prerender # Build site + pre-render to static HTML (SSG)
11
11
  */
12
12
 
13
- import { existsSync } from 'node:fs'
13
+ import { existsSync, readFileSync, readdirSync } from 'node:fs'
14
14
  import { resolve, join } from 'node:path'
15
15
  import { spawn } from 'node:child_process'
16
16
  import { writeFile, mkdir } from 'node:fs/promises'
@@ -53,22 +53,51 @@ function info(message) {
53
53
 
54
54
  /**
55
55
  * Detect project type based on directory contents
56
+ *
57
+ * Detection priority:
58
+ * 1. foundation.js → foundation
59
+ * 2. site.yml → site
60
+ * 3. src/components/ → foundation (fallback)
61
+ * 4. pages/ → site (fallback)
62
+ * 5. pnpm-workspace.yaml or package.json workspaces → workspace
56
63
  */
57
64
  function detectProjectType(projectDir) {
58
- const srcDir = join(projectDir, 'src')
59
- const componentsDir = join(srcDir, 'components')
60
- const pagesDir = join(projectDir, 'pages')
65
+ // Primary detection: config files
66
+ if (existsSync(join(projectDir, 'src', 'foundation.js'))) {
67
+ return 'foundation'
68
+ }
61
69
 
62
- // Foundation: has src/components/ with component folders containing meta.js
63
- if (existsSync(componentsDir)) {
70
+ if (existsSync(join(projectDir, 'site.yml'))) {
71
+ return 'site'
72
+ }
73
+
74
+ // Fallback detection: directory structure
75
+ if (existsSync(join(projectDir, 'src', 'components'))) {
64
76
  return 'foundation'
65
77
  }
66
78
 
67
- // Site: has pages/ directory
68
- if (existsSync(pagesDir)) {
79
+ if (existsSync(join(projectDir, 'pages'))) {
69
80
  return 'site'
70
81
  }
71
82
 
83
+ // Workspace: has pnpm-workspace.yaml or package.json with workspaces
84
+ if (existsSync(join(projectDir, 'pnpm-workspace.yaml'))) {
85
+ return 'workspace'
86
+ }
87
+
88
+ // Check package.json for workspaces field
89
+ const packageJsonPath = join(projectDir, 'package.json')
90
+ if (existsSync(packageJsonPath)) {
91
+ try {
92
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'))
93
+ if (packageJson.workspaces) {
94
+ return 'workspace'
95
+ }
96
+ } catch {
97
+ // Ignore parse errors
98
+ }
99
+ }
100
+
72
101
  return null
73
102
  }
74
103
 
@@ -395,6 +424,123 @@ async function buildSite(projectDir, options = {}) {
395
424
  }
396
425
  }
397
426
 
427
+ /**
428
+ * Check if a directory is a foundation
429
+ */
430
+ function isFoundation(dir) {
431
+ // Primary: has foundation.js config
432
+ if (existsSync(join(dir, 'src', 'foundation.js'))) return true
433
+ // Fallback: has src/components/
434
+ if (existsSync(join(dir, 'src', 'components'))) return true
435
+ return false
436
+ }
437
+
438
+ /**
439
+ * Check if a directory is a site
440
+ */
441
+ function isSite(dir) {
442
+ // Primary: has site.yml config
443
+ if (existsSync(join(dir, 'site.yml'))) return true
444
+ // Fallback: has pages/
445
+ if (existsSync(join(dir, 'pages'))) return true
446
+ return false
447
+ }
448
+
449
+ /**
450
+ * Discover workspace packages based on workspace config
451
+ */
452
+ function discoverWorkspacePackages(workspaceDir) {
453
+ const foundations = []
454
+ const sites = []
455
+
456
+ // Check standard locations
457
+ const standardFoundation = join(workspaceDir, 'foundation')
458
+ const standardSite = join(workspaceDir, 'site')
459
+
460
+ if (existsSync(standardFoundation) && isFoundation(standardFoundation)) {
461
+ foundations.push({ name: 'foundation', path: standardFoundation })
462
+ }
463
+
464
+ if (existsSync(standardSite) && isSite(standardSite)) {
465
+ sites.push({ name: 'site', path: standardSite })
466
+ }
467
+
468
+ // Check multi-site locations (foundations/*, sites/*)
469
+ const foundationsDir = join(workspaceDir, 'foundations')
470
+ const sitesDir = join(workspaceDir, 'sites')
471
+
472
+ if (existsSync(foundationsDir)) {
473
+ for (const name of readdirSync(foundationsDir)) {
474
+ const path = join(foundationsDir, name)
475
+ if (isFoundation(path)) {
476
+ foundations.push({ name, path })
477
+ }
478
+ }
479
+ }
480
+
481
+ if (existsSync(sitesDir)) {
482
+ for (const name of readdirSync(sitesDir)) {
483
+ const path = join(sitesDir, name)
484
+ if (isSite(path)) {
485
+ sites.push({ name, path })
486
+ }
487
+ }
488
+ }
489
+
490
+ return { foundations, sites }
491
+ }
492
+
493
+ /**
494
+ * Build all packages in a workspace
495
+ */
496
+ async function buildWorkspace(workspaceDir, options = {}) {
497
+ const { prerenderFlag, noPrerenderFlag } = options
498
+
499
+ log(`${colors.cyan}${colors.bright}Building workspace...${colors.reset}`)
500
+ log('')
501
+
502
+ const { foundations, sites } = discoverWorkspacePackages(workspaceDir)
503
+
504
+ if (foundations.length === 0 && sites.length === 0) {
505
+ error('No foundations or sites found in workspace')
506
+ log('')
507
+ log('Expected structure:')
508
+ log(' foundation/ or foundations/*/')
509
+ log(' site/ or sites/*/')
510
+ process.exit(1)
511
+ }
512
+
513
+ // Build foundations first (sites depend on them)
514
+ for (const foundation of foundations) {
515
+ log(`${colors.bright}[${foundation.name}]${colors.reset}`)
516
+ await buildFoundation(foundation.path)
517
+ log('')
518
+ }
519
+
520
+ // Build sites
521
+ for (const site of sites) {
522
+ log(`${colors.bright}[${site.name}]${colors.reset}`)
523
+
524
+ const siteConfig = readSiteConfig(site.path)
525
+ const configPrerender = siteConfig.build?.prerender === true
526
+
527
+ let prerender = configPrerender
528
+ if (prerenderFlag) prerender = true
529
+ if (noPrerenderFlag) prerender = false
530
+
531
+ // Resolve foundation directory for this site
532
+ const foundationDir = resolveFoundationDir(site.path, siteConfig)
533
+
534
+ await buildSite(site.path, { prerender, foundationDir, siteConfig })
535
+ log('')
536
+ }
537
+
538
+ // Summary
539
+ log(`${colors.green}${colors.bright}Workspace build complete!${colors.reset}`)
540
+ log('')
541
+ log(`Built ${foundations.length} foundation(s) and ${sites.length} site(s)`)
542
+ }
543
+
398
544
  /**
399
545
  * Main build command handler
400
546
  */
@@ -430,22 +576,26 @@ export async function build(args = []) {
430
576
 
431
577
  if (!targetType) {
432
578
  error('Could not detect project type')
433
- log('Use --target foundation or --target site')
579
+ log('Use --target foundation or --target site, or run from workspace root')
434
580
  process.exit(1)
435
581
  }
436
582
 
437
- info(`Detected project type: ${targetType}`)
583
+ if (targetType !== 'workspace') {
584
+ info(`Detected project type: ${targetType}`)
585
+ }
438
586
  }
439
587
 
440
- // Validate prerender flags are only used with site target
441
- if ((prerenderFlag || noPrerenderFlag) && targetType !== 'site') {
442
- error('--prerender/--no-prerender can only be used with site builds')
588
+ // Validate prerender flags are only used with site/workspace target
589
+ if ((prerenderFlag || noPrerenderFlag) && targetType === 'foundation') {
590
+ error('--prerender/--no-prerender can only be used with site or workspace builds')
443
591
  process.exit(1)
444
592
  }
445
593
 
446
594
  // Run appropriate build
447
595
  try {
448
- if (targetType === 'foundation') {
596
+ if (targetType === 'workspace') {
597
+ await buildWorkspace(projectDir, { prerenderFlag, noPrerenderFlag })
598
+ } else if (targetType === 'foundation') {
449
599
  await buildFoundation(projectDir)
450
600
  } else {
451
601
  // For sites, read config to determine prerender default
package/src/index.js CHANGED
@@ -68,10 +68,6 @@ const templates = {
68
68
  name: 'Multi-Site Workspace',
69
69
  description: 'Multiple sites and foundations in sites/* and foundations/*',
70
70
  },
71
- template: {
72
- name: 'Template Starter',
73
- description: 'Create a shareable Uniweb template for npm or GitHub',
74
- },
75
71
  }
76
72
 
77
73
  /**
@@ -235,11 +231,6 @@ async function main() {
235
231
  description: templates.multi.description,
236
232
  value: 'multi',
237
233
  },
238
- {
239
- title: templates.template.name,
240
- description: templates.template.description,
241
- value: 'template',
242
- },
243
234
  ],
244
235
  },
245
236
  ], {
@@ -300,6 +291,11 @@ async function main() {
300
291
  })
301
292
  } catch (err) {
302
293
  error(`Failed to apply template: ${err.message}`)
294
+ log('')
295
+ log(`${colors.yellow}Troubleshooting:${colors.reset}`)
296
+ log(` • Check your network connection`)
297
+ log(` • Official templates require GitHub access (may be blocked by corporate networks)`)
298
+ log(` • Try the built-in template instead: ${colors.cyan}uniweb create ${projectName}${colors.reset}`)
303
299
  process.exit(1)
304
300
  }
305
301
  }
@@ -339,6 +335,7 @@ ${colors.bright}Build Options:${colors.reset}
339
335
  --foundation-dir Path to foundation directory (for prerendering)
340
336
  --platform <name> Deployment platform (e.g., vercel) for platform-specific output
341
337
 
338
+ At workspace root, builds all foundations first, then all sites.
342
339
  Pre-rendering is enabled by default when build.prerender: true in site.yml
343
340
 
344
341
  ${colors.bright}Docs Subcommands:${colors.reset}
@@ -359,7 +356,6 @@ ${colors.bright}i18n Commands:${colors.reset}
359
356
  ${colors.bright}Template Types:${colors.reset}
360
357
  single One site + one foundation (default)
361
358
  multi Multiple sites and foundations
362
- template Starter for creating shareable templates
363
359
  marketing Official marketing template
364
360
  @scope/template-name npm package
365
361
  github:user/repo GitHub repository
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  // Built-in templates (file-based in cli/templates/)
6
- export const BUILTIN_TEMPLATES = ['single', 'multi', 'template']
6
+ export const BUILTIN_TEMPLATES = ['single', 'multi']
7
7
 
8
8
  // Official templates from @uniweb/templates package (downloaded from GitHub releases)
9
9
  export const OFFICIAL_TEMPLATES = ['marketing', 'academic', 'docs', 'international']
@@ -2,6 +2,9 @@ name: {{projectName}}
2
2
  description: Main site for {{projectName}}
3
3
  foundation: default
4
4
 
5
+ # Homepage - specifies which page folder becomes the root route (/)
6
+ index: home
7
+
5
8
  theme:
6
9
  primary: '#3b82f6'
7
10
 
@@ -1,5 +1,8 @@
1
1
  name: {{projectName}}
2
2
 
3
+ # Homepage - specifies which page folder becomes the root route (/)
4
+ index: home
5
+
3
6
  theme:
4
7
  primary: '#3b82f6'
5
8