uniweb 0.12.0 → 0.12.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uniweb",
3
- "version": "0.12.0",
3
+ "version": "0.12.1",
4
4
  "description": "Create structured Vite + React sites with content/code separation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -41,14 +41,14 @@
41
41
  "js-yaml": "^4.1.0",
42
42
  "prompts": "^2.4.2",
43
43
  "tar": "^7.0.0",
44
- "@uniweb/core": "0.7.8",
45
- "@uniweb/kit": "0.9.8",
46
- "@uniweb/runtime": "0.8.9"
44
+ "@uniweb/core": "0.7.9",
45
+ "@uniweb/kit": "0.9.9",
46
+ "@uniweb/runtime": "0.8.10"
47
47
  },
48
48
  "peerDependencies": {
49
- "@uniweb/build": "0.13.0",
49
+ "@uniweb/build": "0.13.1",
50
50
  "@uniweb/content-reader": "1.1.9",
51
- "@uniweb/semantic-parser": "1.1.15"
51
+ "@uniweb/semantic-parser": "1.1.16"
52
52
  },
53
53
  "peerDependenciesMeta": {
54
54
  "@uniweb/build": {
@@ -730,12 +730,9 @@ async function applyFromTemplate(templateId, packageType, targetDir, projectName
730
730
  const metadata = await validateTemplate(resolved.path, {})
731
731
 
732
732
  // Look in contentDirs for matching package type
733
- let contentDir = null
734
733
  const match = metadata.contentDirs.find(d => d.type === packageType) ||
735
734
  metadata.contentDirs.find(d => d.name === packageType)
736
- if (match) {
737
- contentDir = match.dir
738
- }
735
+ const contentDir = match ? match.dir : null
739
736
 
740
737
  if (contentDir) {
741
738
  info(`Applying ${metadata.name} content...`)
@@ -744,6 +741,7 @@ async function applyFromTemplate(templateId, packageType, targetDir, projectName
744
741
  versions: getVersionsForTemplates(),
745
742
  }, {
746
743
  onProgress: (msg) => info(` ${msg}`),
744
+ renames: match.renames,
747
745
  })
748
746
 
749
747
  // Merge template dependencies
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-04-29T13:02:15.924Z",
3
+ "generatedAt": "2026-04-29T13:42:04.684Z",
4
4
  "packages": {
5
5
  "@uniweb/build": {
6
- "version": "0.13.0",
6
+ "version": "0.13.1",
7
7
  "path": "framework/build",
8
8
  "deps": [
9
9
  "@uniweb/content-reader",
@@ -24,7 +24,7 @@
24
24
  "deps": []
25
25
  },
26
26
  "@uniweb/core": {
27
- "version": "0.7.8",
27
+ "version": "0.7.9",
28
28
  "path": "framework/core",
29
29
  "deps": [
30
30
  "@uniweb/semantic-parser",
@@ -42,7 +42,7 @@
42
42
  "deps": []
43
43
  },
44
44
  "@uniweb/kit": {
45
- "version": "0.9.8",
45
+ "version": "0.9.9",
46
46
  "path": "framework/kit",
47
47
  "deps": [
48
48
  "@uniweb/core"
@@ -59,7 +59,7 @@
59
59
  "deps": []
60
60
  },
61
61
  "@uniweb/runtime": {
62
- "version": "0.8.9",
62
+ "version": "0.8.10",
63
63
  "path": "framework/runtime",
64
64
  "deps": [
65
65
  "@uniweb/core",
@@ -77,7 +77,7 @@
77
77
  "deps": []
78
78
  },
79
79
  "@uniweb/semantic-parser": {
80
- "version": "1.1.15",
80
+ "version": "1.1.16",
81
81
  "path": "framework/semantic-parser",
82
82
  "deps": []
83
83
  },
@@ -92,7 +92,7 @@
92
92
  "deps": []
93
93
  },
94
94
  "@uniweb/unipress": {
95
- "version": "0.3.0",
95
+ "version": "0.4.0",
96
96
  "path": "framework/unipress",
97
97
  "deps": [
98
98
  "@uniweb/build",
package/src/index.js CHANGED
@@ -347,7 +347,11 @@ async function createFromContentTemplate(projectDir, projectName, metadata, temp
347
347
  const contentDir = findContentDirFor(metadata.contentDirs, pkg)
348
348
  if (contentDir) {
349
349
  onProgress?.(`Applying ${metadata.name} content to ${pkg.name}...`)
350
- await applyContent(contentDir.dir, fullPath, { projectName }, { onProgress, onWarning })
350
+ await applyContent(contentDir.dir, fullPath, { projectName }, {
351
+ onProgress,
352
+ onWarning,
353
+ renames: contentDir.renames,
354
+ })
351
355
  }
352
356
 
353
357
  // Merge template dependencies into package.json
@@ -175,7 +175,7 @@ export async function validateTemplate(templateRoot, options = {}) {
175
175
  *
176
176
  * @param {string} templateRoot - Root of the template (contains template.json)
177
177
  * @param {Object} metadata - Parsed template.json
178
- * @returns {Array<Object>} Content directories: [{ type, name, dir, foundation? }]
178
+ * @returns {Array<Object>} Content directories: [{ type, name, dir, foundation?, renames? }]
179
179
  */
180
180
  export function resolveContentDirs(templateRoot, metadata) {
181
181
  const dirs = []
@@ -185,19 +185,25 @@ export function resolveContentDirs(templateRoot, metadata) {
185
185
  for (const pkg of metadata.packages) {
186
186
  const dir = path.join(templateRoot, pkg.name)
187
187
  if (existsSync(dir)) {
188
- dirs.push({
188
+ const entry = {
189
189
  type: pkg.type,
190
190
  name: pkg.name,
191
191
  dir,
192
192
  ...(pkg.foundation ? { foundation: pkg.foundation } : {}),
193
- })
193
+ }
194
+ if (entry.type === 'foundation' || entry.type === 'extension') {
195
+ applyLegacyFoundationLayout(entry)
196
+ }
197
+ dirs.push(entry)
194
198
  }
195
199
  }
196
200
  } else {
197
201
  // Standard template: look for foundation/ and site/
198
202
  const foundationDir = path.join(templateRoot, 'foundation')
199
203
  if (existsSync(foundationDir)) {
200
- dirs.push({ type: 'foundation', name: 'foundation', dir: foundationDir })
204
+ const entry = { type: 'foundation', name: 'foundation', dir: foundationDir }
205
+ applyLegacyFoundationLayout(entry)
206
+ dirs.push(entry)
201
207
  }
202
208
 
203
209
  const siteDir = path.join(templateRoot, 'site')
@@ -209,6 +215,48 @@ export function resolveContentDirs(templateRoot, metadata) {
209
215
  return dirs
210
216
  }
211
217
 
218
+ /**
219
+ * Detect and unwrap the legacy foundation layout.
220
+ *
221
+ * Old templates (published in the `uniweb/templates` releases up to
222
+ * v0.7.x) shipped foundation content nested one level deeper, with the
223
+ * package source under `foundation/src/` and the user-authored
224
+ * declarations file named `foundation.js`:
225
+ *
226
+ * <template>/foundation/src/foundation.js
227
+ * <template>/foundation/src/sections/...
228
+ * <template>/foundation/src/components/...
229
+ *
230
+ * The current layout is flat — the foundation package root contains
231
+ * the source directly, and the declarations file is named `main.js`:
232
+ *
233
+ * <template>/foundation/main.js
234
+ * <template>/foundation/sections/...
235
+ *
236
+ * The CLI scaffolds the new flat shape into the project's `src/`
237
+ * directory, so an unmodified copy of an old-format template would
238
+ * land at `src/src/foundation.js` (extra `src/` layer + old name).
239
+ *
240
+ * This helper detects the legacy marker (`<dir>/src/foundation.js`),
241
+ * mutates the contentDir entry to point at the inner `src/` directory,
242
+ * and records a top-level rename so `foundation.js` is written as
243
+ * `main.js`. Once `uniweb/templates` is republished with the flat
244
+ * layout, this branch becomes a no-op.
245
+ */
246
+ function applyLegacyFoundationLayout(entry) {
247
+ const innerSrc = path.join(entry.dir, 'src')
248
+ if (!existsSync(innerSrc)) return
249
+
250
+ const legacyMain = path.join(innerSrc, 'foundation.js')
251
+ const newMain = path.join(innerSrc, 'main.js')
252
+ if (!existsSync(legacyMain) && !existsSync(newMain)) return
253
+
254
+ entry.dir = innerSrc
255
+ if (existsSync(legacyMain)) {
256
+ entry.renames = { ...(entry.renames || {}), 'foundation.js': 'main.js' }
257
+ }
258
+ }
259
+
212
260
  /**
213
261
  * Get list of available templates in a templates directory
214
262
  *
@@ -106,6 +106,11 @@ export async function scaffoldSite(targetDir, context, options = {}) {
106
106
  * @param {string} targetDir - Target directory to overlay onto
107
107
  * @param {Object} context - Handlebars context for .hbs files
108
108
  * @param {Object} [options] - Processing options
109
+ * @param {Object} [options.renames] - Top-level filename remapping
110
+ * (e.g. `{ 'foundation.js': 'main.js' }`). Applied only at depth 0
111
+ * so renames don't accidentally rewrite same-named files in nested
112
+ * directories. Used to migrate legacy `foundation/foundation.js`
113
+ * templates onto the new flat `src/main.js` layout.
109
114
  */
110
115
  export async function applyContent(contentDir, targetDir, context, options = {}) {
111
116
  if (!existsSync(contentDir)) return
@@ -127,29 +132,41 @@ export async function applyContent(contentDir, targetDir, context, options = {})
127
132
  'site.yml': ['name', 'foundation'],
128
133
  }
129
134
 
130
- await copyContentRecursive(contentDir, targetDir, context, STRUCTURAL_FILES, MERGE_FILES, options)
135
+ await copyContentRecursive(contentDir, targetDir, context, STRUCTURAL_FILES, MERGE_FILES, options, 0)
131
136
  }
132
137
 
133
138
  /**
134
- * Recursively copy content files, skipping structural files
139
+ * Recursively copy content files, skipping structural files.
140
+ *
141
+ * `depth` is tracked so the `renames` map (passed via options) only
142
+ * applies at depth 0 — the top of the content directory. Without this
143
+ * guard, a rename like `foundation.js → main.js` would also rewrite a
144
+ * nested `sections/foo/foundation.js` if one existed, which is not the
145
+ * intent.
135
146
  */
136
- async function copyContentRecursive(sourceDir, targetDir, context, structuralFiles, mergeFiles, options) {
147
+ async function copyContentRecursive(sourceDir, targetDir, context, structuralFiles, mergeFiles, options, depth = 0) {
137
148
  await fs.mkdir(targetDir, { recursive: true })
138
149
 
139
150
  const entries = readdirSync(sourceDir, { withFileTypes: true })
151
+ const renames = (depth === 0 && options.renames) || null
140
152
 
141
153
  for (const entry of entries) {
142
154
  const sourcePath = join(sourceDir, entry.name)
143
155
 
144
156
  if (entry.isDirectory()) {
145
157
  const targetSubDir = join(targetDir, entry.name)
146
- await copyContentRecursive(sourcePath, targetSubDir, context, structuralFiles, mergeFiles, options)
158
+ await copyContentRecursive(sourcePath, targetSubDir, context, structuralFiles, mergeFiles, options, depth + 1)
147
159
  } else {
148
160
  // Determine the output filename (strip .hbs extension)
149
- const outputName = entry.name.endsWith('.hbs')
161
+ let outputName = entry.name.endsWith('.hbs')
150
162
  ? entry.name.slice(0, -4)
151
163
  : entry.name
152
164
 
165
+ // Apply top-level rename (e.g. legacy `foundation.js` → `main.js`)
166
+ if (renames && Object.prototype.hasOwnProperty.call(renames, outputName)) {
167
+ outputName = renames[outputName]
168
+ }
169
+
153
170
  // Skip structural files
154
171
  if (structuralFiles.has(outputName)) continue
155
172
 
@@ -190,8 +207,17 @@ async function copyContentRecursive(sourceDir, targetDir, context, structuralFil
190
207
  for (const key of preserveKeys) {
191
208
  if (existing[key] === undefined) continue
192
209
  const baseLine = matchTopLevelLine(existingContent, key)
193
- if (baseLine) {
210
+ if (!baseLine) continue
211
+ // If the new content carries the key, replace its line with
212
+ // the scaffolded value (preserving the user's project/foundation
213
+ // choice). Otherwise insert the line — older content templates
214
+ // (notably `docs/site/site.yml.hbs`) omit `foundation:` entirely,
215
+ // and dropping it leaves the site without a foundation ref so
216
+ // the entry's `import '#foundation/styles'` fails at build time.
217
+ if (matchTopLevelLine(merged, key)) {
194
218
  merged = replaceTopLevelLine(merged, key, baseLine)
219
+ } else {
220
+ merged = insertTopLevelLine(merged, baseLine)
195
221
  }
196
222
  }
197
223
 
@@ -213,14 +239,20 @@ async function copyContentRecursive(sourceDir, targetDir, context, structuralFil
213
239
  /**
214
240
  * Apply the built-in starter content
215
241
  *
242
+ * The starter ships its foundation content under `cli/starter/foundation/`
243
+ * (a description of the *kind* of content), and is applied into whatever
244
+ * folder the workspace uses for the foundation package — which is `src/`
245
+ * in the current single-foundation layout, not `foundation/`. Callers
246
+ * can override via options when scaffolding multi-foundation workspaces.
247
+ *
216
248
  * @param {string} projectDir - Root project directory
217
249
  * @param {Object} context - Template context
218
250
  * @param {Object} [options] - Processing options
219
- * @param {string} [options.foundationDir] - Foundation directory name (default: 'foundation')
251
+ * @param {string} [options.foundationDir] - Foundation directory name (default: 'src')
220
252
  * @param {string} [options.siteDir] - Site directory name (default: 'site')
221
253
  */
222
254
  export async function applyStarter(projectDir, context, options = {}) {
223
- const foundationDir = options.foundationDir || 'foundation'
255
+ const foundationDir = options.foundationDir || 'src'
224
256
  const siteDir = options.siteDir || 'site'
225
257
 
226
258
  // Apply foundation starter content
@@ -266,6 +298,25 @@ function replaceTopLevelLine(content, key, replacement) {
266
298
  )
267
299
  }
268
300
 
301
+ /**
302
+ * Insert a top-level YAML line into a content string.
303
+ *
304
+ * Used by the merge path when the content template omits a key that
305
+ * the scaffolded base file declared (e.g. an older `site.yml.hbs` with
306
+ * no `foundation:` line). Inserts immediately after the `name:` line
307
+ * if one exists — that's the conventional position for site
308
+ * configuration — and otherwise prepends to the file. The original
309
+ * trailing newline (or lack thereof) of the file is preserved.
310
+ */
311
+ function insertTopLevelLine(content, line) {
312
+ const nameMatch = content.match(/^name:.*$/m)
313
+ if (nameMatch) {
314
+ const idx = nameMatch.index + nameMatch[0].length
315
+ return content.slice(0, idx) + '\n' + line + content.slice(idx)
316
+ }
317
+ return line + '\n' + content
318
+ }
319
+
269
320
  /**
270
321
  * Resolve a dependency version string from a template.json entry.
271
322
  *