uniweb 0.7.1 → 0.7.2

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 +4 -3
  3. package/src/commands/add.js +563 -0
  4. package/src/commands/build.js +49 -6
  5. package/src/commands/doctor.js +181 -2
  6. package/src/index.js +273 -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 +175 -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
@@ -19,9 +19,6 @@ let partialsRegistered = false
19
19
  // Store for version data (set by registerVersions)
20
20
  let versionData = {}
21
21
 
22
- // Track missing versions during processing
23
- const missingVersions = new Set()
24
-
25
22
  // Default fallback version when a package version is unknown
26
23
  const DEFAULT_FALLBACK_VERSION = '^0.1.0'
27
24
 
@@ -32,23 +29,6 @@ const DEFAULT_FALLBACK_VERSION = '^0.1.0'
32
29
  */
33
30
  export function registerVersions(versions) {
34
31
  versionData = versions || {}
35
- missingVersions.clear()
36
- }
37
-
38
- /**
39
- * Get the list of missing versions encountered during processing
40
- *
41
- * @returns {string[]} Array of package names that were missing versions
42
- */
43
- export function getMissingVersions() {
44
- return [...missingVersions]
45
- }
46
-
47
- /**
48
- * Clear the missing versions set
49
- */
50
- export function clearMissingVersions() {
51
- missingVersions.clear()
52
32
  }
53
33
 
54
34
  /**
@@ -100,8 +80,6 @@ Handlebars.registerHelper('version', function(packageName) {
100
80
  return versionData[`@uniweb/${packageName}`]
101
81
  }
102
82
 
103
- // Track the missing version and return a fallback
104
- missingVersions.add(packageName)
105
83
  return DEFAULT_FALLBACK_VERSION
106
84
  })
107
85
 
@@ -195,78 +173,29 @@ async function processFile(sourcePath, targetPath, data, options = {}) {
195
173
  * @param {string} targetPath - Destination directory
196
174
  * @param {Object} data - Template variables
197
175
  * @param {Object} options - Processing options
198
- * @param {string|null} options.variant - Template variant to use
199
- * @param {string|null} options.basePath - Base template to merge with (files copied first)
200
- * @param {boolean} options.isBase - Internal: true when processing base template (allows overwriting)
201
176
  * @param {Function} options.onWarning - Warning callback
202
177
  * @param {Function} options.onProgress - Progress callback
203
178
  */
204
179
  export async function copyTemplateDirectory(sourcePath, targetPath, data, options = {}) {
205
- const { variant = null, basePath = null, isBase = false, onWarning, onProgress } = options
206
-
207
- // If a base template is specified, copy it first (with isBase=true so main can overwrite)
208
- if (basePath && existsSync(basePath)) {
209
- await copyTemplateDirectory(basePath, targetPath, data, { variant, isBase: true, onWarning, onProgress })
210
- }
180
+ const { onWarning, onProgress } = options
211
181
 
212
182
  await fs.mkdir(targetPath, { recursive: true })
213
183
  const entries = await fs.readdir(sourcePath, { withFileTypes: true })
214
184
 
215
- // Build a set of base names that have variant-specific directories
216
- const variantBases = new Set()
217
- for (const entry of entries) {
218
- if (entry.isDirectory()) {
219
- const variantMatch = entry.name.match(/^(.+)\.([^.]+)$/)
220
- if (variantMatch) {
221
- variantBases.add(variantMatch[1]) // e.g., 'foundation' from 'foundation.tailwind4'
222
- }
223
- }
224
- }
225
-
226
- // Options for recursive calls (without basePath to avoid re-copying base at each level)
227
- const recursionOptions = { variant, isBase, onWarning, onProgress }
228
-
229
185
  for (const entry of entries) {
230
186
  const sourceName = entry.name
231
187
 
232
- // Check if this is a variant-specific item (e.g., "dir.variant")
233
- const variantMatch = entry.isDirectory()
234
- ? sourceName.match(/^(.+)\.([^.]+)$/)
235
- : null
236
-
237
188
  if (entry.isDirectory()) {
238
- if (variantMatch) {
239
- const [, baseName, dirVariant] = variantMatch
240
-
241
- // When no variant is specified, skip all variant directories
242
- if (!variant) {
243
- continue
244
- }
245
-
246
- // When variant is specified, skip directories that don't match
247
- if (dirVariant !== variant) {
248
- continue
249
- }
250
-
251
- // Use the base name without variant suffix for the target
252
- const sourceFullPath = path.join(sourcePath, sourceName)
253
- const targetFullPath = path.join(targetPath, baseName)
254
-
255
- await copyTemplateDirectory(sourceFullPath, targetFullPath, data, recursionOptions)
256
- } else {
257
- // Regular directory - skip if a variant override exists and we're using that variant
258
- if (variant && variantBases.has(sourceName)) {
259
- // Skip this directory because a variant-specific version exists
260
- continue
261
- }
262
-
263
- const sourceFullPath = path.join(sourcePath, sourceName)
264
- const targetFullPath = path.join(targetPath, sourceName)
265
-
266
- await copyTemplateDirectory(sourceFullPath, targetFullPath, data, recursionOptions)
267
- }
189
+ const sourceFullPath = path.join(sourcePath, sourceName)
190
+ // Rename _prefix directories to .prefix (e.g., _vscode → .vscode)
191
+ // This allows dotfile directories to be committed without being gitignored
192
+ const targetName = sourceName.startsWith('_') && !sourceName.startsWith('__')
193
+ ? `.${sourceName.slice(1)}`
194
+ : sourceName
195
+ const targetFullPath = path.join(targetPath, targetName)
196
+
197
+ await copyTemplateDirectory(sourceFullPath, targetFullPath, data, { onWarning, onProgress })
268
198
  } else {
269
- // File processing
270
199
  // Skip template.json as it's metadata for the template, not for the output
271
200
  if (sourceName === 'template.json') {
272
201
  continue
@@ -280,12 +209,6 @@ export async function copyTemplateDirectory(sourcePath, targetPath, data, option
280
209
  const sourceFullPath = path.join(sourcePath, sourceName)
281
210
  const targetFullPath = path.join(targetPath, targetName)
282
211
 
283
- // When processing base template, skip if target exists (main template files take precedence)
284
- // When processing main template, overwrite any files from base
285
- if (isBase && existsSync(targetFullPath)) {
286
- continue
287
- }
288
-
289
212
  if (onProgress) {
290
213
  onProgress(`Creating ${targetName}`)
291
214
  }
@@ -2,8 +2,8 @@
2
2
  * Template resolver - parses template identifiers and determines source type
3
3
  */
4
4
 
5
- // Built-in templates (file-based in cli/templates/)
6
- export const BUILTIN_TEMPLATES = ['single', 'multi']
5
+ // Built-in templates (programmatic, not file-based)
6
+ export const BUILTIN_TEMPLATES = ['blank']
7
7
 
8
8
  // Official templates from @uniweb/templates package (downloaded from GitHub releases)
9
9
  export const OFFICIAL_TEMPLATES = ['marketing', 'academic', 'docs', 'international', 'dynamic', 'store', 'extensions']
@@ -11,7 +11,7 @@ export const OFFICIAL_TEMPLATES = ['marketing', 'academic', 'docs', 'internation
11
11
  /**
12
12
  * Parse a template identifier and determine its source type
13
13
  *
14
- * @param {string} identifier - Template identifier (e.g., 'single', 'marketing', 'github:user/repo')
14
+ * @param {string} identifier - Template identifier (e.g., 'blank', 'marketing', 'github:user/repo')
15
15
  * @returns {Object} Parsed template info
16
16
  */
17
17
  export function parseTemplateId(identifier) {
@@ -91,7 +91,7 @@ export class ValidationError extends Error {
91
91
  export const ErrorCodes = {
92
92
  MISSING_TEMPLATE_JSON: 'MISSING_TEMPLATE_JSON',
93
93
  INVALID_TEMPLATE_JSON: 'INVALID_TEMPLATE_JSON',
94
- MISSING_TEMPLATE_DIR: 'MISSING_TEMPLATE_DIR',
94
+ MISSING_CONTENT_DIR: 'MISSING_CONTENT_DIR',
95
95
  MISSING_REQUIRED_FIELD: 'MISSING_REQUIRED_FIELD',
96
96
  VERSION_MISMATCH: 'VERSION_MISMATCH',
97
97
  }
@@ -139,34 +139,76 @@ export async function validateTemplate(templateRoot, options = {}) {
139
139
  )
140
140
  }
141
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)) {
142
+ // Check version compatibility (support both `compatible` and `uniweb` fields)
143
+ const versionRange = metadata.compatible || metadata.uniweb
144
+ if (uniwebVersion && versionRange) {
145
+ if (!satisfiesVersion(uniwebVersion, versionRange)) {
155
146
  throw new ValidationError(
156
- `Template requires Uniweb ${metadata.uniweb}, but current version is ${uniwebVersion}`,
147
+ `Template requires Uniweb ${versionRange}, but current version is ${uniwebVersion}`,
157
148
  ErrorCodes.VERSION_MISMATCH,
158
- { required: metadata.uniweb, current: uniwebVersion }
149
+ { required: versionRange, current: uniwebVersion }
159
150
  )
160
151
  }
161
152
  }
162
153
 
154
+ // Format 2: content template — foundation/ and/or site/ directories alongside template.json
155
+ const contentDirs = resolveContentDirs(templateRoot, metadata)
156
+
157
+ if (contentDirs.length === 0) {
158
+ throw new ValidationError(
159
+ `No content directories found in ${templateRoot}. Templates need foundation/ and/or site/ directories alongside template.json.`,
160
+ ErrorCodes.MISSING_CONTENT_DIR,
161
+ { path: templateRoot }
162
+ )
163
+ }
164
+
163
165
  return {
164
166
  ...metadata,
165
- templateDir,
167
+ format: 2,
168
+ contentDirs,
166
169
  metadataPath
167
170
  }
168
171
  }
169
172
 
173
+ /**
174
+ * Resolve content directories from a format 2 template
175
+ *
176
+ * @param {string} templateRoot - Root of the template (contains template.json)
177
+ * @param {Object} metadata - Parsed template.json
178
+ * @returns {Array<Object>} Content directories: [{ type, name, dir, foundation? }]
179
+ */
180
+ export function resolveContentDirs(templateRoot, metadata) {
181
+ const dirs = []
182
+
183
+ if (metadata.packages) {
184
+ // Multi-package template: iterate declared packages
185
+ for (const pkg of metadata.packages) {
186
+ const dir = path.join(templateRoot, pkg.name)
187
+ if (existsSync(dir)) {
188
+ dirs.push({
189
+ type: pkg.type,
190
+ name: pkg.name,
191
+ dir,
192
+ ...(pkg.foundation ? { foundation: pkg.foundation } : {}),
193
+ })
194
+ }
195
+ }
196
+ } else {
197
+ // Standard template: look for foundation/ and site/
198
+ const foundationDir = path.join(templateRoot, 'foundation')
199
+ if (existsSync(foundationDir)) {
200
+ dirs.push({ type: 'foundation', name: 'foundation', dir: foundationDir })
201
+ }
202
+
203
+ const siteDir = path.join(templateRoot, 'site')
204
+ if (existsSync(siteDir)) {
205
+ dirs.push({ type: 'site', name: 'site', dir: siteDir })
206
+ }
207
+ }
208
+
209
+ return dirs
210
+ }
211
+
170
212
  /**
171
213
  * Get list of available templates in a templates directory
172
214
  *
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Workspace Config Management
3
+ *
4
+ * Read/write pnpm-workspace.yaml and root package.json.
5
+ * Used by both `create` and `add` commands.
6
+ */
7
+
8
+ import { existsSync } from 'node:fs'
9
+ import { readFile, writeFile } from 'node:fs/promises'
10
+ import { join } from 'node:path'
11
+ import yaml from 'js-yaml'
12
+
13
+ /**
14
+ * Read pnpm-workspace.yaml
15
+ * @param {string} rootDir - Workspace root directory
16
+ * @returns {Promise<{packages: string[]}>}
17
+ */
18
+ export async function readWorkspaceConfig(rootDir) {
19
+ const configPath = join(rootDir, 'pnpm-workspace.yaml')
20
+ if (!existsSync(configPath)) {
21
+ return { packages: [] }
22
+ }
23
+ const content = await readFile(configPath, 'utf-8')
24
+ const config = yaml.load(content)
25
+ return { packages: config?.packages || [] }
26
+ }
27
+
28
+ /**
29
+ * Write pnpm-workspace.yaml
30
+ * @param {string} rootDir - Workspace root directory
31
+ * @param {{packages: string[]}} config
32
+ */
33
+ export async function writeWorkspaceConfig(rootDir, config) {
34
+ const configPath = join(rootDir, 'pnpm-workspace.yaml')
35
+ const content = yaml.dump(config, { flowLevel: -1, quotingType: '"' })
36
+ await writeFile(configPath, content)
37
+ }
38
+
39
+ /**
40
+ * Add a glob pattern to pnpm-workspace.yaml if not already present
41
+ * @param {string} rootDir - Workspace root directory
42
+ * @param {string} glob - Glob pattern to add
43
+ */
44
+ export async function addWorkspaceGlob(rootDir, glob) {
45
+ const config = await readWorkspaceConfig(rootDir)
46
+ if (!config.packages.includes(glob)) {
47
+ config.packages.push(glob)
48
+ await writeWorkspaceConfig(rootDir, config)
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Read root package.json
54
+ * @param {string} rootDir - Workspace root directory
55
+ * @returns {Promise<Object>}
56
+ */
57
+ export async function readRootPackageJson(rootDir) {
58
+ const pkgPath = join(rootDir, 'package.json')
59
+ if (!existsSync(pkgPath)) {
60
+ return {}
61
+ }
62
+ return JSON.parse(await readFile(pkgPath, 'utf-8'))
63
+ }
64
+
65
+ /**
66
+ * Write root package.json (2-space indent)
67
+ * @param {string} rootDir - Workspace root directory
68
+ * @param {Object} pkg - Package.json object
69
+ */
70
+ export async function writeRootPackageJson(rootDir, pkg) {
71
+ const pkgPath = join(rootDir, 'package.json')
72
+ await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
73
+ }
74
+
75
+ /**
76
+ * Compute root scripts based on discovered sites
77
+ * @param {Array<{name: string, path: string}>} sites - Discovered sites
78
+ * @returns {Object} Scripts object for package.json
79
+ */
80
+ export function computeRootScripts(sites) {
81
+ const scripts = {
82
+ build: 'uniweb build',
83
+ }
84
+
85
+ if (sites.length === 0) {
86
+ return scripts
87
+ }
88
+
89
+ if (sites.length === 1) {
90
+ scripts.dev = `pnpm --filter ${sites[0].name} dev`
91
+ scripts.preview = `pnpm --filter ${sites[0].name} preview`
92
+ } else {
93
+ // First site gets unqualified dev/preview
94
+ scripts.dev = `pnpm --filter ${sites[0].name} dev`
95
+ scripts.preview = `pnpm --filter ${sites[0].name} preview`
96
+
97
+ // Subsequent sites get qualified dev:{name}/preview:{name}
98
+ for (let i = 1; i < sites.length; i++) {
99
+ scripts[`dev:${sites[i].name}`] = `pnpm --filter ${sites[i].name} dev`
100
+ scripts[`preview:${sites[i].name}`] = `pnpm --filter ${sites[i].name} preview`
101
+ }
102
+ }
103
+
104
+ return scripts
105
+ }
106
+
107
+ /**
108
+ * Update root scripts after adding a new site
109
+ * @param {string} rootDir - Workspace root directory
110
+ * @param {Array<{name: string, path: string}>} sites - All sites (including new one)
111
+ */
112
+ export async function updateRootScripts(rootDir, sites) {
113
+ const pkg = await readRootPackageJson(rootDir)
114
+ const newScripts = computeRootScripts(sites)
115
+
116
+ // If we're adding a second site, rename existing dev/preview to dev:{firstName}
117
+ if (sites.length === 2 && pkg.scripts?.dev) {
118
+ const firstName = sites[0].name
119
+ // Only rename if the existing dev matches the first site
120
+ if (pkg.scripts.dev === `pnpm --filter ${firstName} dev`) {
121
+ pkg.scripts[`dev:${firstName}`] = pkg.scripts.dev
122
+ pkg.scripts[`preview:${firstName}`] = pkg.scripts.preview
123
+ }
124
+ }
125
+
126
+ pkg.scripts = { ...pkg.scripts, ...newScripts }
127
+ await writeRootPackageJson(rootDir, pkg)
128
+ }
129
+
130
+ /**
131
+ * Discover foundations in the workspace
132
+ * @param {string} rootDir - Workspace root directory
133
+ * @returns {Promise<Array<{name: string, path: string}>>}
134
+ */
135
+ export async function discoverFoundations(rootDir) {
136
+ const { packages } = await readWorkspaceConfig(rootDir)
137
+ const foundations = []
138
+
139
+ for (const pattern of packages) {
140
+ const dirs = await resolveGlob(rootDir, pattern)
141
+ for (const dir of dirs) {
142
+ const pkgPath = join(rootDir, dir, 'package.json')
143
+ if (!existsSync(pkgPath)) continue
144
+ try {
145
+ const pkg = JSON.parse(await readFile(pkgPath, 'utf-8'))
146
+ // Foundation: has @uniweb/build in devDeps but NOT @uniweb/runtime in deps
147
+ if (pkg.devDependencies?.['@uniweb/build'] && !pkg.dependencies?.['@uniweb/runtime']) {
148
+ foundations.push({ name: pkg.name, path: dir })
149
+ }
150
+ } catch {
151
+ // skip
152
+ }
153
+ }
154
+ }
155
+
156
+ return foundations
157
+ }
158
+
159
+ /**
160
+ * Discover sites in the workspace
161
+ * @param {string} rootDir - Workspace root directory
162
+ * @returns {Promise<Array<{name: string, path: string}>>}
163
+ */
164
+ export async function discoverSites(rootDir) {
165
+ const { packages } = await readWorkspaceConfig(rootDir)
166
+ const sites = []
167
+
168
+ for (const pattern of packages) {
169
+ const dirs = await resolveGlob(rootDir, pattern)
170
+ for (const dir of dirs) {
171
+ const pkgPath = join(rootDir, dir, 'package.json')
172
+ if (!existsSync(pkgPath)) continue
173
+ try {
174
+ const pkg = JSON.parse(await readFile(pkgPath, 'utf-8'))
175
+ // Site: has @uniweb/runtime in deps
176
+ if (pkg.dependencies?.['@uniweb/runtime']) {
177
+ sites.push({ name: pkg.name, path: dir })
178
+ }
179
+ } catch {
180
+ // skip
181
+ }
182
+ }
183
+ }
184
+
185
+ return sites
186
+ }
187
+
188
+ // Resolve a workspace glob pattern to actual directories
189
+ async function resolveGlob(rootDir, pattern) {
190
+ const clean = pattern.replace(/^["']|["']$/g, '')
191
+
192
+ if (clean.endsWith('/*')) {
193
+ // Pattern like "foundations/*" - list subdirectories
194
+ const baseDir = clean.slice(0, -2)
195
+ const fullPath = join(rootDir, baseDir)
196
+ if (!existsSync(fullPath)) return []
197
+ try {
198
+ const { readdirSync } = await import('node:fs')
199
+ const entries = readdirSync(fullPath, { withFileTypes: true })
200
+ return entries
201
+ .filter(e => e.isDirectory() && !e.name.startsWith('.'))
202
+ .map(e => join(baseDir, e.name))
203
+ } catch {
204
+ return []
205
+ }
206
+ }
207
+
208
+ if (clean.startsWith('*/')) {
209
+ // Pattern like "*/foundation" - find subdirs with this child
210
+ const suffix = clean.slice(2)
211
+ const { readdirSync } = await import('node:fs')
212
+ try {
213
+ const entries = readdirSync(rootDir, { withFileTypes: true })
214
+ return entries
215
+ .filter(e => e.isDirectory() && !e.name.startsWith('.') && e.name !== 'node_modules')
216
+ .filter(e => existsSync(join(rootDir, e.name, suffix)))
217
+ .map(e => join(e.name, suffix))
218
+ } catch {
219
+ return []
220
+ }
221
+ }
222
+
223
+ // Direct path like "foundation" or "site"
224
+ if (existsSync(join(rootDir, clean))) {
225
+ return [clean]
226
+ }
227
+
228
+ return []
229
+ }
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Scaffolding Utilities
3
+ *
4
+ * Shared scaffolding functions used by both `create` and `add` commands.
5
+ * Each function scaffolds a single package from its package template.
6
+ */
7
+
8
+ import fs from 'node:fs/promises'
9
+ import { existsSync, readdirSync } from 'node:fs'
10
+ import { join, dirname } from 'node:path'
11
+ import { fileURLToPath } from 'node:url'
12
+ import { copyTemplateDirectory, registerVersions } from '../templates/processor.js'
13
+ import { getVersionsForTemplates } from '../versions.js'
14
+
15
+ const __dirname = dirname(fileURLToPath(import.meta.url))
16
+ const TEMPLATES_DIR = join(__dirname, '..', '..', 'templates')
17
+ const STARTER_DIR = join(__dirname, '..', '..', 'starter')
18
+
19
+ /**
20
+ * Scaffold a workspace root from the workspace package template
21
+ *
22
+ * @param {string} targetDir - Target directory
23
+ * @param {Object} context - Template context
24
+ * @param {string} context.projectName - Workspace/project name
25
+ * @param {string[]} context.workspaceGlobs - Workspace glob patterns
26
+ * @param {Object} context.scripts - Root package.json scripts
27
+ * @param {Object} [options] - Processing options
28
+ */
29
+ export async function scaffoldWorkspace(targetDir, context, options = {}) {
30
+ registerVersions(getVersionsForTemplates())
31
+
32
+ const templatePath = join(TEMPLATES_DIR, 'workspace')
33
+ await copyTemplateDirectory(templatePath, targetDir, context, {
34
+ onProgress: options.onProgress,
35
+ onWarning: options.onWarning,
36
+ })
37
+ }
38
+
39
+ /**
40
+ * Scaffold a foundation from the foundation package template
41
+ *
42
+ * @param {string} targetDir - Target directory for the foundation
43
+ * @param {Object} context - Template context
44
+ * @param {string} context.name - Package name
45
+ * @param {string} context.projectName - Workspace name
46
+ * @param {boolean} [context.isExtension] - Whether this is an extension
47
+ * @param {Object} [options] - Processing options
48
+ */
49
+ export async function scaffoldFoundation(targetDir, context, options = {}) {
50
+ registerVersions(getVersionsForTemplates())
51
+
52
+ const templatePath = join(TEMPLATES_DIR, 'foundation')
53
+ await copyTemplateDirectory(templatePath, targetDir, context, {
54
+ onProgress: options.onProgress,
55
+ onWarning: options.onWarning,
56
+ })
57
+ }
58
+
59
+ /**
60
+ * Scaffold a site from the site package template
61
+ *
62
+ * @param {string} targetDir - Target directory for the site
63
+ * @param {Object} context - Template context
64
+ * @param {string} context.name - Package name
65
+ * @param {string} context.projectName - Workspace name
66
+ * @param {string} context.foundationName - Foundation package name
67
+ * @param {string} context.foundationPath - Relative file: path to foundation
68
+ * @param {string} [context.foundationRef] - Foundation ref for site.yml (when multiple foundations)
69
+ * @param {Object} [options] - Processing options
70
+ */
71
+ export async function scaffoldSite(targetDir, context, options = {}) {
72
+ registerVersions(getVersionsForTemplates())
73
+
74
+ const templatePath = join(TEMPLATES_DIR, 'site')
75
+ await copyTemplateDirectory(templatePath, targetDir, context, {
76
+ onProgress: options.onProgress,
77
+ onWarning: options.onWarning,
78
+ })
79
+ }
80
+
81
+ /**
82
+ * Apply content overlay from a content directory onto a target
83
+ *
84
+ * Content files overwrite scaffolded defaults. Structural files
85
+ * (package.json, vite.config.js, main.js, index.html) are NOT overwritten.
86
+ *
87
+ * @param {string} contentDir - Source content directory (e.g., starter/foundation/)
88
+ * @param {string} targetDir - Target directory to overlay onto
89
+ * @param {Object} context - Handlebars context for .hbs files
90
+ * @param {Object} [options] - Processing options
91
+ */
92
+ export async function applyContent(contentDir, targetDir, context, options = {}) {
93
+ if (!existsSync(contentDir)) return
94
+
95
+ registerVersions(getVersionsForTemplates())
96
+
97
+ // Structural files that content should never overwrite
98
+ const STRUCTURAL_FILES = new Set([
99
+ 'package.json',
100
+ 'vite.config.js',
101
+ 'main.js',
102
+ 'index.html',
103
+ '.gitignore',
104
+ ])
105
+
106
+ await copyContentRecursive(contentDir, targetDir, context, STRUCTURAL_FILES, options)
107
+ }
108
+
109
+ /**
110
+ * Recursively copy content files, skipping structural files
111
+ */
112
+ async function copyContentRecursive(sourceDir, targetDir, context, structuralFiles, options) {
113
+ await fs.mkdir(targetDir, { recursive: true })
114
+
115
+ const entries = readdirSync(sourceDir, { withFileTypes: true })
116
+
117
+ for (const entry of entries) {
118
+ const sourcePath = join(sourceDir, entry.name)
119
+
120
+ if (entry.isDirectory()) {
121
+ const targetSubDir = join(targetDir, entry.name)
122
+ await copyContentRecursive(sourcePath, targetSubDir, context, structuralFiles, options)
123
+ } else {
124
+ // Determine the output filename (strip .hbs extension)
125
+ const outputName = entry.name.endsWith('.hbs')
126
+ ? entry.name.slice(0, -4)
127
+ : entry.name
128
+
129
+ // Skip structural files
130
+ if (structuralFiles.has(outputName)) continue
131
+
132
+ const targetPath = join(targetDir, outputName)
133
+
134
+ if (entry.name.endsWith('.hbs')) {
135
+ // Process through Handlebars
136
+ const Handlebars = (await import('handlebars')).default
137
+ const content = await fs.readFile(sourcePath, 'utf-8')
138
+ const template = Handlebars.compile(content)
139
+ const result = template(context)
140
+ await fs.writeFile(targetPath, result)
141
+ } else {
142
+ // Copy as-is
143
+ await fs.copyFile(sourcePath, targetPath)
144
+ }
145
+
146
+ if (options.onProgress) {
147
+ options.onProgress(`Creating ${outputName}`)
148
+ }
149
+ }
150
+ }
151
+ }
152
+
153
+ /**
154
+ * Apply the built-in starter content
155
+ *
156
+ * @param {string} projectDir - Root project directory
157
+ * @param {Object} context - Template context
158
+ * @param {Object} [options] - Processing options
159
+ * @param {string} [options.foundationDir] - Foundation directory name (default: 'foundation')
160
+ * @param {string} [options.siteDir] - Site directory name (default: 'site')
161
+ */
162
+ export async function applyStarter(projectDir, context, options = {}) {
163
+ const foundationDir = options.foundationDir || 'foundation'
164
+ const siteDir = options.siteDir || 'site'
165
+
166
+ // Apply foundation starter content
167
+ const foundationContentDir = join(STARTER_DIR, 'foundation')
168
+ const foundationTargetDir = join(projectDir, foundationDir)
169
+ await applyContent(foundationContentDir, foundationTargetDir, context, options)
170
+
171
+ // Apply site starter content
172
+ const siteContentDir = join(STARTER_DIR, 'site')
173
+ const siteTargetDir = join(projectDir, siteDir)
174
+ await applyContent(siteContentDir, siteTargetDir, context, options)
175
+ }
@@ -1,7 +1,7 @@
1
1
  {
2
- "name": "foundation",
2
+ "name": "{{name}}",
3
3
  "version": "0.1.0",
4
- "description": "{{projectName}} foundation - Uniweb component library",
4
+ "description": "{{projectName}} foundation",
5
5
  "type": "module",
6
6
  "main": "./src/_entry.generated.js",
7
7
  "exports": {
@@ -0,0 +1,7 @@
1
+ {{#if isExtension}}
2
+ export default {
3
+ extension: true,
4
+ }
5
+ {{else}}
6
+ export default {}
7
+ {{/if}}
File without changes