@uniweb/templates 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +32 -0
- package/src/index.js +120 -0
- package/src/processor.js +184 -0
- package/src/validator.js +206 -0
- package/templates/marketing/template/README.md.hbs +56 -0
- package/templates/marketing/template/foundation/package.json.hbs +34 -0
- package/templates/marketing/template/foundation/postcss.config.js +6 -0
- package/templates/marketing/template/foundation/src/components/CTA/index.jsx +75 -0
- package/templates/marketing/template/foundation/src/components/CTA/meta.js +46 -0
- package/templates/marketing/template/foundation/src/components/Features/index.jsx +81 -0
- package/templates/marketing/template/foundation/src/components/Features/meta.js +46 -0
- package/templates/marketing/template/foundation/src/components/Hero/index.jsx +73 -0
- package/templates/marketing/template/foundation/src/components/Hero/meta.js +46 -0
- package/templates/marketing/template/foundation/src/components/Pricing/index.jsx +108 -0
- package/templates/marketing/template/foundation/src/components/Pricing/meta.js +36 -0
- package/templates/marketing/template/foundation/src/components/Testimonials/index.jsx +96 -0
- package/templates/marketing/template/foundation/src/components/Testimonials/meta.js +44 -0
- package/templates/marketing/template/foundation/src/entry-runtime.js +3 -0
- package/templates/marketing/template/foundation/src/index.js +37 -0
- package/templates/marketing/template/foundation/src/meta.js.hbs +28 -0
- package/templates/marketing/template/foundation/src/styles.css +3 -0
- package/templates/marketing/template/foundation/tailwind.config.js +17 -0
- package/templates/marketing/template/foundation/vite.config.js +23 -0
- package/templates/marketing/template/package.json.hbs +13 -0
- package/templates/marketing/template/pnpm-workspace.yaml +3 -0
- package/templates/marketing/template/site/index.html.hbs +18 -0
- package/templates/marketing/template/site/package.json.hbs +27 -0
- package/templates/marketing/template/site/pages/home/1-hero.md +12 -0
- package/templates/marketing/template/site/pages/home/2-features.md +33 -0
- package/templates/marketing/template/site/pages/home/3-pricing.md +43 -0
- package/templates/marketing/template/site/pages/home/4-testimonials.md +27 -0
- package/templates/marketing/template/site/pages/home/5-cta.md +12 -0
- package/templates/marketing/template/site/pages/home/page.yml +2 -0
- package/templates/marketing/template/site/postcss.config.js +6 -0
- package/templates/marketing/template/site/site.yml.hbs +5 -0
- package/templates/marketing/template/site/src/main.jsx +19 -0
- package/templates/marketing/template/site/tailwind.config.js +24 -0
- package/templates/marketing/template/site/vite.config.js +42 -0
- package/templates/marketing/template.json +8 -0
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@uniweb/templates",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Template processing engine and official templates for Uniweb",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js",
|
|
9
|
+
"./templates/*": "./templates/*"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"src",
|
|
13
|
+
"templates"
|
|
14
|
+
],
|
|
15
|
+
"keywords": [
|
|
16
|
+
"uniweb",
|
|
17
|
+
"templates",
|
|
18
|
+
"scaffolding"
|
|
19
|
+
],
|
|
20
|
+
"author": "Uniweb",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/uniweb/templates.git"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"handlebars": "^4.7.8"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @uniweb/templates - Template processing engine and official templates
|
|
3
|
+
*
|
|
4
|
+
* Provides utilities for applying file-based templates with Handlebars
|
|
5
|
+
* variable substitution and variant support.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
import { existsSync } from 'node:fs'
|
|
10
|
+
import { fileURLToPath } from 'node:url'
|
|
11
|
+
import { copyTemplateDirectory, clearCache } from './processor.js'
|
|
12
|
+
import {
|
|
13
|
+
validateTemplate,
|
|
14
|
+
listTemplates,
|
|
15
|
+
satisfiesVersion,
|
|
16
|
+
ValidationError,
|
|
17
|
+
ErrorCodes
|
|
18
|
+
} from './validator.js'
|
|
19
|
+
|
|
20
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
21
|
+
const TEMPLATES_DIR = path.join(__dirname, '../templates')
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get the path to the built-in templates directory
|
|
25
|
+
*/
|
|
26
|
+
export function getTemplatesDirectory() {
|
|
27
|
+
return TEMPLATES_DIR
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get a specific template by name from the built-in templates
|
|
32
|
+
*
|
|
33
|
+
* @param {string} name - Template name (e.g., 'marketing', 'docs')
|
|
34
|
+
* @returns {string|null} Path to template directory, or null if not found
|
|
35
|
+
*/
|
|
36
|
+
export function getTemplatePath(name) {
|
|
37
|
+
const templatePath = path.join(TEMPLATES_DIR, name)
|
|
38
|
+
return templatePath
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* List all available built-in templates
|
|
43
|
+
*
|
|
44
|
+
* @returns {Promise<Array<Object>>} List of template metadata
|
|
45
|
+
*/
|
|
46
|
+
export async function listBuiltinTemplates() {
|
|
47
|
+
return listTemplates(TEMPLATES_DIR)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Check if a built-in template exists
|
|
52
|
+
*
|
|
53
|
+
* @param {string} name - Template name
|
|
54
|
+
* @returns {boolean}
|
|
55
|
+
*/
|
|
56
|
+
export function hasTemplate(name) {
|
|
57
|
+
const templatePath = path.join(TEMPLATES_DIR, name)
|
|
58
|
+
return existsSync(path.join(templatePath, 'template.json'))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Apply a template to a target directory
|
|
63
|
+
*
|
|
64
|
+
* @param {string} templatePath - Path to the template root (contains template.json)
|
|
65
|
+
* @param {string} targetPath - Destination directory for the scaffolded project
|
|
66
|
+
* @param {Object} data - Template variables
|
|
67
|
+
* @param {Object} options - Apply options
|
|
68
|
+
* @param {string} options.variant - Template variant to use
|
|
69
|
+
* @param {string} options.uniwebVersion - Current Uniweb version for compatibility check
|
|
70
|
+
* @param {Function} options.onWarning - Warning callback
|
|
71
|
+
* @param {Function} options.onProgress - Progress callback
|
|
72
|
+
* @returns {Promise<Object>} Template metadata
|
|
73
|
+
*/
|
|
74
|
+
export async function applyTemplate(templatePath, targetPath, data = {}, options = {}) {
|
|
75
|
+
const { uniwebVersion, variant, onWarning, onProgress } = options
|
|
76
|
+
|
|
77
|
+
// Validate the template
|
|
78
|
+
const metadata = await validateTemplate(templatePath, { uniwebVersion })
|
|
79
|
+
|
|
80
|
+
// Apply default variables
|
|
81
|
+
const templateData = {
|
|
82
|
+
year: new Date().getFullYear(),
|
|
83
|
+
...data
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Copy template files
|
|
87
|
+
await copyTemplateDirectory(
|
|
88
|
+
metadata.templateDir,
|
|
89
|
+
targetPath,
|
|
90
|
+
templateData,
|
|
91
|
+
{ variant, onWarning, onProgress }
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return metadata
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Apply a built-in template by name
|
|
99
|
+
*
|
|
100
|
+
* @param {string} name - Template name (e.g., 'marketing')
|
|
101
|
+
* @param {string} targetPath - Destination directory
|
|
102
|
+
* @param {Object} data - Template variables
|
|
103
|
+
* @param {Object} options - Apply options
|
|
104
|
+
* @returns {Promise<Object>} Template metadata
|
|
105
|
+
*/
|
|
106
|
+
export async function applyBuiltinTemplate(name, targetPath, data = {}, options = {}) {
|
|
107
|
+
const templatePath = getTemplatePath(name)
|
|
108
|
+
return applyTemplate(templatePath, targetPath, data, options)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Re-export utilities
|
|
112
|
+
export {
|
|
113
|
+
copyTemplateDirectory,
|
|
114
|
+
clearCache,
|
|
115
|
+
validateTemplate,
|
|
116
|
+
listTemplates,
|
|
117
|
+
satisfiesVersion,
|
|
118
|
+
ValidationError,
|
|
119
|
+
ErrorCodes
|
|
120
|
+
}
|
package/src/processor.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template processor - handles file copying and Handlebars substitution
|
|
3
|
+
* Adapted from Proximify dev-tools template system
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'node:fs/promises'
|
|
7
|
+
import { existsSync } from 'node:fs'
|
|
8
|
+
import path from 'node:path'
|
|
9
|
+
import Handlebars from 'handlebars'
|
|
10
|
+
|
|
11
|
+
// Cache for compiled templates
|
|
12
|
+
const templateCache = new Map()
|
|
13
|
+
|
|
14
|
+
// Text file extensions that should be processed for variables
|
|
15
|
+
const TEXT_EXTENSIONS = new Set([
|
|
16
|
+
'.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs',
|
|
17
|
+
'.json', '.yml', '.yaml', '.md', '.mdx',
|
|
18
|
+
'.html', '.htm', '.css', '.scss', '.less',
|
|
19
|
+
'.txt', '.xml', '.svg', '.vue', '.astro'
|
|
20
|
+
])
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if a path is a directory
|
|
24
|
+
*/
|
|
25
|
+
async function isDirectory(filePath) {
|
|
26
|
+
try {
|
|
27
|
+
const stats = await fs.stat(filePath)
|
|
28
|
+
return stats.isDirectory()
|
|
29
|
+
} catch {
|
|
30
|
+
return false
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Check if string contains unresolved Handlebars placeholders
|
|
36
|
+
*/
|
|
37
|
+
function findUnresolvedPlaceholders(content) {
|
|
38
|
+
const patterns = [
|
|
39
|
+
/\{\{([^#/}>][^}]*)\}\}/g, // {{variable}} - not blocks or partials
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
const unresolved = []
|
|
43
|
+
for (const pattern of patterns) {
|
|
44
|
+
const matches = content.matchAll(pattern)
|
|
45
|
+
for (const match of matches) {
|
|
46
|
+
const varName = match[1].trim()
|
|
47
|
+
// Skip helpers and expressions with spaces (likely intentional)
|
|
48
|
+
if (!varName.includes(' ')) {
|
|
49
|
+
unresolved.push(varName)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return [...new Set(unresolved)]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Load and compile a Handlebars template with caching
|
|
58
|
+
*/
|
|
59
|
+
async function loadTemplate(templatePath) {
|
|
60
|
+
if (templateCache.has(templatePath)) {
|
|
61
|
+
return templateCache.get(templatePath)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const template = await fs.readFile(templatePath, 'utf8')
|
|
65
|
+
const compiled = Handlebars.compile(template)
|
|
66
|
+
templateCache.set(templatePath, compiled)
|
|
67
|
+
return compiled
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Process a single file - either copy or apply Handlebars
|
|
72
|
+
*/
|
|
73
|
+
async function processFile(sourcePath, targetPath, data, options = {}) {
|
|
74
|
+
const isHbs = sourcePath.endsWith('.hbs')
|
|
75
|
+
const ext = path.extname(isHbs ? sourcePath.slice(0, -4) : sourcePath)
|
|
76
|
+
const isTextFile = TEXT_EXTENSIONS.has(ext)
|
|
77
|
+
|
|
78
|
+
if (isHbs) {
|
|
79
|
+
// Process Handlebars template
|
|
80
|
+
const template = await loadTemplate(sourcePath)
|
|
81
|
+
const content = template(data)
|
|
82
|
+
|
|
83
|
+
// Check for unresolved placeholders
|
|
84
|
+
const unresolved = findUnresolvedPlaceholders(content)
|
|
85
|
+
if (unresolved.length > 0 && options.onWarning) {
|
|
86
|
+
options.onWarning(`Unresolved placeholders in ${path.basename(targetPath)}: ${unresolved.join(', ')}`)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
await fs.writeFile(targetPath, content)
|
|
90
|
+
} else if (isTextFile && options.processAllText) {
|
|
91
|
+
// Optionally process non-hbs text files for simple replacements
|
|
92
|
+
let content = await fs.readFile(sourcePath, 'utf8')
|
|
93
|
+
// Simple {{var}} replacement without full Handlebars
|
|
94
|
+
for (const [key, value] of Object.entries(data)) {
|
|
95
|
+
if (typeof value === 'string') {
|
|
96
|
+
content = content.replaceAll(`{{${key}}}`, value)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
await fs.writeFile(targetPath, content)
|
|
100
|
+
} else {
|
|
101
|
+
// Binary or non-template file - just copy
|
|
102
|
+
await fs.copyFile(sourcePath, targetPath)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Copy a directory structure recursively, processing templates
|
|
108
|
+
*
|
|
109
|
+
* @param {string} sourcePath - Source template directory
|
|
110
|
+
* @param {string} targetPath - Destination directory
|
|
111
|
+
* @param {Object} data - Template variables
|
|
112
|
+
* @param {Object} options - Processing options
|
|
113
|
+
* @param {string|null} options.variant - Template variant to use
|
|
114
|
+
* @param {Function} options.onWarning - Warning callback
|
|
115
|
+
* @param {Function} options.onProgress - Progress callback
|
|
116
|
+
*/
|
|
117
|
+
export async function copyTemplateDirectory(sourcePath, targetPath, data, options = {}) {
|
|
118
|
+
const { variant = null, onWarning, onProgress } = options
|
|
119
|
+
|
|
120
|
+
await fs.mkdir(targetPath, { recursive: true })
|
|
121
|
+
const entries = await fs.readdir(sourcePath, { withFileTypes: true })
|
|
122
|
+
|
|
123
|
+
for (const entry of entries) {
|
|
124
|
+
const sourceName = entry.name
|
|
125
|
+
|
|
126
|
+
// Check if this is a variant-specific item (e.g., "dir.variant" or "file.variant.ext")
|
|
127
|
+
const variantMatch = entry.isDirectory()
|
|
128
|
+
? sourceName.match(/^(.+)\.([^.]+)$/)
|
|
129
|
+
: null
|
|
130
|
+
|
|
131
|
+
if (entry.isDirectory()) {
|
|
132
|
+
if (variantMatch) {
|
|
133
|
+
const [, baseName, dirVariant] = variantMatch
|
|
134
|
+
|
|
135
|
+
// Skip directories that don't match our variant
|
|
136
|
+
if (variant && dirVariant !== variant) {
|
|
137
|
+
continue
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Use the base name without variant suffix for the target
|
|
141
|
+
const sourceFullPath = path.join(sourcePath, sourceName)
|
|
142
|
+
const targetFullPath = path.join(targetPath, baseName)
|
|
143
|
+
|
|
144
|
+
await copyTemplateDirectory(sourceFullPath, targetFullPath, data, options)
|
|
145
|
+
} else {
|
|
146
|
+
// Regular directory
|
|
147
|
+
const sourceFullPath = path.join(sourcePath, sourceName)
|
|
148
|
+
const targetFullPath = path.join(targetPath, sourceName)
|
|
149
|
+
|
|
150
|
+
await copyTemplateDirectory(sourceFullPath, targetFullPath, data, options)
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
// File processing
|
|
154
|
+
// Remove .hbs extension for target filename
|
|
155
|
+
const targetName = sourceName.endsWith('.hbs')
|
|
156
|
+
? sourceName.slice(0, -4)
|
|
157
|
+
: sourceName
|
|
158
|
+
|
|
159
|
+
const sourceFullPath = path.join(sourcePath, sourceName)
|
|
160
|
+
const targetFullPath = path.join(targetPath, targetName)
|
|
161
|
+
|
|
162
|
+
// Skip if target already exists (don't overwrite)
|
|
163
|
+
if (existsSync(targetFullPath)) {
|
|
164
|
+
if (onWarning) {
|
|
165
|
+
onWarning(`Skipping ${targetFullPath} - file already exists`)
|
|
166
|
+
}
|
|
167
|
+
continue
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (onProgress) {
|
|
171
|
+
onProgress(`Creating ${targetName}`)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
await processFile(sourceFullPath, targetFullPath, data, { onWarning })
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Clear the template cache
|
|
181
|
+
*/
|
|
182
|
+
export function clearCache() {
|
|
183
|
+
templateCache.clear()
|
|
184
|
+
}
|
package/src/validator.js
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template validation - checks template.json and compatibility
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs/promises'
|
|
6
|
+
import { existsSync } from 'node:fs'
|
|
7
|
+
import path from 'node:path'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Simple semver satisfaction check
|
|
11
|
+
* Supports: >=x.y.z, ^x.y.z, ~x.y.z, x.y.z, x.y.x, *, latest
|
|
12
|
+
*/
|
|
13
|
+
export function satisfiesVersion(version, range) {
|
|
14
|
+
if (!range || range === '*' || range === 'latest') {
|
|
15
|
+
return true
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const parseVersion = (v) => {
|
|
19
|
+
const match = v.match(/^(\d+)\.(\d+)\.(\d+)/)
|
|
20
|
+
if (!match) return null
|
|
21
|
+
return {
|
|
22
|
+
major: parseInt(match[1], 10),
|
|
23
|
+
minor: parseInt(match[2], 10),
|
|
24
|
+
patch: parseInt(match[3], 10)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const current = parseVersion(version)
|
|
29
|
+
if (!current) return true // Can't parse, assume compatible
|
|
30
|
+
|
|
31
|
+
// Handle different range formats
|
|
32
|
+
if (range.startsWith('>=')) {
|
|
33
|
+
const min = parseVersion(range.slice(2))
|
|
34
|
+
if (!min) return true
|
|
35
|
+
if (current.major > min.major) return true
|
|
36
|
+
if (current.major < min.major) return false
|
|
37
|
+
if (current.minor > min.minor) return true
|
|
38
|
+
if (current.minor < min.minor) return false
|
|
39
|
+
return current.patch >= min.patch
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (range.startsWith('^')) {
|
|
43
|
+
// ^x.y.z means >=x.y.z and <(x+1).0.0
|
|
44
|
+
const min = parseVersion(range.slice(1))
|
|
45
|
+
if (!min) return true
|
|
46
|
+
if (current.major !== min.major) return current.major > min.major && min.major === 0
|
|
47
|
+
if (current.minor > min.minor) return true
|
|
48
|
+
if (current.minor < min.minor) return false
|
|
49
|
+
return current.patch >= min.patch
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (range.startsWith('~')) {
|
|
53
|
+
// ~x.y.z means >=x.y.z and <x.(y+1).0
|
|
54
|
+
const min = parseVersion(range.slice(1))
|
|
55
|
+
if (!min) return true
|
|
56
|
+
if (current.major !== min.major) return false
|
|
57
|
+
if (current.minor !== min.minor) return false
|
|
58
|
+
return current.patch >= min.patch
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Exact version or x.y.x pattern
|
|
62
|
+
if (range.includes('x')) {
|
|
63
|
+
const parts = range.split('.')
|
|
64
|
+
const min = parseVersion(range.replace(/x/g, '0'))
|
|
65
|
+
if (!min) return true
|
|
66
|
+
if (parts[0] !== 'x' && current.major !== min.major) return false
|
|
67
|
+
if (parts[1] !== 'x' && current.minor !== min.minor) return false
|
|
68
|
+
return true
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Exact match
|
|
72
|
+
const exact = parseVersion(range)
|
|
73
|
+
if (!exact) return true
|
|
74
|
+
return current.major === exact.major &&
|
|
75
|
+
current.minor === exact.minor &&
|
|
76
|
+
current.patch === exact.patch
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Validation error with structured details
|
|
81
|
+
*/
|
|
82
|
+
export class ValidationError extends Error {
|
|
83
|
+
constructor(message, code, details = {}) {
|
|
84
|
+
super(message)
|
|
85
|
+
this.name = 'ValidationError'
|
|
86
|
+
this.code = code
|
|
87
|
+
this.details = details
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const ErrorCodes = {
|
|
92
|
+
MISSING_TEMPLATE_JSON: 'MISSING_TEMPLATE_JSON',
|
|
93
|
+
INVALID_TEMPLATE_JSON: 'INVALID_TEMPLATE_JSON',
|
|
94
|
+
MISSING_TEMPLATE_DIR: 'MISSING_TEMPLATE_DIR',
|
|
95
|
+
MISSING_REQUIRED_FIELD: 'MISSING_REQUIRED_FIELD',
|
|
96
|
+
VERSION_MISMATCH: 'VERSION_MISMATCH',
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Validate a template directory structure and metadata
|
|
101
|
+
*
|
|
102
|
+
* @param {string} templateRoot - Path to the template root (contains template.json)
|
|
103
|
+
* @param {Object} options - Validation options
|
|
104
|
+
* @param {string} options.uniwebVersion - Current Uniweb version to check compatibility
|
|
105
|
+
* @returns {Object} Parsed and validated template metadata
|
|
106
|
+
*/
|
|
107
|
+
export async function validateTemplate(templateRoot, options = {}) {
|
|
108
|
+
const { uniwebVersion } = options
|
|
109
|
+
|
|
110
|
+
// Check for template.json
|
|
111
|
+
const metadataPath = path.join(templateRoot, 'template.json')
|
|
112
|
+
if (!existsSync(metadataPath)) {
|
|
113
|
+
throw new ValidationError(
|
|
114
|
+
`Missing template.json in ${templateRoot}`,
|
|
115
|
+
ErrorCodes.MISSING_TEMPLATE_JSON,
|
|
116
|
+
{ path: templateRoot }
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Parse template.json
|
|
121
|
+
let metadata
|
|
122
|
+
try {
|
|
123
|
+
const content = await fs.readFile(metadataPath, 'utf8')
|
|
124
|
+
metadata = JSON.parse(content)
|
|
125
|
+
} catch (err) {
|
|
126
|
+
throw new ValidationError(
|
|
127
|
+
`Invalid template.json: ${err.message}`,
|
|
128
|
+
ErrorCodes.INVALID_TEMPLATE_JSON,
|
|
129
|
+
{ path: metadataPath, error: err.message }
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Check required fields
|
|
134
|
+
if (!metadata.name) {
|
|
135
|
+
throw new ValidationError(
|
|
136
|
+
'template.json missing required field: name',
|
|
137
|
+
ErrorCodes.MISSING_REQUIRED_FIELD,
|
|
138
|
+
{ field: 'name' }
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check for template/ directory
|
|
143
|
+
const templateDir = path.join(templateRoot, 'template')
|
|
144
|
+
if (!existsSync(templateDir)) {
|
|
145
|
+
throw new ValidationError(
|
|
146
|
+
`Missing template/ directory in ${templateRoot}`,
|
|
147
|
+
ErrorCodes.MISSING_TEMPLATE_DIR,
|
|
148
|
+
{ path: templateRoot }
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Check version compatibility
|
|
153
|
+
if (uniwebVersion && metadata.uniweb) {
|
|
154
|
+
if (!satisfiesVersion(uniwebVersion, metadata.uniweb)) {
|
|
155
|
+
throw new ValidationError(
|
|
156
|
+
`Template requires Uniweb ${metadata.uniweb}, but current version is ${uniwebVersion}`,
|
|
157
|
+
ErrorCodes.VERSION_MISMATCH,
|
|
158
|
+
{ required: metadata.uniweb, current: uniwebVersion }
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
...metadata,
|
|
165
|
+
templateDir,
|
|
166
|
+
metadataPath
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Get list of available templates in a templates directory
|
|
172
|
+
*
|
|
173
|
+
* @param {string} templatesDir - Path to directory containing templates
|
|
174
|
+
* @returns {Array<Object>} List of template metadata
|
|
175
|
+
*/
|
|
176
|
+
export async function listTemplates(templatesDir) {
|
|
177
|
+
if (!existsSync(templatesDir)) {
|
|
178
|
+
return []
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const entries = await fs.readdir(templatesDir, { withFileTypes: true })
|
|
182
|
+
const templates = []
|
|
183
|
+
|
|
184
|
+
for (const entry of entries) {
|
|
185
|
+
if (!entry.isDirectory()) continue
|
|
186
|
+
|
|
187
|
+
const templatePath = path.join(templatesDir, entry.name)
|
|
188
|
+
const metadataPath = path.join(templatePath, 'template.json')
|
|
189
|
+
|
|
190
|
+
if (existsSync(metadataPath)) {
|
|
191
|
+
try {
|
|
192
|
+
const content = await fs.readFile(metadataPath, 'utf8')
|
|
193
|
+
const metadata = JSON.parse(content)
|
|
194
|
+
templates.push({
|
|
195
|
+
id: entry.name,
|
|
196
|
+
...metadata,
|
|
197
|
+
path: templatePath
|
|
198
|
+
})
|
|
199
|
+
} catch {
|
|
200
|
+
// Skip invalid templates
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return templates
|
|
206
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# {{projectName}}
|
|
2
|
+
|
|
3
|
+
A marketing site built with [Uniweb](https://uniweb.io).
|
|
4
|
+
|
|
5
|
+
## Getting Started
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Install dependencies
|
|
9
|
+
pnpm install
|
|
10
|
+
|
|
11
|
+
# Start development server
|
|
12
|
+
pnpm dev
|
|
13
|
+
|
|
14
|
+
# Build for production
|
|
15
|
+
pnpm build
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Project Structure
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
{{projectName}}/
|
|
22
|
+
├── foundation/ # Component library
|
|
23
|
+
│ └── src/
|
|
24
|
+
│ └── components/
|
|
25
|
+
│ ├── Hero/
|
|
26
|
+
│ ├── Features/
|
|
27
|
+
│ ├── Pricing/
|
|
28
|
+
│ ├── Testimonials/
|
|
29
|
+
│ └── CTA/
|
|
30
|
+
└── site/ # Marketing site
|
|
31
|
+
└── pages/
|
|
32
|
+
└── home/
|
|
33
|
+
├── 1-hero.md
|
|
34
|
+
├── 2-features.md
|
|
35
|
+
├── 3-pricing.md
|
|
36
|
+
├── 4-testimonials.md
|
|
37
|
+
└── 5-cta.md
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Components
|
|
41
|
+
|
|
42
|
+
- **Hero** - Eye-catching header with headline, description, and call-to-action
|
|
43
|
+
- **Features** - Showcase product features with icons and descriptions
|
|
44
|
+
- **Pricing** - Pricing table with multiple tiers
|
|
45
|
+
- **Testimonials** - Customer quotes and social proof
|
|
46
|
+
- **CTA** - Call-to-action section to drive conversions
|
|
47
|
+
|
|
48
|
+
## Customization
|
|
49
|
+
|
|
50
|
+
Edit the markdown files in `site/pages/` to update content. Each section file has YAML frontmatter for configuration and markdown body for content.
|
|
51
|
+
|
|
52
|
+
Edit component styles in `foundation/src/components/*/index.jsx`.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
Created with the Uniweb Marketing Template
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{projectName}}-foundation",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./src/index.js",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.js",
|
|
8
|
+
"./styles": "./src/styles.css",
|
|
9
|
+
"./dist": "./dist/foundation.js",
|
|
10
|
+
"./dist/styles": "./dist/assets/style.css"
|
|
11
|
+
},
|
|
12
|
+
"files": ["dist", "src"],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"dev": "vite",
|
|
15
|
+
"build": "uniweb build",
|
|
16
|
+
"build:vite": "vite build",
|
|
17
|
+
"preview": "vite preview"
|
|
18
|
+
},
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"react": "^18.0.0",
|
|
21
|
+
"react-dom": "^18.0.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@vitejs/plugin-react": "^4.2.1",
|
|
25
|
+
"autoprefixer": "^10.4.18",
|
|
26
|
+
"postcss": "^8.4.35",
|
|
27
|
+
"react": "^18.2.0",
|
|
28
|
+
"react-dom": "^18.2.0",
|
|
29
|
+
"tailwindcss": "^3.4.1",
|
|
30
|
+
"uniweb": "^0.2.0",
|
|
31
|
+
"vite": "^5.1.0",
|
|
32
|
+
"vite-plugin-svgr": "^4.2.0"
|
|
33
|
+
}
|
|
34
|
+
}
|