uniweb 0.7.1 → 0.7.3

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.
Files changed (59) hide show
  1. package/README.md +64 -17
  2. package/package.json +6 -5
  3. package/src/commands/add.js +571 -0
  4. package/src/commands/build.js +49 -6
  5. package/src/commands/doctor.js +181 -2
  6. package/src/index.js +281 -131
  7. package/src/templates/index.js +0 -94
  8. package/src/templates/processor.js +10 -87
  9. package/src/templates/resolver.js +3 -3
  10. package/src/templates/validator.js +59 -17
  11. package/src/utils/config.js +229 -0
  12. package/src/utils/scaffold.js +196 -0
  13. package/templates/{single/foundation → foundation}/package.json.hbs +2 -2
  14. package/templates/foundation/src/foundation.js.hbs +7 -0
  15. package/templates/foundation/src/sections/.gitkeep +0 -0
  16. package/templates/{multi/sites/main → site}/package.json.hbs +2 -2
  17. package/templates/site/site.yml.hbs +10 -0
  18. package/templates/site/theme.yml +1 -0
  19. package/templates/{_shared → workspace}/package.json.hbs +3 -9
  20. package/templates/workspace/pnpm-workspace.yaml.hbs +4 -0
  21. package/templates/_shared/pnpm-workspace.yaml +0 -5
  22. package/templates/multi/README.md.hbs +0 -85
  23. package/templates/multi/foundations/default/package.json.hbs +0 -38
  24. package/templates/multi/foundations/default/src/foundation.js +0 -41
  25. package/templates/multi/package.json.hbs +0 -26
  26. package/templates/multi/sites/main/pages/home/1-welcome.md.hbs +0 -14
  27. package/templates/multi/sites/main/site.yml.hbs +0 -12
  28. package/templates/multi/sites/main/vite.config.js +0 -7
  29. package/templates/multi/template/.vscode/settings.json +0 -6
  30. package/templates/multi/template.json +0 -5
  31. package/templates/single/foundation/src/sections/Section/index.jsx +0 -121
  32. package/templates/single/foundation/src/sections/Section/meta.js +0 -61
  33. package/templates/single/foundation/src/styles.css +0 -5
  34. package/templates/single/foundation/vite.config.js +0 -3
  35. package/templates/single/site/index.html.hbs +0 -13
  36. package/templates/single/site/main.js +0 -7
  37. package/templates/single/site/package.json.hbs +0 -27
  38. package/templates/single/site/pages/about/1-about.md.hbs +0 -13
  39. package/templates/single/site/pages/about/page.yml +0 -2
  40. package/templates/single/site/pages/home/page.yml +0 -2
  41. package/templates/single/site/public/favicon.svg +0 -7
  42. package/templates/single/site/site.yml.hbs +0 -10
  43. package/templates/single/template.json +0 -10
  44. /package/{templates/single → starter}/foundation/src/foundation.js +0 -0
  45. /package/{templates/multi/foundations/default → starter/foundation}/src/sections/Section/index.jsx +0 -0
  46. /package/{templates/multi/foundations/default → starter/foundation}/src/sections/Section/meta.js +0 -0
  47. /package/{templates/multi/sites/main → starter/site}/pages/about/1-about.md.hbs +0 -0
  48. /package/{templates/multi/sites/main → starter/site}/pages/about/page.yml +0 -0
  49. /package/{templates/single → starter}/site/pages/home/1-welcome.md.hbs +0 -0
  50. /package/{templates/multi/sites/main → starter/site}/pages/home/page.yml +0 -0
  51. /package/templates/{multi/foundations/default → foundation}/src/styles.css +0 -0
  52. /package/templates/{multi/foundations/default → foundation}/vite.config.js +0 -0
  53. /package/templates/{multi/sites/main → site}/index.html.hbs +0 -0
  54. /package/templates/{multi/sites/main → site}/main.js +0 -0
  55. /package/templates/{multi/sites/main → site}/public/favicon.svg +0 -0
  56. /package/templates/{single/site → site}/vite.config.js +0 -0
  57. /package/templates/{_shared → workspace}/AGENTS.md.hbs +0 -0
  58. /package/templates/{single → workspace}/README.md.hbs +0 -0
  59. /package/templates/{single/template/.vscode → workspace/_vscode}/settings.json +0 -0
package/src/index.js CHANGED
@@ -8,30 +8,26 @@
8
8
  * Usage:
9
9
  * npx uniweb create [project-name]
10
10
  * npx uniweb create --template marketing
11
+ * npx uniweb add foundation [name]
11
12
  * npx uniweb build
12
13
  * npx uniweb docs # Generate COMPONENTS.md from schema
13
14
  */
14
15
 
15
- import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync } from 'node:fs'
16
+ import { existsSync } from 'node:fs'
16
17
  import { execSync } from 'node:child_process'
17
- import { resolve, join, dirname } from 'node:path'
18
- import { fileURLToPath } from 'node:url'
18
+ import { resolve, join, relative } from 'node:path'
19
19
  import prompts from 'prompts'
20
20
  import { build } from './commands/build.js'
21
21
  import { docs } from './commands/docs.js'
22
22
  import { doctor } from './commands/doctor.js'
23
23
  import { i18n } from './commands/i18n.js'
24
- import { getVersionsForTemplates, getVersion } from './versions.js'
24
+ import { add } from './commands/add.js'
25
25
  import {
26
26
  resolveTemplate,
27
- applyExternalTemplate,
28
27
  parseTemplateId,
29
- listAvailableTemplates,
30
- BUILTIN_TEMPLATES,
31
28
  } from './templates/index.js'
32
- import { copyTemplateDirectory, registerVersions } from './templates/processor.js'
33
-
34
- const __dirname = dirname(fileURLToPath(import.meta.url))
29
+ import { validateTemplate } from './templates/validator.js'
30
+ import { scaffoldWorkspace, scaffoldFoundation, scaffoldSite, applyContent, applyStarter, mergeTemplateDependencies } from './utils/scaffold.js'
35
31
 
36
32
  // Colors for terminal output
37
33
  const colors = {
@@ -60,79 +56,221 @@ function title(message) {
60
56
  console.log(`\n${colors.cyan}${colors.bright}${message}${colors.reset}\n`)
61
57
  }
62
58
 
63
- // Built-in template definitions (metadata for display)
64
- const templates = {
65
- single: {
66
- name: 'Single Project',
67
- description: 'One site + one foundation in site/ and foundation/ (recommended)',
68
- },
69
- multi: {
70
- name: 'Multi-Site Workspace',
71
- description: 'Multiple sites and foundations in sites/* and foundations/*',
72
- },
73
- }
74
-
75
59
  /**
76
- * Get the path to a built-in template
60
+ * Create a project using the new package template flow (default)
77
61
  */
78
- function getBuiltinTemplatePath(templateName) {
79
- return join(__dirname, '..', 'templates', templateName)
62
+ async function createFromPackageTemplates(projectDir, projectName, options = {}) {
63
+ const { onProgress, onWarning } = options
64
+
65
+ onProgress?.('Setting up workspace...')
66
+
67
+ // 1. Scaffold workspace
68
+ await scaffoldWorkspace(projectDir, {
69
+ projectName,
70
+ workspaceGlobs: ['foundation', 'site'],
71
+ scripts: {
72
+ dev: 'pnpm --filter site dev',
73
+ build: 'uniweb build',
74
+ preview: 'pnpm --filter site preview',
75
+ },
76
+ }, { onProgress, onWarning })
77
+
78
+ // 2. Scaffold foundation
79
+ onProgress?.('Creating foundation...')
80
+ await scaffoldFoundation(join(projectDir, 'foundation'), {
81
+ name: 'foundation',
82
+ projectName,
83
+ isExtension: false,
84
+ }, { onProgress, onWarning })
85
+
86
+ // 3. Scaffold site
87
+ onProgress?.('Creating site...')
88
+ await scaffoldSite(join(projectDir, 'site'), {
89
+ name: 'site',
90
+ projectName,
91
+ foundationName: 'foundation',
92
+ foundationPath: 'file:../foundation',
93
+ }, { onProgress, onWarning })
94
+
95
+ // 4. Apply starter content
96
+ onProgress?.('Adding starter content...')
97
+ await applyStarter(projectDir, { projectName }, { onProgress, onWarning })
98
+
99
+ success(`Created project: ${projectName}`)
80
100
  }
81
101
 
82
102
  /**
83
- * Get the shared base template path
103
+ * Create a blank workspace (no packages, grow with `add`)
84
104
  */
85
- function getSharedTemplatePath() {
86
- return join(__dirname, '..', 'templates', '_shared')
105
+ async function createBlankWorkspace(projectDir, projectName, options = {}) {
106
+ const { onProgress, onWarning } = options
107
+
108
+ onProgress?.('Setting up blank workspace...')
109
+
110
+ await scaffoldWorkspace(projectDir, {
111
+ projectName,
112
+ workspaceGlobs: [],
113
+ scripts: {
114
+ build: 'uniweb build',
115
+ },
116
+ }, { onProgress, onWarning })
117
+
118
+ success(`Created blank workspace: ${projectName}`)
87
119
  }
88
120
 
89
121
  /**
90
- * Apply a built-in template using file-based templates
122
+ * Create a project from a format 2 content template
123
+ *
124
+ * Scaffolds workspace structure from package templates, then overlays
125
+ * content (sections, pages, theme) from the content template.
91
126
  */
92
- async function applyBuiltinTemplate(templateName, targetPath, options = {}) {
93
- const { projectName, variant, onProgress, onWarning } = options
127
+ async function createFromContentTemplate(projectDir, projectName, metadata, templateRootPath, options = {}) {
128
+ const { onProgress, onWarning } = options
129
+
130
+ // Determine packages to create
131
+ const packages = metadata.packages || [
132
+ { type: 'foundation', name: 'foundation' },
133
+ { type: 'site', name: 'site', foundation: 'foundation' },
134
+ ]
135
+
136
+ // Compute placement for each package
137
+ const placed = computePlacement(packages)
138
+
139
+ // Compute workspace globs and scripts from placement
140
+ const workspaceGlobs = placed.map(p => p.relativePath)
141
+ const sites = placed.filter(p => p.type === 'site')
142
+ const scripts = {
143
+ build: 'uniweb build',
144
+ }
145
+ if (sites.length === 1) {
146
+ scripts.dev = `pnpm --filter ${sites[0].name} dev`
147
+ scripts.preview = `pnpm --filter ${sites[0].name} preview`
148
+ } else {
149
+ for (const s of sites) {
150
+ scripts[`dev:${s.name}`] = `pnpm --filter ${s.name} dev`
151
+ scripts[`preview:${s.name}`] = `pnpm --filter ${s.name} preview`
152
+ }
153
+ // First site gets unqualified aliases
154
+ if (sites.length > 0) {
155
+ scripts.dev = `pnpm --filter ${sites[0].name} dev`
156
+ scripts.preview = `pnpm --filter ${sites[0].name} preview`
157
+ }
158
+ }
94
159
 
95
- const templatePath = getBuiltinTemplatePath(templateName)
160
+ // 1. Scaffold workspace
161
+ onProgress?.('Setting up workspace...')
162
+ await scaffoldWorkspace(projectDir, {
163
+ projectName,
164
+ workspaceGlobs,
165
+ scripts,
166
+ }, { onProgress, onWarning })
167
+
168
+ // 2. Scaffold and apply content for each package
169
+ for (const pkg of placed) {
170
+ const fullPath = join(projectDir, pkg.relativePath)
171
+
172
+ if (pkg.type === 'foundation' || pkg.type === 'extension') {
173
+ onProgress?.(`Creating ${pkg.type}: ${pkg.name}...`)
174
+ await scaffoldFoundation(fullPath, {
175
+ name: pkg.name,
176
+ projectName,
177
+ isExtension: pkg.type === 'extension',
178
+ }, { onProgress, onWarning })
179
+ } else if (pkg.type === 'site') {
180
+ // Find the foundation this site wires to
181
+ const foundationName = pkg.foundation || 'foundation'
182
+ const foundationPkg = placed.find(p =>
183
+ (p.type === 'foundation') && (p.name === foundationName)
184
+ )
185
+ const foundationPath = foundationPkg
186
+ ? computeFoundationFilePath(pkg.relativePath, foundationPkg.relativePath)
187
+ : 'file:../foundation'
188
+
189
+ onProgress?.(`Creating site: ${pkg.name}...`)
190
+ await scaffoldSite(fullPath, {
191
+ name: pkg.name,
192
+ projectName,
193
+ foundationName,
194
+ foundationPath,
195
+ foundationRef: foundationName !== 'foundation' ? foundationName : undefined,
196
+ }, { onProgress, onWarning })
197
+ }
96
198
 
97
- // Load template.json for metadata
98
- let templateConfig = {}
99
- const configPath = join(templatePath, 'template.json')
100
- if (existsSync(configPath)) {
101
- templateConfig = JSON.parse(readFileSync(configPath, 'utf8'))
102
- }
199
+ // Apply content from the matching content directory
200
+ const contentDir = findContentDirFor(metadata.contentDirs, pkg)
201
+ if (contentDir) {
202
+ onProgress?.(`Applying ${metadata.name} content to ${pkg.name}...`)
203
+ await applyContent(contentDir.dir, fullPath, { projectName }, { onProgress, onWarning })
204
+ }
103
205
 
104
- // Determine base template path if specified
105
- let basePath = null
106
- if (templateConfig.base) {
107
- basePath = join(__dirname, '..', 'templates', templateConfig.base)
108
- if (!existsSync(basePath)) {
109
- if (onWarning) {
110
- onWarning(`Base template '${templateConfig.base}' not found at ${basePath}`)
206
+ // Merge template dependencies into package.json
207
+ if (metadata.dependencies) {
208
+ const deps = metadata.dependencies[pkg.name] || metadata.dependencies[pkg.type]
209
+ if (deps) {
210
+ await mergeTemplateDependencies(join(fullPath, 'package.json'), deps)
111
211
  }
112
- basePath = null
113
212
  }
114
213
  }
115
214
 
116
- // Register versions for Handlebars templates
117
- registerVersions(getVersionsForTemplates())
215
+ success(`Created project: ${projectName}`)
216
+ }
217
+
218
+ /**
219
+ * Compute placement (relative paths) for packages
220
+ *
221
+ * Rules:
222
+ * - 1 foundation named "foundation" → foundation/
223
+ * - Multiple foundations → foundations/{name}/
224
+ * - Extensions → extensions/{name}/
225
+ * - 1 site named "site" → site/
226
+ * - Multiple sites → sites/{name}/
227
+ */
228
+ function computePlacement(packages) {
229
+ const foundations = packages.filter(p => p.type === 'foundation')
230
+ const extensions = packages.filter(p => p.type === 'extension')
231
+ const sites = packages.filter(p => p.type === 'site')
232
+
233
+ const placed = []
234
+
235
+ for (const f of foundations) {
236
+ if (foundations.length === 1 && f.name === 'foundation') {
237
+ placed.push({ ...f, relativePath: 'foundation' })
238
+ } else {
239
+ placed.push({ ...f, relativePath: `foundations/${f.name}` })
240
+ }
241
+ }
118
242
 
119
- // Prepare template data
120
- const templateData = {
121
- projectName: projectName || 'my-project',
122
- templateName: templateName,
123
- templateTitle: projectName || 'My Project',
124
- templateDescription: templateConfig.description || 'A Uniweb project',
243
+ for (const e of extensions) {
244
+ placed.push({ ...e, relativePath: `extensions/${e.name}` })
125
245
  }
126
246
 
127
- // Copy template files
128
- await copyTemplateDirectory(templatePath, targetPath, templateData, {
129
- variant,
130
- basePath,
131
- onProgress,
132
- onWarning,
133
- })
247
+ for (const s of sites) {
248
+ if (sites.length === 1 && s.name === 'site') {
249
+ placed.push({ ...s, relativePath: 'site' })
250
+ } else {
251
+ placed.push({ ...s, relativePath: `sites/${s.name}` })
252
+ }
253
+ }
134
254
 
135
- success(`Created project: ${projectName || 'my-project'}`)
255
+ return placed
256
+ }
257
+
258
+ /**
259
+ * Find the content directory that matches a placed package
260
+ */
261
+ function findContentDirFor(contentDirs, pkg) {
262
+ if (!contentDirs) return null
263
+ // Match by name first, then by type
264
+ return contentDirs.find(d => d.name === pkg.name) ||
265
+ contentDirs.find(d => d.type === pkg.type && d.name === pkg.type)
266
+ }
267
+
268
+ /**
269
+ * Compute relative file: path from site to foundation
270
+ */
271
+ function computeFoundationFilePath(sitePath, foundationPath) {
272
+ const rel = relative(sitePath, foundationPath)
273
+ return `file:${rel}`
136
274
  }
137
275
 
138
276
  async function main() {
@@ -169,6 +307,12 @@ async function main() {
169
307
  return
170
308
  }
171
309
 
310
+ // Handle add command
311
+ if (command === 'add') {
312
+ await add(args.slice(1))
313
+ return
314
+ }
315
+
172
316
  // Handle create command
173
317
  if (command !== 'create') {
174
318
  error(`Unknown command: ${command}`)
@@ -180,7 +324,7 @@ async function main() {
180
324
 
181
325
  // Parse arguments
182
326
  let projectName = args[1]
183
- let templateType = "single"; // or null for iteractive selection
327
+ let templateType = null // null = use new package template flow
184
328
 
185
329
  // Check for --template flag
186
330
  const templateIndex = args.indexOf('--template')
@@ -195,13 +339,6 @@ async function main() {
195
339
  }
196
340
  }
197
341
 
198
- // Check for --variant flag
199
- let variant = null
200
- const variantIndex = args.indexOf('--variant')
201
- if (variantIndex !== -1 && args[variantIndex + 1]) {
202
- variant = args[variantIndex + 1]
203
- }
204
-
205
342
  // Check for --name flag (used for project display name)
206
343
  let displayName = null
207
344
  const nameIndex = args.indexOf('--name')
@@ -212,6 +349,11 @@ async function main() {
212
349
  // Check for --no-git flag
213
350
  const noGit = args.includes('--no-git')
214
351
 
352
+ // Skip positional name if it starts with -- (it's a flag, not a name)
353
+ if (projectName && projectName.startsWith('--')) {
354
+ projectName = null
355
+ }
356
+
215
357
  // Interactive prompts
216
358
  const response = await prompts([
217
359
  {
@@ -227,23 +369,6 @@ async function main() {
227
369
  return true
228
370
  },
229
371
  },
230
- {
231
- type: templateType ? null : 'select',
232
- name: 'template',
233
- message: 'What would you like to create?',
234
- choices: [
235
- {
236
- title: templates.single.name,
237
- description: templates.single.description,
238
- value: 'single',
239
- },
240
- {
241
- title: templates.multi.name,
242
- description: templates.multi.description,
243
- value: 'multi',
244
- },
245
- ],
246
- },
247
372
  ], {
248
373
  onCancel: () => {
249
374
  log('\nScaffolding cancelled.')
@@ -252,13 +377,14 @@ async function main() {
252
377
  })
253
378
 
254
379
  projectName = projectName || response.projectName
255
- templateType = templateType || response.template
256
380
 
257
- if (!projectName || !templateType) {
258
- error('Missing project name or template type')
381
+ if (!projectName) {
382
+ error('Missing project name')
259
383
  process.exit(1)
260
384
  }
261
385
 
386
+ const effectiveName = displayName || projectName
387
+
262
388
  // Create project directory
263
389
  const projectDir = resolve(process.cwd(), projectName)
264
390
 
@@ -267,46 +393,53 @@ async function main() {
267
393
  process.exit(1)
268
394
  }
269
395
 
270
- // Resolve and create project based on template
271
- const parsed = parseTemplateId(templateType)
272
-
273
- if (parsed.type === 'builtin') {
274
- const templateMeta = templates[templateType]
275
- log(`\nCreating ${templateMeta ? templateMeta.name.toLowerCase() : templateType}...`)
396
+ // Template routing logic
397
+ const progressCb = (msg) => log(` ${colors.dim}${msg}${colors.reset}`)
398
+ const warningCb = (msg) => log(` ${colors.yellow}Warning: ${msg}${colors.reset}`)
276
399
 
277
- // Apply file-based built-in template
278
- await applyBuiltinTemplate(templateType, projectDir, {
279
- projectName: displayName || projectName,
280
- variant,
281
- onProgress: (msg) => log(` ${colors.dim}${msg}${colors.reset}`),
282
- onWarning: (msg) => log(` ${colors.yellow}Warning: ${msg}${colors.reset}`),
400
+ if (templateType === 'blank') {
401
+ // Blank workspace
402
+ log('\nCreating blank workspace...')
403
+ await createBlankWorkspace(projectDir, effectiveName, {
404
+ onProgress: progressCb,
405
+ onWarning: warningCb,
406
+ })
407
+ } else if (!templateType) {
408
+ // Default flow (package templates + starter)
409
+ log('\nCreating project...')
410
+ await createFromPackageTemplates(projectDir, effectiveName, {
411
+ onProgress: progressCb,
412
+ onWarning: warningCb,
283
413
  })
284
414
  } else {
285
- // External template (official, npm, or github)
415
+ // External: official/npm/github/local
286
416
  log(`\nResolving template: ${templateType}...`)
287
417
 
288
418
  try {
289
419
  const resolved = await resolveTemplate(templateType, {
290
- onProgress: (msg) => log(` ${colors.dim}${msg}${colors.reset}`),
420
+ onProgress: progressCb,
291
421
  })
292
422
 
293
423
  log(`\nCreating project from ${resolved.name || resolved.package || `${resolved.owner}/${resolved.repo}`}...`)
294
424
 
295
- await applyExternalTemplate(resolved, projectDir, {
296
- projectName: displayName || projectName,
297
- versions: getVersionsForTemplates(),
298
- }, {
299
- variant,
300
- onProgress: (msg) => log(` ${colors.dim}${msg}${colors.reset}`),
301
- onWarning: (msg) => log(` ${colors.yellow}Warning: ${msg}${colors.reset}`),
302
- })
425
+ // Validate and apply as format 2 content template
426
+ const metadata = await validateTemplate(resolved.path, {})
427
+
428
+ try {
429
+ await createFromContentTemplate(projectDir, effectiveName, metadata, resolved.path, {
430
+ onProgress: progressCb,
431
+ onWarning: warningCb,
432
+ })
433
+ } finally {
434
+ if (resolved.cleanup) await resolved.cleanup()
435
+ }
303
436
  } catch (err) {
304
437
  error(`Failed to apply template: ${err.message}`)
305
438
  log('')
306
439
  log(`${colors.yellow}Troubleshooting:${colors.reset}`)
307
440
  log(` • Check your network connection`)
308
441
  log(` • Official templates require GitHub access (may be blocked by corporate networks)`)
309
- log(` • Try the built-in template instead: ${colors.cyan}uniweb create ${projectName}${colors.reset}`)
442
+ log(` • Try the default template instead: ${colors.cyan}uniweb create ${projectName}${colors.reset}`)
310
443
  process.exit(1)
311
444
  }
312
445
  }
@@ -332,10 +465,19 @@ async function main() {
332
465
  // Success message
333
466
  title('Project created successfully!')
334
467
 
335
- log(`Next steps:\n`)
336
- log(` ${colors.cyan}cd ${projectName}${colors.reset}`)
337
- log(` ${colors.cyan}pnpm install${colors.reset}`)
338
- log(` ${colors.cyan}pnpm dev${colors.reset}`)
468
+ if (templateType === 'blank') {
469
+ log(`Next steps:\n`)
470
+ log(` ${colors.cyan}cd ${projectName}${colors.reset}`)
471
+ log(` ${colors.cyan}uniweb add foundation${colors.reset}`)
472
+ log(` ${colors.cyan}uniweb add site${colors.reset}`)
473
+ log(` ${colors.cyan}pnpm install${colors.reset}`)
474
+ log(` ${colors.cyan}pnpm dev${colors.reset}`)
475
+ } else {
476
+ log(`Next steps:\n`)
477
+ log(` ${colors.cyan}cd ${projectName}${colors.reset}`)
478
+ log(` ${colors.cyan}pnpm install${colors.reset}`)
479
+ log(` ${colors.cyan}pnpm dev${colors.reset}`)
480
+ }
339
481
  log('')
340
482
  }
341
483
 
@@ -348,16 +490,21 @@ ${colors.bright}Usage:${colors.reset}
348
490
 
349
491
  ${colors.bright}Commands:${colors.reset}
350
492
  create [name] Create a new project
493
+ add <type> [name] Add a foundation, site, or extension to a project
351
494
  build Build the current project
352
495
  docs Generate component documentation
353
496
  doctor Diagnose project configuration issues
354
497
  i18n <cmd> Internationalization (extract, sync, status)
355
498
 
356
499
  ${colors.bright}Create Options:${colors.reset}
357
- --template <type> Project template
358
- --variant <name> Template variant (e.g., tailwind3 for legacy)
500
+ --template <type> Project template (default: creates foundation + site + starter)
359
501
  --name <name> Project display name
360
- --no-git Skip git repository initialization
502
+ --no-git Skip git repository initialization
503
+
504
+ ${colors.bright}Add Subcommands:${colors.reset}
505
+ add foundation [name] Add a foundation (--from, --path, --project)
506
+ add site [name] Add a site (--from, --foundation, --path, --project)
507
+ add extension <name> Add an extension (--from, --site, --path)
361
508
 
362
509
  ${colors.bright}Build Options:${colors.reset}
363
510
  --target <type> Build target (foundation, site) - auto-detected if not specified
@@ -386,8 +533,7 @@ ${colors.bright}i18n Commands:${colors.reset}
386
533
  status Show translation coverage per locale
387
534
 
388
535
  ${colors.bright}Template Types:${colors.reset}
389
- single One site + one foundation (default)
390
- multi Multiple sites and foundations
536
+ blank Empty workspace (grow with 'add')
391
537
  marketing Official marketing template
392
538
  ./path/to/template Local directory
393
539
  @scope/template-name npm package
@@ -395,17 +541,21 @@ ${colors.bright}Template Types:${colors.reset}
395
541
  https://github.com/user/repo GitHub URL
396
542
 
397
543
  ${colors.bright}Examples:${colors.reset}
398
- npx uniweb create my-project
399
- npx uniweb create my-project --template single
400
- npx uniweb create my-project --template marketing
401
- npx uniweb create my-project --template marketing --variant tailwind3
402
- npx uniweb create my-project --template ./my-template
403
- npx uniweb create my-project --template github:myorg/template
544
+ npx uniweb create my-project # Default (foundation + site + starter)
545
+ npx uniweb create my-project --template blank # Blank workspace
546
+ npx uniweb create my-project --template marketing # Official template
547
+ npx uniweb create my-project --template ./my-template # Local template
548
+
549
+ cd my-project
550
+ npx uniweb add foundation marketing # Add foundations/marketing/
551
+ npx uniweb add foundation marketing --from marketing # Scaffold + marketing sections
552
+ npx uniweb add site blog --foundation marketing # Add sites/blog/ wired to marketing
553
+ npx uniweb add site blog --from docs --foundation blog # Scaffold + docs pages
554
+ npx uniweb add extension effects --site site # Add extensions/effects/
555
+
404
556
  npx uniweb build
405
557
  npx uniweb build --target foundation
406
- npx uniweb build # Auto-prerenders if site.yml has build.prerender: true
407
- npx uniweb build --no-prerender # Skip prerendering even if enabled in config
408
- cd foundation && npx uniweb docs # Generate COMPONENTS.md
558
+ cd foundation && npx uniweb docs # Generate COMPONENTS.md
409
559
  `)
410
560
  }
411
561
 
@@ -12,12 +12,6 @@ import { fetchNpmTemplate } from './fetchers/npm.js'
12
12
  import { fetchGitHubTemplate } from './fetchers/github.js'
13
13
  import { fetchOfficialTemplate, listOfficialTemplates } from './fetchers/release.js'
14
14
  import { validateTemplate } from './validator.js'
15
- import {
16
- copyTemplateDirectory,
17
- registerVersions,
18
- getMissingVersions,
19
- clearMissingVersions
20
- } from './processor.js'
21
15
 
22
16
  /**
23
17
  * Resolve a template identifier and return the template path
@@ -163,100 +157,12 @@ async function resolveLocalTemplate(templatePath, options = {}) {
163
157
  }
164
158
  }
165
159
 
166
- /**
167
- * Apply a template to a target directory
168
- *
169
- * @param {string} templatePath - Path to the template root (contains template.json)
170
- * @param {string} targetPath - Destination directory for the scaffolded project
171
- * @param {Object} data - Template variables
172
- * @param {Object} options - Apply options
173
- * @param {string} options.variant - Template variant to use
174
- * @param {string} options.uniwebVersion - Current Uniweb version for compatibility check
175
- * @param {Function} options.onWarning - Warning callback
176
- * @param {Function} options.onProgress - Progress callback
177
- * @returns {Promise<Object>} Template metadata
178
- */
179
- export async function applyTemplate(templatePath, targetPath, data = {}, options = {}) {
180
- const { uniwebVersion, variant, onWarning, onProgress } = options
181
-
182
- // Validate the template
183
- const metadata = await validateTemplate(templatePath, { uniwebVersion })
184
-
185
- // Register versions for the {{version}} helper
186
- if (data.versions) {
187
- registerVersions(data.versions)
188
- }
189
-
190
- // Apply default variables
191
- const templateData = {
192
- year: new Date().getFullYear(),
193
- ...data
194
- }
195
-
196
- // Copy template files
197
- await copyTemplateDirectory(
198
- metadata.templateDir,
199
- targetPath,
200
- templateData,
201
- { variant, onWarning, onProgress }
202
- )
203
-
204
- // Check for missing versions and warn
205
- const missingVersions = getMissingVersions()
206
- if (missingVersions.length > 0 && onWarning) {
207
- onWarning(`Missing version data for packages: ${missingVersions.join(', ')}. Using fallback version.`)
208
- }
209
- clearMissingVersions()
210
-
211
- return metadata
212
- }
213
-
214
- /**
215
- * Apply an external template to a target directory
216
- *
217
- * @param {Object} resolved - Resolved template from resolveTemplate()
218
- * @param {string} targetPath - Target directory
219
- * @param {Object} data - Template variables
220
- * @param {Object} options - Apply options
221
- */
222
- export async function applyExternalTemplate(resolved, targetPath, data, options = {}) {
223
- const { variant, onProgress, onWarning } = options
224
-
225
- try {
226
- const metadata = await applyTemplate(
227
- resolved.path,
228
- targetPath,
229
- data,
230
- { variant, onProgress, onWarning }
231
- )
232
-
233
- return metadata
234
- } finally {
235
- // Clean up temp directory if there is one
236
- if (resolved.cleanup) {
237
- await resolved.cleanup()
238
- }
239
- }
240
- }
241
-
242
160
  /**
243
161
  * List all available templates
244
162
  */
245
163
  export async function listAvailableTemplates() {
246
164
  const templates = []
247
165
 
248
- // Built-in templates
249
- for (const name of BUILTIN_TEMPLATES) {
250
- templates.push({
251
- type: 'builtin',
252
- id: name,
253
- name: name.charAt(0).toUpperCase() + name.slice(1),
254
- description: name === 'single'
255
- ? 'One site + one foundation'
256
- : 'Multiple sites and foundations',
257
- })
258
- }
259
-
260
166
  // Official templates from GitHub releases
261
167
  try {
262
168
  const official = await listOfficialTemplates()