uniweb 0.2.14 → 0.2.15

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uniweb",
3
- "version": "0.2.14",
3
+ "version": "0.2.15",
4
4
  "description": "Create structured Vite + React sites with content/code separation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -35,6 +35,6 @@
35
35
  "js-yaml": "^4.1.0",
36
36
  "prompts": "^2.4.2",
37
37
  "tar": "^7.0.0",
38
- "@uniweb/build": "0.1.6"
38
+ "@uniweb/build": "0.1.8"
39
39
  }
40
40
  }
package/src/index.js CHANGED
@@ -12,7 +12,7 @@
12
12
  * npx uniweb docs # Generate COMPONENTS.md from schema
13
13
  */
14
14
 
15
- import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs'
15
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync } from 'node:fs'
16
16
  import { resolve, join, dirname } from 'node:path'
17
17
  import { fileURLToPath } from 'node:url'
18
18
  import prompts from 'prompts'
@@ -27,6 +27,7 @@ import {
27
27
  listAvailableTemplates,
28
28
  BUILTIN_TEMPLATES,
29
29
  } from './templates/index.js'
30
+ import { copyTemplateDirectory, registerVersions } from './templates/processor.js'
30
31
 
31
32
  const __dirname = dirname(fileURLToPath(import.meta.url))
32
33
 
@@ -57,16 +58,83 @@ function title(message) {
57
58
  console.log(`\n${colors.cyan}${colors.bright}${message}${colors.reset}\n`)
58
59
  }
59
60
 
60
- // Template definitions
61
+ // Built-in template definitions (metadata for display)
61
62
  const templates = {
62
63
  single: {
63
64
  name: 'Single Project',
64
65
  description: 'One site + one foundation in site/ and foundation/ (recommended)',
65
66
  },
66
67
  multi: {
67
- name: 'Multi-Site Project',
68
+ name: 'Multi-Site Workspace',
68
69
  description: 'Multiple sites and foundations in sites/* and foundations/*',
69
70
  },
71
+ template: {
72
+ name: 'Template Starter',
73
+ description: 'Create a shareable Uniweb template for npm or GitHub',
74
+ },
75
+ }
76
+
77
+ /**
78
+ * Get the path to a built-in template
79
+ */
80
+ function getBuiltinTemplatePath(templateName) {
81
+ return join(__dirname, '..', 'templates', templateName)
82
+ }
83
+
84
+ /**
85
+ * Get the shared base template path
86
+ */
87
+ function getSharedTemplatePath() {
88
+ return join(__dirname, '..', 'templates', '_shared')
89
+ }
90
+
91
+ /**
92
+ * Apply a built-in template using file-based templates
93
+ */
94
+ async function applyBuiltinTemplate(templateName, targetPath, options = {}) {
95
+ const { projectName, variant, onProgress, onWarning } = options
96
+
97
+ const templatePath = getBuiltinTemplatePath(templateName)
98
+
99
+ // Load template.json for metadata
100
+ let templateConfig = {}
101
+ const configPath = join(templatePath, 'template.json')
102
+ if (existsSync(configPath)) {
103
+ templateConfig = JSON.parse(readFileSync(configPath, 'utf8'))
104
+ }
105
+
106
+ // Determine base template path if specified
107
+ let basePath = null
108
+ if (templateConfig.base) {
109
+ basePath = join(__dirname, '..', 'templates', templateConfig.base)
110
+ if (!existsSync(basePath)) {
111
+ if (onWarning) {
112
+ onWarning(`Base template '${templateConfig.base}' not found at ${basePath}`)
113
+ }
114
+ basePath = null
115
+ }
116
+ }
117
+
118
+ // Register versions for Handlebars templates
119
+ registerVersions(getVersionsForTemplates())
120
+
121
+ // Prepare template data
122
+ const templateData = {
123
+ projectName: projectName || 'my-project',
124
+ templateName: templateName,
125
+ templateTitle: projectName || 'My Project',
126
+ templateDescription: templateConfig.description || 'A Uniweb project',
127
+ }
128
+
129
+ // Copy template files
130
+ await copyTemplateDirectory(templatePath, targetPath, templateData, {
131
+ variant,
132
+ basePath,
133
+ onProgress,
134
+ onWarning,
135
+ })
136
+
137
+ success(`Created project: ${projectName || 'my-project'}`)
70
138
  }
71
139
 
72
140
  async function main() {
@@ -167,6 +235,11 @@ async function main() {
167
235
  description: templates.multi.description,
168
236
  value: 'multi',
169
237
  },
238
+ {
239
+ title: templates.template.name,
240
+ description: templates.template.description,
241
+ value: 'template',
242
+ },
170
243
  ],
171
244
  },
172
245
  ], {
@@ -196,17 +269,16 @@ async function main() {
196
269
  const parsed = parseTemplateId(templateType)
197
270
 
198
271
  if (parsed.type === 'builtin') {
199
- log(`\nCreating ${templates[templateType].name.toLowerCase()}...`)
200
-
201
- // Generate project based on built-in template
202
- switch (templateType) {
203
- case 'single':
204
- await createSingleProject(projectDir, projectName)
205
- break
206
- case 'multi':
207
- await createMultiProject(projectDir, projectName)
208
- break
209
- }
272
+ const templateMeta = templates[templateType]
273
+ log(`\nCreating ${templateMeta ? templateMeta.name.toLowerCase() : templateType}...`)
274
+
275
+ // Apply file-based built-in template
276
+ await applyBuiltinTemplate(templateType, projectDir, {
277
+ projectName: displayName || projectName,
278
+ variant,
279
+ onProgress: (msg) => log(` ${colors.dim}${msg}${colors.reset}`),
280
+ onWarning: (msg) => log(` ${colors.yellow}Warning: ${msg}${colors.reset}`),
281
+ })
210
282
  } else {
211
283
  // External template (official, npm, or github)
212
284
  log(`\nResolving template: ${templateType}...`)
@@ -278,6 +350,7 @@ ${colors.bright}i18n Commands:${colors.reset}
278
350
  ${colors.bright}Template Types:${colors.reset}
279
351
  single One site + one foundation (default)
280
352
  multi Multiple sites and foundations
353
+ template Starter for creating shareable templates
281
354
  marketing Official marketing template
282
355
  @scope/template-name npm package
283
356
  github:user/repo GitHub repository
@@ -296,1014 +369,6 @@ ${colors.bright}Examples:${colors.reset}
296
369
  `)
297
370
  }
298
371
 
299
- // Template generators
300
-
301
- /**
302
- * Creates the common project base: package.json, pnpm-workspace.yaml, .gitignore
303
- * Both single and multi templates share the same workspace configuration.
304
- */
305
- function createProjectBase(projectDir, projectName, defaultFilter) {
306
- mkdirSync(projectDir, { recursive: true })
307
-
308
- // Root package.json (workspaces field for npm compatibility)
309
- writeJSON(join(projectDir, 'package.json'), {
310
- name: projectName,
311
- version: '0.1.0',
312
- private: true,
313
- type: 'module',
314
- scripts: {
315
- dev: `pnpm --filter ${defaultFilter} dev`,
316
- 'dev:runtime': `VITE_FOUNDATION_MODE=runtime pnpm --filter ${defaultFilter} dev`,
317
- build: 'pnpm -r build',
318
- },
319
- workspaces: [
320
- 'site',
321
- 'foundation',
322
- 'sites/*',
323
- 'foundations/*',
324
- ],
325
- pnpm: {
326
- onlyBuiltDependencies: ['esbuild', 'sharp'],
327
- },
328
- })
329
-
330
- // pnpm-workspace.yaml (all patterns for seamless evolution)
331
- writeFile(join(projectDir, 'pnpm-workspace.yaml'), `packages:
332
- - 'site'
333
- - 'foundation'
334
- - 'sites/*'
335
- - 'foundations/*'
336
- `)
337
-
338
- // .gitignore
339
- writeFile(join(projectDir, '.gitignore'), `node_modules
340
- dist
341
- .DS_Store
342
- *.local
343
- `)
344
- }
345
-
346
- /**
347
- * Creates a single project with site/ and foundation/ as sibling packages.
348
- * This is the default template for new projects.
349
- */
350
- async function createSingleProject(projectDir, projectName) {
351
- createProjectBase(projectDir, projectName, 'site')
352
-
353
- // README.md
354
- writeFile(join(projectDir, 'README.md'), `# ${projectName}
355
-
356
- Structured Vite + React, ready to go.
357
-
358
- ## Quick Start
359
-
360
- \`\`\`bash
361
- pnpm install
362
- pnpm dev
363
- \`\`\`
364
-
365
- Open [http://localhost:3000](http://localhost:3000) to see your site.
366
-
367
- ## Project Structure
368
-
369
- \`\`\`
370
- ${projectName}/
371
- ├── site/ # Content + configuration
372
- │ ├── pages/ # File-based routing
373
- │ │ └── home/
374
- │ │ ├── page.yml # Page metadata
375
- │ │ └── 1-hero.md # Section content
376
- │ ├── locales/ # i18n (mirrors pages/)
377
- │ ├── src/ # Site entry point
378
- │ └── public/ # Static assets
379
-
380
- ├── foundation/ # Your components
381
- │ └── src/
382
- │ ├── components/ # React components
383
- │ ├── meta.js # Foundation metadata
384
- │ └── styles.css # Tailwind styles
385
-
386
- ├── package.json
387
- └── pnpm-workspace.yaml
388
- \`\`\`
389
-
390
- ## Development
391
-
392
- ### Standard Mode (recommended)
393
-
394
- \`\`\`bash
395
- pnpm dev
396
- \`\`\`
397
-
398
- Edit components in \`foundation/src/components/\` and see changes instantly via HMR.
399
- Edit content in \`site/pages/\` to add or modify pages.
400
-
401
- ### Runtime Loading Mode
402
-
403
- \`\`\`bash
404
- pnpm dev:runtime
405
- \`\`\`
406
-
407
- Tests production behavior where the foundation is loaded as a separate module.
408
- Use this to debug issues that only appear in production.
409
-
410
- ## Building for Production
411
-
412
- \`\`\`bash
413
- pnpm build
414
- \`\`\`
415
-
416
- **Output:**
417
- - \`foundation/dist/\` — Bundled components, CSS, and schema.json
418
- - \`site/dist/\` — Production-ready static site
419
-
420
- ## Adding Components
421
-
422
- 1. Create a new folder in \`foundation/src/components/YourComponent/\`
423
- 2. Add \`index.jsx\` with your React component
424
- 3. Add \`meta.js\` describing the component's content slots and options
425
- 4. Export from \`foundation/src/index.js\`
426
-
427
- ## Adding Pages
428
-
429
- 1. Create a folder in \`site/pages/your-page/\`
430
- 2. Add \`page.yml\` with page metadata
431
- 3. Add markdown files (\`1-section.md\`, \`2-section.md\`, etc.) for each section
432
-
433
- Each markdown file specifies which component to use:
434
-
435
- \`\`\`markdown
436
- ---
437
- type: Hero
438
- theme: dark
439
- ---
440
-
441
- # Your Title
442
-
443
- Your subtitle or description here.
444
-
445
- [Get Started](#)
446
- \`\`\`
447
-
448
- ## Scaling Up
449
-
450
- The workspace is pre-configured for growth—no config changes needed.
451
-
452
- **Add a second site** (keep existing structure):
453
-
454
- \`\`\`bash
455
- mkdir -p sites/docs
456
- # Create your docs site in sites/docs/
457
- \`\`\`
458
-
459
- **Or migrate to multi-site structure**:
460
-
461
- \`\`\`bash
462
- # Move and rename by purpose
463
- mv site sites/marketing
464
- mv foundation foundations/marketing
465
-
466
- # Update package names in package.json files
467
- # Update dependencies to reference new names
468
- \`\`\`
469
-
470
- Both patterns work simultaneously—evolve gradually as needed.
471
-
472
- ## Publishing Your Foundation
473
-
474
- Your \`foundation/\` is already a complete package:
475
-
476
- \`\`\`bash
477
- cd foundation
478
- npx uniweb build
479
- npm publish
480
- \`\`\`
481
-
482
- ## What is Uniweb?
483
-
484
- Uniweb is a **Component Web Platform** that bridges content and components.
485
- Foundations define the vocabulary (available components, options, design rules).
486
- Sites provide content that flows through Foundations.
487
-
488
- Learn more:
489
- - [Uniweb on GitHub](https://github.com/uniweb)
490
- - [CLI Documentation](https://github.com/uniweb/cli)
491
- - [uniweb.app](https://uniweb.app) — Visual editing platform
492
-
493
- `)
494
-
495
- // Create site package
496
- await createSite(join(projectDir, 'site'), 'site', true)
497
-
498
- // Create foundation package
499
- await createFoundation(join(projectDir, 'foundation'), 'foundation', true)
500
-
501
- // Update site to reference workspace foundation
502
- const sitePackageJson = join(projectDir, 'site/package.json')
503
- const sitePkg = JSON.parse(readFile(sitePackageJson))
504
- sitePkg.dependencies['foundation'] = 'file:../foundation'
505
- writeJSON(sitePackageJson, sitePkg)
506
-
507
- success(`Created project: ${projectName}`)
508
- }
509
-
510
- /**
511
- * Creates a multi-site/foundation workspace with sites/ and foundations/ directories.
512
- * Used for larger projects with multiple sites sharing foundations.
513
- */
514
- async function createMultiProject(projectDir, projectName) {
515
- createProjectBase(projectDir, projectName, 'marketing')
516
-
517
- // README.md
518
- writeFile(join(projectDir, 'README.md'), `# ${projectName}
519
-
520
- A Uniweb workspace for multiple sites and foundations.
521
-
522
- ## Quick Start
523
-
524
- \`\`\`bash
525
- pnpm install
526
- pnpm dev
527
- \`\`\`
528
-
529
- Open [http://localhost:3000](http://localhost:3000) to see the marketing site.
530
-
531
- ## Project Structure
532
-
533
- \`\`\`
534
- ${projectName}/
535
- ├── sites/
536
- │ └── marketing/ # Main marketing site
537
- │ ├── pages/ # Content pages
538
- │ ├── locales/ # i18n
539
- │ └── src/
540
-
541
- ├── foundations/
542
- │ └── marketing/ # Marketing foundation
543
- │ └── src/
544
- │ ├── components/ # React components
545
- │ └── styles.css # Tailwind styles
546
-
547
- ├── package.json
548
- └── pnpm-workspace.yaml
549
- \`\`\`
550
-
551
- ## Development
552
-
553
- \`\`\`bash
554
- # Run the marketing site (default)
555
- pnpm dev
556
-
557
- # Run a specific site
558
- pnpm --filter docs dev
559
-
560
- # Runtime loading mode (tests production behavior)
561
- pnpm dev:runtime
562
- \`\`\`
563
-
564
- ## Adding More Sites
565
-
566
- Create a new site in \`sites/\`:
567
-
568
- \`\`\`bash
569
- mkdir -p sites/docs
570
- # Copy structure from sites/marketing or create manually
571
- \`\`\`
572
-
573
- Update \`sites/docs/site.yml\` to specify which foundation:
574
-
575
- \`\`\`yaml
576
- name: docs
577
- defaultLanguage: en
578
- foundation: documentation # Or marketing, or any foundation
579
- \`\`\`
580
-
581
- ## Adding More Foundations
582
-
583
- Create a new foundation in \`foundations/\`:
584
-
585
- \`\`\`bash
586
- mkdir -p foundations/documentation
587
- # Build components for documentation use case
588
- \`\`\`
589
-
590
- Name foundations by purpose: marketing, documentation, learning, etc.
591
-
592
- ## Building for Production
593
-
594
- \`\`\`bash
595
- # Build everything
596
- pnpm build
597
-
598
- # Build specific packages
599
- pnpm --filter marketing build
600
- pnpm --filter foundations/marketing build
601
- \`\`\`
602
-
603
- ## Learn More
604
-
605
- - [Uniweb on GitHub](https://github.com/uniweb)
606
- - [uniweb.app](https://uniweb.app) — Visual editing platform
607
-
608
- `)
609
-
610
- // Create first site in sites/marketing
611
- await createSite(join(projectDir, 'sites/marketing'), 'marketing', true)
612
-
613
- // Create first foundation in foundations/marketing
614
- await createFoundation(join(projectDir, 'foundations/marketing'), 'marketing', true)
615
-
616
- // Update site to reference workspace foundation
617
- const sitePackageJson = join(projectDir, 'sites/marketing/package.json')
618
- const sitePkg = JSON.parse(readFile(sitePackageJson))
619
- sitePkg.dependencies['marketing'] = 'file:../../foundations/marketing'
620
- writeJSON(sitePackageJson, sitePkg)
621
-
622
- // Update site.yml to reference the marketing foundation
623
- writeFile(join(projectDir, 'sites/marketing/site.yml'), `name: marketing
624
- defaultLanguage: en
625
-
626
- # Foundation to use for this site
627
- foundation: marketing
628
- `)
629
-
630
- success(`Created workspace: ${projectName}`)
631
- }
632
-
633
- async function createSite(projectDir, projectName, isWorkspace = false) {
634
- mkdirSync(projectDir, { recursive: true })
635
- mkdirSync(join(projectDir, 'src'), { recursive: true })
636
- mkdirSync(join(projectDir, 'pages/home'), { recursive: true })
637
-
638
- // package.json
639
- writeJSON(join(projectDir, 'package.json'), {
640
- name: projectName,
641
- version: '0.1.0',
642
- type: 'module',
643
- private: true,
644
- scripts: {
645
- dev: 'vite',
646
- build: 'vite build',
647
- preview: 'vite preview',
648
- },
649
- dependencies: {
650
- '@uniweb/runtime': getVersion('@uniweb/runtime'),
651
- ...(isWorkspace ? {} : { 'foundation-example': '^0.1.0' }),
652
- },
653
- devDependencies: {
654
- '@uniweb/build': getVersion('@uniweb/build'),
655
- '@vitejs/plugin-react': '^5.0.0',
656
- autoprefixer: '^10.4.18',
657
- 'js-yaml': '^4.1.0',
658
- postcss: '^8.4.35',
659
- react: '^18.2.0',
660
- 'react-dom': '^18.2.0',
661
- 'react-router-dom': '^7.0.0',
662
- tailwindcss: '^3.4.1',
663
- vite: '^7.0.0',
664
- 'vite-plugin-svgr': '^4.2.0',
665
- },
666
- })
667
-
668
- // Foundation import name (used for initial site.yml)
669
- const foundationImport = isWorkspace ? 'foundation' : 'foundation-example'
670
-
671
- // tailwind.config.js - reads foundation from site.yml
672
- writeFile(join(projectDir, 'tailwind.config.js'), `import { readFileSync, existsSync } from 'fs'
673
- import yaml from 'js-yaml'
674
-
675
- // Read foundation from site.yml
676
- const siteConfig = yaml.load(readFileSync('./site.yml', 'utf8'))
677
- const foundation = siteConfig.foundation || 'foundation'
678
-
679
- // Resolve foundation path (workspace sibling or node_modules)
680
- const workspacePath = \`../\${foundation}/src/**/*.{js,jsx,ts,tsx}\`
681
- const npmPath = \`./node_modules/\${foundation}/src/**/*.{js,jsx,ts,tsx}\`
682
- const contentPath = existsSync(\`../\${foundation}\`) ? workspacePath : npmPath
683
-
684
- export default {
685
- content: [contentPath],
686
- theme: {
687
- extend: {
688
- colors: {
689
- primary: '#3b82f6',
690
- secondary: '#64748b',
691
- },
692
- },
693
- },
694
- plugins: [],
695
- }
696
- `)
697
-
698
- // postcss.config.js
699
- writeFile(join(projectDir, 'postcss.config.js'), `export default {
700
- plugins: {
701
- tailwindcss: {},
702
- autoprefixer: {},
703
- },
704
- }
705
- `)
706
-
707
- // vite.config.js - reads foundation from site.yml
708
- writeFile(join(projectDir, 'vite.config.js'), `import { defineConfig } from 'vite'
709
- import { readFileSync, existsSync } from 'fs'
710
- import yaml from 'js-yaml'
711
- import react from '@vitejs/plugin-react'
712
- import svgr from 'vite-plugin-svgr'
713
- import { siteContentPlugin } from '@uniweb/build/site'
714
- import { foundationDevPlugin } from '@uniweb/build/dev'
715
-
716
- // Read foundation from site.yml
717
- const siteConfig = yaml.load(readFileSync('./site.yml', 'utf8'))
718
- const foundation = siteConfig.foundation || 'foundation'
719
-
720
- // Check if foundation is a workspace sibling or npm package
721
- const isWorkspaceFoundation = existsSync(\`../\${foundation}\`)
722
- const foundationPath = isWorkspaceFoundation ? \`../\${foundation}\` : \`./node_modules/\${foundation}\`
723
-
724
- const useRuntimeLoading = process.env.VITE_FOUNDATION_MODE === 'runtime'
725
-
726
- export default defineConfig({
727
- resolve: {
728
- alias: {
729
- // Alias #foundation to the actual foundation package
730
- '#foundation': foundation,
731
- },
732
- },
733
- plugins: [
734
- react(),
735
- svgr(),
736
- siteContentPlugin({
737
- sitePath: './',
738
- inject: true,
739
- }),
740
- useRuntimeLoading && foundationDevPlugin({
741
- name: foundation,
742
- path: foundationPath,
743
- serve: '/foundation',
744
- watch: true,
745
- }),
746
- ].filter(Boolean),
747
- server: {
748
- fs: { allow: ['..'] },
749
- port: 3000,
750
- },
751
- optimizeDeps: {
752
- include: ['react', 'react-dom', 'react-dom/client', 'react-router-dom'],
753
- },
754
- })
755
- `)
756
-
757
- // index.html
758
- writeFile(join(projectDir, 'index.html'), `<!DOCTYPE html>
759
- <html lang="en">
760
- <head>
761
- <meta charset="UTF-8" />
762
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
763
- <title>${projectName}</title>
764
- <style>
765
- body { margin: 0; font-family: system-ui, sans-serif; }
766
- .loading { display: flex; align-items: center; justify-content: center; min-height: 100vh; color: #64748b; }
767
- </style>
768
- </head>
769
- <body>
770
- <div id="root">
771
- <div class="loading">Loading...</div>
772
- </div>
773
- <script type="module" src="/src/main.jsx"></script>
774
- </body>
775
- </html>
776
- `)
777
-
778
- // main.jsx - uses #foundation alias (configured in vite.config.js from site.yml)
779
- writeFile(join(projectDir, 'src/main.jsx'), `import initRuntime from '@uniweb/runtime'
780
-
781
- const useRuntimeLoading = import.meta.env.VITE_FOUNDATION_MODE === 'runtime'
782
-
783
- async function start() {
784
- if (useRuntimeLoading) {
785
- initRuntime({
786
- url: '/foundation/foundation.js',
787
- cssUrl: '/foundation/assets/style.css'
788
- })
789
- } else {
790
- // #foundation alias is resolved by Vite based on site.yml config
791
- const foundation = await import('#foundation')
792
- await import('#foundation/styles')
793
- initRuntime(foundation)
794
- }
795
- }
796
-
797
- start().catch(console.error)
798
- `)
799
-
800
- // site.yml
801
- writeFile(join(projectDir, 'site.yml'), `name: ${projectName}
802
- defaultLanguage: en
803
-
804
- # Foundation to use for this site
805
- foundation: ${foundationImport}
806
- `)
807
-
808
- // pages/home/page.yml
809
- writeFile(join(projectDir, 'pages/home/page.yml'), `title: Home
810
- order: 1
811
- `)
812
-
813
- // pages/home/1-hero.md
814
- writeFile(join(projectDir, 'pages/home/1-hero.md'), `---
815
- type: Hero
816
- theme: dark
817
- ---
818
-
819
- # Welcome to ${projectName}
820
-
821
- Built with Uniweb and Vite.
822
-
823
- [Get Started](#)
824
- `)
825
-
826
- // README.md (only for standalone site, not workspace)
827
- if (!isWorkspace) {
828
- writeFile(join(projectDir, 'README.md'), `# ${projectName}
829
-
830
- A Uniweb site — a content-driven website powered by a Foundation component library.
831
-
832
- ## Quick Start
833
-
834
- \`\`\`bash
835
- pnpm install
836
- pnpm dev
837
- \`\`\`
838
-
839
- Open [http://localhost:3000](http://localhost:3000) to see your site.
840
-
841
- ## Project Structure
842
-
843
- \`\`\`
844
- ${projectName}/
845
- ├── pages/ # Your content
846
- │ └── home/
847
- │ ├── page.yml # Page metadata
848
- │ └── 1-hero.md # Section content
849
- ├── src/
850
- │ └── main.jsx # Site entry point
851
- ├── site.yml # Site configuration
852
- ├── vite.config.js
853
- └── package.json
854
- \`\`\`
855
-
856
- ## Adding Pages
857
-
858
- 1. Create a folder in \`pages/your-page/\`
859
- 2. Add \`page.yml\`:
860
-
861
- \`\`\`yaml
862
- title: Your Page Title
863
- order: 2
864
- \`\`\`
865
-
866
- 3. Add section files (\`1-hero.md\`, \`2-features.md\`, etc.):
867
-
868
- \`\`\`markdown
869
- ---
870
- type: Hero
871
- theme: dark
872
- ---
873
-
874
- # Section Title
875
-
876
- Section description here.
877
-
878
- [Call to Action](#)
879
- \`\`\`
880
-
881
- ## How It Works
882
-
883
- - Each folder in \`pages/\` becomes a route (\`/home\`, \`/about\`, etc.)
884
- - Section files are numbered to control order (\`1-*.md\`, \`2-*.md\`)
885
- - Frontmatter specifies the component and configuration parameters
886
- - Content in the markdown body is semantically parsed into structured data
887
-
888
- ## Configuration
889
-
890
- The \`site.yml\` file configures your site:
891
-
892
- \`\`\`yaml
893
- name: ${projectName}
894
- defaultLanguage: en
895
- foundation: ${foundationImport} # Which foundation to use
896
- \`\`\`
897
-
898
- To use a different foundation, update the \`foundation\` field and install the package.
899
-
900
- ## Building for Production
901
-
902
- \`\`\`bash
903
- pnpm build
904
- \`\`\`
905
-
906
- Output is in \`dist/\` — ready to deploy to any static host.
907
-
908
- ## Learn More
909
-
910
- - [Uniweb on GitHub](https://github.com/uniweb)
911
- - [uniweb.app](https://uniweb.app)
912
-
913
- `)
914
- }
915
-
916
- success(`Created site: ${projectName}`)
917
- }
918
-
919
- async function createFoundation(projectDir, projectName, isWorkspace = false) {
920
- mkdirSync(projectDir, { recursive: true })
921
- mkdirSync(join(projectDir, 'src/components/Hero'), { recursive: true })
922
- mkdirSync(join(projectDir, 'src/icons'), { recursive: true })
923
-
924
- // package.json
925
- writeJSON(join(projectDir, 'package.json'), {
926
- name: projectName,
927
- version: '0.1.0',
928
- type: 'module',
929
- main: './src/index.js',
930
- exports: {
931
- '.': './src/index.js',
932
- './styles': './src/styles.css',
933
- './dist': './dist/foundation.js',
934
- './dist/styles': './dist/assets/style.css',
935
- },
936
- files: ['dist', 'src'],
937
- scripts: {
938
- dev: 'vite',
939
- build: 'uniweb build',
940
- 'build:vite': 'vite build',
941
- preview: 'vite preview',
942
- },
943
- peerDependencies: {
944
- react: '^18.0.0',
945
- 'react-dom': '^18.0.0',
946
- },
947
- devDependencies: {
948
- '@vitejs/plugin-react': '^5.0.0',
949
- autoprefixer: '^10.4.18',
950
- postcss: '^8.4.35',
951
- react: '^18.2.0',
952
- 'react-dom': '^18.2.0',
953
- tailwindcss: '^3.4.1',
954
- uniweb: getVersion('uniweb'),
955
- vite: '^7.0.0',
956
- 'vite-plugin-svgr': '^4.2.0',
957
- },
958
- })
959
-
960
- // vite.config.js
961
- writeFile(join(projectDir, 'vite.config.js'), `import { defineConfig } from 'vite'
962
- import react from '@vitejs/plugin-react'
963
- import svgr from 'vite-plugin-svgr'
964
- import { resolve } from 'path'
965
-
966
- export default defineConfig({
967
- plugins: [react(), svgr()],
968
- build: {
969
- lib: {
970
- entry: resolve(__dirname, 'src/entry-runtime.js'),
971
- formats: ['es'],
972
- fileName: 'foundation',
973
- },
974
- rollupOptions: {
975
- external: ['react', 'react-dom', 'react/jsx-runtime', 'react/jsx-dev-runtime'],
976
- output: {
977
- assetFileNames: 'assets/[name][extname]',
978
- },
979
- },
980
- sourcemap: true,
981
- cssCodeSplit: false,
982
- },
983
- })
984
- `)
985
-
986
- // tailwind.config.js
987
- writeFile(join(projectDir, 'tailwind.config.js'), `import { fileURLToPath } from 'url'
988
- import { dirname, join } from 'path'
989
-
990
- const __dirname = dirname(fileURLToPath(import.meta.url))
991
-
992
- export default {
993
- content: [join(__dirname, './src/**/*.{js,jsx,ts,tsx}')],
994
- theme: {
995
- extend: {
996
- colors: {
997
- primary: '#3b82f6',
998
- secondary: '#64748b',
999
- },
1000
- },
1001
- },
1002
- plugins: [],
1003
- }
1004
- `)
1005
-
1006
- // postcss.config.js
1007
- writeFile(join(projectDir, 'postcss.config.js'), `export default {
1008
- plugins: {
1009
- tailwindcss: {},
1010
- autoprefixer: {},
1011
- },
1012
- }
1013
- `)
1014
-
1015
- // .gitignore
1016
- writeFile(join(projectDir, '.gitignore'), `node_modules
1017
- dist
1018
- .DS_Store
1019
- *.local
1020
- _entry.generated.js
1021
- `)
1022
-
1023
- // src/styles.css
1024
- writeFile(join(projectDir, 'src/styles.css'), `@tailwind base;
1025
- @tailwind components;
1026
- @tailwind utilities;
1027
- `)
1028
-
1029
- // src/meta.js (foundation-level metadata)
1030
- writeFile(join(projectDir, 'src/meta.js'), `/**
1031
- * ${projectName} Foundation Metadata
1032
- */
1033
- export default {
1034
- name: '${projectName}',
1035
- description: 'A Uniweb foundation',
1036
-
1037
- // Runtime props (available at render time)
1038
- props: {
1039
- themeToggleEnabled: true,
1040
- },
1041
-
1042
- // Style configuration for the editor
1043
- styleFields: [
1044
- {
1045
- id: 'primary-color',
1046
- type: 'color',
1047
- label: 'Primary Color',
1048
- default: '#3b82f6',
1049
- },
1050
- ],
1051
- }
1052
- `)
1053
-
1054
- // src/index.js (manual entry for dev - imports from components)
1055
- writeFile(join(projectDir, 'src/index.js'), `/**
1056
- * ${projectName} Foundation
1057
- *
1058
- * This is the manual entry point for development.
1059
- * During build, _entry.generated.js is created automatically.
1060
- */
1061
-
1062
- import Hero from './components/Hero/index.jsx'
1063
-
1064
- const components = { Hero }
1065
-
1066
- export function getComponent(name) {
1067
- return components[name]
1068
- }
1069
-
1070
- export function listComponents() {
1071
- return Object.keys(components)
1072
- }
1073
-
1074
- export function getSchema(name) {
1075
- return components[name]?.schema
1076
- }
1077
-
1078
- export function getAllSchemas() {
1079
- const schemas = {}
1080
- for (const [name, component] of Object.entries(components)) {
1081
- if (component.schema) schemas[name] = component.schema
1082
- }
1083
- return schemas
1084
- }
1085
-
1086
- export { Hero }
1087
- export default { getComponent, listComponents, getSchema, getAllSchemas, components }
1088
- `)
1089
-
1090
- // src/entry-runtime.js
1091
- writeFile(join(projectDir, 'src/entry-runtime.js'), `import './styles.css'
1092
- export * from './index.js'
1093
- export { default } from './index.js'
1094
- `)
1095
-
1096
- // src/components/Hero/index.jsx
1097
- writeFile(join(projectDir, 'src/components/Hero/index.jsx'), `import React from 'react'
1098
-
1099
- export function Hero({ content, params }) {
1100
- // Extract from semantic parser structure
1101
- const { title } = content.main?.header || {}
1102
- const { paragraphs = [], links = [] } = content.main?.body || {}
1103
- const { theme = 'light' } = params || {}
1104
-
1105
- const isDark = theme === 'dark'
1106
- const cta = links[0]
1107
-
1108
- return (
1109
- <section className={\`py-20 px-6 \${isDark ? 'bg-gradient-to-br from-primary to-blue-700 text-white' : 'bg-gray-50'}\`}>
1110
- <div className="max-w-4xl mx-auto text-center">
1111
- {title && (
1112
- <h1 className="text-4xl md:text-5xl font-bold mb-6">{title}</h1>
1113
- )}
1114
- {paragraphs[0] && (
1115
- <p className={\`text-lg mb-8 \${isDark ? 'text-blue-100' : 'text-gray-600'}\`}>
1116
- {paragraphs[0]}
1117
- </p>
1118
- )}
1119
- {cta && (
1120
- <a
1121
- href={cta.url}
1122
- className={\`inline-block px-6 py-3 font-semibold rounded-lg transition-colors \${
1123
- isDark
1124
- ? 'bg-white text-primary hover:bg-blue-50'
1125
- : 'bg-primary text-white hover:bg-blue-700'
1126
- }\`}
1127
- >
1128
- {cta.text}
1129
- </a>
1130
- )}
1131
- </div>
1132
- </section>
1133
- )
1134
- }
1135
-
1136
- export default Hero
1137
- `)
1138
-
1139
- // src/components/Hero/meta.js
1140
- writeFile(join(projectDir, 'src/components/Hero/meta.js'), `/**
1141
- * Hero Component Metadata
1142
- *
1143
- * Content comes from the markdown body (parsed semantically):
1144
- * - H1 → title (content.main.header.title)
1145
- * - Paragraphs → description (content.main.body.paragraphs)
1146
- * - Links → CTA buttons (content.main.body.links)
1147
- */
1148
- export default {
1149
- title: 'Hero Banner',
1150
- description: 'A prominent header section with headline, description, and call-to-action',
1151
- category: 'Headers',
1152
-
1153
- // Content structure (informational - describes what the semantic parser provides)
1154
- elements: {
1155
- title: {
1156
- label: 'Headline',
1157
- description: 'From H1 in markdown',
1158
- required: true,
1159
- },
1160
- paragraphs: {
1161
- label: 'Description',
1162
- description: 'From paragraphs in markdown',
1163
- },
1164
- links: {
1165
- label: 'Call to Action',
1166
- description: 'From links in markdown',
1167
- },
1168
- },
1169
-
1170
- // Configuration parameters (set in frontmatter)
1171
- properties: {
1172
- theme: {
1173
- type: 'select',
1174
- label: 'Theme',
1175
- options: [
1176
- { value: 'light', label: 'Light' },
1177
- { value: 'dark', label: 'Dark' },
1178
- ],
1179
- default: 'light',
1180
- },
1181
- alignment: {
1182
- type: 'select',
1183
- label: 'Text Alignment',
1184
- options: [
1185
- { value: 'center', label: 'Center' },
1186
- { value: 'left', label: 'Left' },
1187
- ],
1188
- default: 'center',
1189
- },
1190
- },
1191
- }
1192
- `)
1193
-
1194
- // README.md (only for standalone foundation, not workspace)
1195
- if (!isWorkspace) {
1196
- writeFile(join(projectDir, 'README.md'), `# ${projectName}
1197
-
1198
- A Uniweb Foundation — a React component library for content-driven websites.
1199
-
1200
- ## Quick Start
1201
-
1202
- \`\`\`bash
1203
- pnpm install
1204
- pnpm dev # Start Vite dev server for component development
1205
- pnpm build # Build for production
1206
- \`\`\`
1207
-
1208
- ## Project Structure
1209
-
1210
- \`\`\`
1211
- ${projectName}/
1212
- ├── src/
1213
- │ ├── components/ # Your components
1214
- │ │ └── Hero/
1215
- │ │ ├── index.jsx # React component
1216
- │ │ └── meta.js # Component metadata
1217
- │ ├── meta.js # Foundation metadata
1218
- │ ├── index.js # Exports
1219
- │ └── styles.css # Tailwind styles
1220
- ├── package.json
1221
- ├── vite.config.js
1222
- └── tailwind.config.js
1223
- \`\`\`
1224
-
1225
- ## Adding Components
1226
-
1227
- 1. Create \`src/components/YourComponent/index.jsx\`:
1228
-
1229
- \`\`\`jsx
1230
- export function YourComponent({ content }) {
1231
- const { title, description } = content
1232
- return (
1233
- <section className="py-12 px-6">
1234
- <h2>{title}</h2>
1235
- <p>{description}</p>
1236
- </section>
1237
- )
1238
- }
1239
-
1240
- export default YourComponent
1241
- \`\`\`
1242
-
1243
- 2. Create \`src/components/YourComponent/meta.js\`:
1244
-
1245
- \`\`\`js
1246
- export default {
1247
- title: 'Your Component',
1248
- description: 'What this component does',
1249
- category: 'Content',
1250
- elements: {
1251
- title: { label: 'Title', required: true },
1252
- description: { label: 'Description' },
1253
- },
1254
- }
1255
- \`\`\`
1256
-
1257
- 3. Export from \`src/index.js\`:
1258
-
1259
- \`\`\`js
1260
- export { YourComponent } from './components/YourComponent/index.jsx'
1261
- \`\`\`
1262
-
1263
- ## Build Output
1264
-
1265
- After \`pnpm build\`:
1266
-
1267
- \`\`\`
1268
- dist/
1269
- ├── foundation.js # Bundled components
1270
- ├── assets/style.css # Compiled Tailwind CSS
1271
- └── schema.json # Component metadata for editors
1272
- \`\`\`
1273
-
1274
- ## What is a Foundation?
1275
-
1276
- A Foundation defines the vocabulary for Uniweb sites:
1277
- - **Components** — The building blocks creators can use
1278
- - **Elements** — Content slots (title, description, images, etc.)
1279
- - **Properties** — Configuration options exposed to creators
1280
- - **Presets** — Pre-configured variations of components
1281
-
1282
- Learn more at [github.com/uniweb](https://github.com/uniweb)
1283
-
1284
- `)
1285
- }
1286
-
1287
- success(`Created foundation: ${projectName}`)
1288
- }
1289
-
1290
- // Utility functions
1291
- function writeFile(path, content) {
1292
- const dir = dirname(path)
1293
- if (!existsSync(dir)) {
1294
- mkdirSync(dir, { recursive: true })
1295
- }
1296
- writeFileSync(path, content)
1297
- }
1298
-
1299
- function writeJSON(path, obj) {
1300
- writeFile(path, JSON.stringify(obj, null, 2) + '\n')
1301
- }
1302
-
1303
- function readFile(path) {
1304
- return readFileSync(path, 'utf-8')
1305
- }
1306
-
1307
372
  // Run CLI
1308
373
  main().catch((err) => {
1309
374
  error(err.message)
@@ -153,11 +153,17 @@ async function processFile(sourcePath, targetPath, data, options = {}) {
153
153
  * @param {Object} data - Template variables
154
154
  * @param {Object} options - Processing options
155
155
  * @param {string|null} options.variant - Template variant to use
156
+ * @param {string|null} options.basePath - Base template to merge with (files copied first)
156
157
  * @param {Function} options.onWarning - Warning callback
157
158
  * @param {Function} options.onProgress - Progress callback
158
159
  */
159
160
  export async function copyTemplateDirectory(sourcePath, targetPath, data, options = {}) {
160
- const { variant = null, onWarning, onProgress } = options
161
+ const { variant = null, basePath = null, onWarning, onProgress } = options
162
+
163
+ // If a base template is specified, copy it first (without the basePath option to avoid recursion)
164
+ if (basePath && existsSync(basePath)) {
165
+ await copyTemplateDirectory(basePath, targetPath, data, { variant, onWarning, onProgress })
166
+ }
161
167
 
162
168
  await fs.mkdir(targetPath, { recursive: true })
163
169
  const entries = await fs.readdir(sourcePath, { withFileTypes: true })
@@ -173,6 +179,9 @@ export async function copyTemplateDirectory(sourcePath, targetPath, data, option
173
179
  }
174
180
  }
175
181
 
182
+ // Options for recursive calls (without basePath to avoid re-copying base at each level)
183
+ const recursionOptions = { variant, onWarning, onProgress }
184
+
176
185
  for (const entry of entries) {
177
186
  const sourceName = entry.name
178
187
 
@@ -199,7 +208,7 @@ export async function copyTemplateDirectory(sourcePath, targetPath, data, option
199
208
  const sourceFullPath = path.join(sourcePath, sourceName)
200
209
  const targetFullPath = path.join(targetPath, baseName)
201
210
 
202
- await copyTemplateDirectory(sourceFullPath, targetFullPath, data, options)
211
+ await copyTemplateDirectory(sourceFullPath, targetFullPath, data, recursionOptions)
203
212
  } else {
204
213
  // Regular directory - skip if a variant override exists and we're using that variant
205
214
  if (variant && variantBases.has(sourceName)) {
@@ -210,10 +219,15 @@ export async function copyTemplateDirectory(sourcePath, targetPath, data, option
210
219
  const sourceFullPath = path.join(sourcePath, sourceName)
211
220
  const targetFullPath = path.join(targetPath, sourceName)
212
221
 
213
- await copyTemplateDirectory(sourceFullPath, targetFullPath, data, options)
222
+ await copyTemplateDirectory(sourceFullPath, targetFullPath, data, recursionOptions)
214
223
  }
215
224
  } else {
216
225
  // File processing
226
+ // Skip template.json as it's metadata for the template, not for the output
227
+ if (sourceName === 'template.json') {
228
+ continue
229
+ }
230
+
217
231
  // Remove .hbs extension for target filename
218
232
  const targetName = sourceName.endsWith('.hbs')
219
233
  ? sourceName.slice(0, -4)
@@ -2,8 +2,8 @@
2
2
  * Template resolver - parses template identifiers and determines source type
3
3
  */
4
4
 
5
- // Built-in templates that are generated programmatically
6
- export const BUILTIN_TEMPLATES = ['single', 'multi']
5
+ // Built-in templates (file-based in cli/templates/)
6
+ export const BUILTIN_TEMPLATES = ['single', 'multi', 'template']
7
7
 
8
8
  // Official templates from @uniweb/templates package
9
9
  export const OFFICIAL_TEMPLATES = ['marketing', 'docs', 'learning']