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 +2 -2
- package/src/index.js +87 -1022
- package/src/templates/processor.js +17 -3
- package/src/templates/resolver.js +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.2.
|
|
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.
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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']
|