uniweb 0.12.0 → 0.12.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uniweb",
3
- "version": "0.12.0",
3
+ "version": "0.12.2",
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/runtime": "0.8.10",
46
+ "@uniweb/kit": "0.9.9"
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:50:36.314Z",
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,12 +77,12 @@
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
  },
84
84
  "@uniweb/templates": {
85
- "version": "0.7.35",
85
+ "version": "0.7.36",
86
86
  "path": "framework/templates",
87
87
  "deps": []
88
88
  },
@@ -92,7 +92,7 @@
92
92
  "deps": []
93
93
  },
94
94
  "@uniweb/unipress": {
95
- "version": "0.3.0",
95
+ "version": "0.4.1",
96
96
  "path": "framework/unipress",
97
97
  "deps": [
98
98
  "@uniweb/build",
package/src/index.js CHANGED
@@ -26,13 +26,14 @@ import { execSync, spawn as spawnChild } from 'node:child_process'
26
26
  import { resolve, join, relative, dirname } from 'node:path'
27
27
  import { fileURLToPath } from 'node:url'
28
28
  import prompts from 'prompts'
29
- import { doctor } from './commands/doctor.js'
29
+ // `doctor`, `add`, `publish`, and `deploy` are loaded lazily via
30
+ // importProjectCommand() — each imports `@uniweb/build` at the top,
31
+ // so a static import here would crash `npx uniweb@latest create …`
32
+ // (no @uniweb/build in the npx scratch dir) before any command runs.
33
+ // Same pattern as `build` and `docs`.
30
34
  import { i18n } from './commands/i18n.js'
31
35
  import { inspect } from './commands/inspect.js'
32
- import { add } from './commands/add.js'
33
36
  import { login } from './commands/login.js'
34
- import { publish } from './commands/publish.js'
35
- import { deploy } from './commands/deploy.js'
36
37
  import { invite } from './commands/invite.js'
37
38
  import { handoff } from './commands/handoff.js'
38
39
  import { update } from './commands/update.js'
@@ -347,7 +348,11 @@ async function createFromContentTemplate(projectDir, projectName, metadata, temp
347
348
  const contentDir = findContentDirFor(metadata.contentDirs, pkg)
348
349
  if (contentDir) {
349
350
  onProgress?.(`Applying ${metadata.name} content to ${pkg.name}...`)
350
- await applyContent(contentDir.dir, fullPath, { projectName }, { onProgress, onWarning })
351
+ await applyContent(contentDir.dir, fullPath, { projectName }, {
352
+ onProgress,
353
+ onWarning,
354
+ renames: contentDir.renames,
355
+ })
351
356
  }
352
357
 
353
358
  // Merge template dependencies into package.json
@@ -487,8 +492,9 @@ async function main() {
487
492
  return
488
493
  }
489
494
 
490
- // Handle doctor command
495
+ // Handle doctor command (dynamic import — depends on @uniweb/build)
491
496
  if (command === 'doctor') {
497
+ const { doctor } = await importProjectCommand('./commands/doctor.js')
492
498
  await doctor(args.slice(1))
493
499
  return
494
500
  }
@@ -505,20 +511,23 @@ async function main() {
505
511
  return
506
512
  }
507
513
 
508
- // Handle add command
514
+ // Handle add command (dynamic import — depends on @uniweb/build)
509
515
  if (command === 'add') {
516
+ const { add } = await importProjectCommand('./commands/add.js')
510
517
  await add(args.slice(1))
511
518
  return
512
519
  }
513
520
 
514
- // Handle publish command
521
+ // Handle publish command (dynamic import — depends on @uniweb/build)
515
522
  if (command === 'publish') {
523
+ const { publish } = await importProjectCommand('./commands/publish.js')
516
524
  await publish(args.slice(1))
517
525
  return
518
526
  }
519
527
 
520
- // Handle deploy command
528
+ // Handle deploy command (dynamic import — depends on @uniweb/build)
521
529
  if (command === 'deploy') {
530
+ const { deploy } = await importProjectCommand('./commands/deploy.js')
522
531
  await deploy(args.slice(1))
523
532
  return
524
533
  }
@@ -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
  *
@@ -10,7 +10,20 @@ import { existsSync, readdirSync, readFileSync } from 'node:fs'
10
10
  import { readFile } from 'node:fs/promises'
11
11
  import { resolve, dirname, join } from 'node:path'
12
12
  import yaml from 'js-yaml'
13
- import { classifyPackage as classifyPackageSync } from '@uniweb/build'
13
+
14
+ // `classifyPackage` from @uniweb/build is loaded lazily — this module
15
+ // is statically imported by index.js (for `findWorkspaceRoot`), and a
16
+ // top-level @uniweb/build import would crash `npx uniweb@latest create`
17
+ // before any command runs (the npx scratch dir has only the CLI's
18
+ // declared deps; @uniweb/build comes from a project's node_modules).
19
+ let _classifyPackageSync = null
20
+ async function getClassifier() {
21
+ if (!_classifyPackageSync) {
22
+ const mod = await import('@uniweb/build')
23
+ _classifyPackageSync = mod.classifyPackage
24
+ }
25
+ return _classifyPackageSync
26
+ }
14
27
 
15
28
  /**
16
29
  * Check if a directory is a workspace root.
@@ -135,7 +148,8 @@ export async function getWorkspacePackages(workspaceRoot) {
135
148
  * @returns {Promise<'foundation'|'site'|null>}
136
149
  */
137
150
  export async function classifyPackage(packagePath) {
138
- return classifyPackageSync(packagePath)
151
+ const classify = await getClassifier()
152
+ return classify(packagePath)
139
153
  }
140
154
 
141
155
  /**
@@ -144,8 +158,11 @@ export async function classifyPackage(packagePath) {
144
158
  * @returns {Promise<string[]>} - Array of foundation paths (relative to workspace root)
145
159
  */
146
160
  export async function findFoundations(workspaceRoot) {
147
- const packages = await getWorkspacePackages(workspaceRoot)
148
- return packages.filter(pkg => classifyPackageSync(join(workspaceRoot, pkg)) === 'foundation')
161
+ const [packages, classify] = await Promise.all([
162
+ getWorkspacePackages(workspaceRoot),
163
+ getClassifier(),
164
+ ])
165
+ return packages.filter(pkg => classify(join(workspaceRoot, pkg)) === 'foundation')
149
166
  }
150
167
 
151
168
  /**
@@ -154,8 +171,11 @@ export async function findFoundations(workspaceRoot) {
154
171
  * @returns {Promise<string[]>} - Array of site paths (relative to workspace root)
155
172
  */
156
173
  export async function findSites(workspaceRoot) {
157
- const packages = await getWorkspacePackages(workspaceRoot)
158
- return packages.filter(pkg => classifyPackageSync(join(workspaceRoot, pkg)) === 'site')
174
+ const [packages, classify] = await Promise.all([
175
+ getWorkspacePackages(workspaceRoot),
176
+ getClassifier(),
177
+ ])
178
+ return packages.filter(pkg => classify(join(workspaceRoot, pkg)) === 'site')
159
179
  }
160
180
 
161
181
  /**