uniweb 0.56.9 → 0.56.10

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.56.9",
3
+ "version": "0.56.10",
4
4
  "description": "Create structured Vite + React sites with content/code separation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -42,15 +42,15 @@
42
42
  "prompts": "^2.4.2",
43
43
  "tar": "^7.0.0",
44
44
  "@uniweb/core": "^0.29.4",
45
- "@uniweb/runtime": "^0.26.4",
46
- "@uniweb/semantic-parser": "^1.4.1",
47
- "@uniweb/schemas": "^0.3.2",
45
+ "@uniweb/kit": "^0.19.3",
46
+ "@uniweb/schemas": "^0.3.3",
48
47
  "@uniweb/content-writer": "^0.3.4",
49
- "@uniweb/kit": "^0.19.3"
48
+ "@uniweb/semantic-parser": "^1.4.1",
49
+ "@uniweb/runtime": "^0.26.4"
50
50
  },
51
51
  "peerDependencies": {
52
- "@uniweb/build": "^0.52.6",
53
52
  "@uniweb/content-reader": "^1.2.5",
53
+ "@uniweb/build": "^0.52.6",
54
54
  "@uniweb/semantic-parser": "^1.4.1"
55
55
  },
56
56
  "peerDependenciesMeta": {
@@ -60,6 +60,7 @@ import { writeJsonPreservingStyleAsync } from '../utils/json-file.js'
60
60
  import { getExistingPackageNames, validatePackageName } from '../utils/names.js'
61
61
  import { detectPackageManager, installCmd } from '../utils/pm.js'
62
62
  import { getCliPrefix } from '../utils/interactive.js'
63
+ import { replaceInTopLevelList, setTopLevelScalar } from '../utils/yaml-edit.js'
63
64
 
64
65
  const colors = {
65
66
  reset: '\x1b[0m',
@@ -208,6 +209,24 @@ async function rewritePackageJsonName(pkgPath, newName) {
208
209
  await writeJsonPreservingStyleAsync(pkgPath, pkg, src)
209
210
  }
210
211
 
212
+ /**
213
+ * Stop, before any file moves, when a site's `site.yml` names the package in a
214
+ * form the in-place edit cannot reach. The alternative is rewriting the whole
215
+ * file, which loses the author's comments and formatting — so we refuse and say
216
+ * how to write it instead.
217
+ */
218
+ function refuseUneditableSiteYml(sitePaths, key, hint) {
219
+ if (sitePaths.length === 0) return
220
+ for (const path of sitePaths) {
221
+ error(
222
+ `Cannot rename: ${colors.bright}${path}/site.yml${colors.reset} writes \`${key}:\` in a form this command cannot edit without rewriting the file.`
223
+ )
224
+ }
225
+ log(`Nothing was changed. ${hint}`)
226
+ log('Then run the rename again.')
227
+ process.exit(1)
228
+ }
229
+
211
230
  // ─── Foundation rename ───────────────────────────────────────────
212
231
 
213
232
  async function renameFoundation(rootDir, oldName, newName, prefix) {
@@ -262,7 +281,7 @@ async function renameFoundation(rootDir, oldName, newName, prefix) {
262
281
  for (const site of sites) {
263
282
  const sitePkgPath = join(rootDir, site.path, 'package.json')
264
283
  const siteYmlPath = join(rootDir, site.path, 'site.yml')
265
- let pkg, pkgSrc, ymlData
284
+ let pkg, pkgSrc, ymlText, ymlData
266
285
  try {
267
286
  pkgSrc = await readFile(sitePkgPath, 'utf-8')
268
287
  pkg = JSON.parse(pkgSrc)
@@ -271,7 +290,8 @@ async function renameFoundation(rootDir, oldName, newName, prefix) {
271
290
  pkgSrc = null
272
291
  }
273
292
  try {
274
- ymlData = yaml.load(await readFile(siteYmlPath, 'utf-8')) || {}
293
+ ymlText = await readFile(siteYmlPath, 'utf-8')
294
+ ymlData = yaml.load(ymlText) || {}
275
295
  } catch {
276
296
  ymlData = null
277
297
  }
@@ -285,12 +305,19 @@ async function renameFoundation(rootDir, oldName, newName, prefix) {
285
305
  pkgSrc,
286
306
  sitePkgPath,
287
307
  siteYmlPath,
288
- ymlData,
308
+ // Edited in place — see utils/yaml-edit.js — and computed HERE, before
309
+ // anything moves, so a file it cannot edit stops the rename cleanly.
310
+ newYmlText: ymlMatches ? setTopLevelScalar(ymlText, 'foundation', newName) : null,
289
311
  hasDep,
290
312
  ymlMatches
291
313
  })
292
314
  }
293
315
  }
316
+ refuseUneditableSiteYml(
317
+ affectedSites.filter((s) => s.ymlMatches && s.newYmlText === null).map((s) => s.path),
318
+ 'foundation',
319
+ `Write it on one line: \`foundation: ${oldName}\`.`
320
+ )
294
321
 
295
322
  // ─── Print plan, then execute ────────────────────────────────
296
323
 
@@ -333,11 +360,7 @@ async function renameFoundation(rootDir, oldName, newName, prefix) {
333
360
  await writeJsonPreservingStyleAsync(s.sitePkgPath, s.pkg, s.pkgSrc)
334
361
  }
335
362
  if (s.ymlMatches) {
336
- const newYmlData = { ...s.ymlData, foundation: newName }
337
- await writeFile(
338
- s.siteYmlPath,
339
- yaml.dump(newYmlData, { flowLevel: -1, quotingType: "'" })
340
- )
363
+ await writeFile(s.siteYmlPath, s.newYmlText)
341
364
  }
342
365
  }
343
366
 
@@ -485,9 +508,10 @@ async function renameExtension(rootDir, oldName, newName, prefix) {
485
508
  const affectedSites = []
486
509
  for (const site of sites) {
487
510
  const siteYmlPath = join(rootDir, site.path, 'site.yml')
488
- let ymlData
511
+ let ymlText, ymlData
489
512
  try {
490
- ymlData = yaml.load(await readFile(siteYmlPath, 'utf-8')) || {}
513
+ ymlText = await readFile(siteYmlPath, 'utf-8')
514
+ ymlData = yaml.load(ymlText) || {}
491
515
  } catch {
492
516
  continue
493
517
  }
@@ -496,9 +520,23 @@ async function renameExtension(rootDir, oldName, newName, prefix) {
496
520
  (e) => typeof e === 'string' && e.startsWith(oldUrlPrefix)
497
521
  )
498
522
  if (hits.length > 0) {
499
- affectedSites.push({ site, ymlData, siteYmlPath, hits })
523
+ const replacements = new Map(
524
+ hits.map((e) => [e, newUrlPrefix + e.slice(oldUrlPrefix.length)])
525
+ )
526
+ affectedSites.push({
527
+ site,
528
+ siteYmlPath,
529
+ hits,
530
+ // Edited in place, and computed before anything moves — as for foundations.
531
+ newYmlText: replaceInTopLevelList(ymlText, 'extensions', replacements)
532
+ })
500
533
  }
501
534
  }
535
+ refuseUneditableSiteYml(
536
+ affectedSites.filter((a) => a.newYmlText === null).map((a) => a.site.path),
537
+ 'extensions',
538
+ `Write each entry on a line of its own, e.g. \`- ${oldUrlPrefix}dist/entry.js\`.`
539
+ )
502
540
 
503
541
  log('')
504
542
  log(
@@ -531,16 +569,7 @@ async function renameExtension(rootDir, oldName, newName, prefix) {
531
569
  await rewritePackageJsonName(join(newExtDir, 'package.json'), newName)
532
570
 
533
571
  for (const a of affectedSites) {
534
- const newExts = (a.ymlData.extensions || []).map((e) =>
535
- typeof e === 'string' && e.startsWith(oldUrlPrefix)
536
- ? newUrlPrefix + e.slice(oldUrlPrefix.length)
537
- : e
538
- )
539
- const newYmlData = { ...a.ymlData, extensions: newExts }
540
- await writeFile(
541
- a.siteYmlPath,
542
- yaml.dump(newYmlData, { flowLevel: -1, quotingType: "'" })
543
- )
572
+ await writeFile(a.siteYmlPath, a.newYmlText)
544
573
  }
545
574
 
546
575
  if (folderWillRename) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-09-17T03:24:18.741Z",
3
+ "generatedAt": "2026-09-17T03:53:59.364Z",
4
4
  "packages": {
5
5
  "@uniweb/api": {
6
6
  "version": "0.3.6",
@@ -96,7 +96,7 @@
96
96
  "deps": []
97
97
  },
98
98
  "@uniweb/schemas": {
99
- "version": "0.3.2",
99
+ "version": "0.3.3",
100
100
  "path": "framework/schemas",
101
101
  "deps": []
102
102
  },
@@ -116,7 +116,7 @@
116
116
  "deps": []
117
117
  },
118
118
  "@uniweb/templates": {
119
- "version": "0.14.7",
119
+ "version": "0.14.8",
120
120
  "path": "framework/templates",
121
121
  "deps": []
122
122
  },
@@ -0,0 +1,115 @@
1
+ /**
2
+ * In-place edits to a YAML file a person wrote (`site.yml`), for a command that
3
+ * must change one value and leave everything else as it was: comments, key
4
+ * order, blank lines, quoting, a flow list written on one line.
5
+ *
6
+ * ⛔ Never load a hand-written file and dump it back. js-yaml's `dump` drops every
7
+ * comment and re-flows lists and long strings — `scaffold.js` records what that
8
+ * did to the templates whose comments are the point of them.
9
+ *
10
+ * ⭐ Every edit is VERIFIED: the edited text must parse to exactly the old data
11
+ * with that one value changed. A value written in a form the line-level edit
12
+ * cannot reach (a block scalar, an entry split across lines) returns null
13
+ * rather than a guess, so the caller can refuse before it changes anything.
14
+ */
15
+
16
+ import { isDeepStrictEqual } from 'node:util'
17
+ import yaml from 'js-yaml'
18
+
19
+ const escapeRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
20
+
21
+ /** A scalar as written after `key: `, quoted only when YAML needs it (`@scope/name` does). */
22
+ const scalar = (value) => yaml.dump(value, { lineWidth: -1 }).trim()
23
+
24
+ /**
25
+ * Where a trailing ` # comment` begins in the text after `key:`, outside quotes,
26
+ * including the whitespace before it. -1 when the line has none.
27
+ */
28
+ function commentStart(rest) {
29
+ let quote = null
30
+ for (let i = 0; i < rest.length; i++) {
31
+ const ch = rest[i]
32
+ if (quote) {
33
+ if (ch === quote) quote = null
34
+ } else if (ch === '"' || ch === "'") {
35
+ quote = ch
36
+ } else if (ch === '#' && i > 0 && /\s/.test(rest[i - 1])) {
37
+ let start = i
38
+ while (start > 0 && /\s/.test(rest[start - 1])) start--
39
+ return start
40
+ }
41
+ }
42
+ return -1
43
+ }
44
+
45
+ /** The edited text when it parses to `expected`, else null. */
46
+ function verified(after, expected) {
47
+ try {
48
+ return isDeepStrictEqual(yaml.load(after) ?? {}, expected) ? after : null
49
+ } catch {
50
+ return null
51
+ }
52
+ }
53
+
54
+ function load(text) {
55
+ try {
56
+ const data = yaml.load(text) ?? {}
57
+ return data && typeof data === 'object' && !Array.isArray(data) ? data : null
58
+ } catch {
59
+ return null
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Set a top-level, one-line scalar `key` to `value`, keeping the line's inline
65
+ * comment and every other line as it was.
66
+ *
67
+ * @param {string} text - the file's contents
68
+ * @param {string} key - a top-level key, e.g. `foundation`
69
+ * @param {string} value
70
+ * @returns {string|null} the new contents, or null when the edit cannot be made in place
71
+ */
72
+ export function setTopLevelScalar(text, key, value) {
73
+ const data = load(text)
74
+ if (!data) return null
75
+ const match = new RegExp(`^${escapeRegex(key)}:([^\\n]*)$`, 'm').exec(text)
76
+ if (!match) return null
77
+ const at = commentStart(match[1])
78
+ const comment = at === -1 ? '' : match[1].slice(at)
79
+ const line = `${key}: ${scalar(value)}${comment}`
80
+ const after = text.slice(0, match.index) + line + text.slice(match.index + match[0].length)
81
+ return verified(after, { ...data, [key]: value })
82
+ }
83
+
84
+ /**
85
+ * Replace entries of a top-level list `key` — each `from` value becomes its `to`
86
+ * — wherever an entry is written on a line of its own or inside a one-line flow
87
+ * list, plain or quoted. A value that only CONTAINS `from` is left alone, and so
88
+ * is a comment line.
89
+ *
90
+ * @param {string} text - the file's contents
91
+ * @param {string} key - a top-level list key, e.g. `extensions`
92
+ * @param {Map<string, string>} replacements - old entry → new entry
93
+ * @returns {string|null} the new contents, or null when the edit cannot be made in place
94
+ */
95
+ export function replaceInTopLevelList(text, key, replacements) {
96
+ const data = load(text)
97
+ if (!data || !Array.isArray(data[key])) return null
98
+ const expected = { ...data, [key]: data[key].map((v) => (replacements.has(v) ? replacements.get(v) : v)) }
99
+
100
+ const after = text
101
+ .split('\n')
102
+ .map((line) => {
103
+ if (/^\s*#/.test(line)) return line
104
+ for (const [from, to] of replacements) {
105
+ const f = escapeRegex(from)
106
+ line = line
107
+ .replace(new RegExp(`'${f}'`, 'g'), () => `'${to.replace(/'/g, "''")}'`)
108
+ .replace(new RegExp(`"${f}"`, 'g'), () => JSON.stringify(to))
109
+ .replace(new RegExp(`(^|[\\s\\[,])${f}(?=$|[\\s,\\]])`, 'g'), (_, lead) => lead + to)
110
+ }
111
+ return line
112
+ })
113
+ .join('\n')
114
+ return verified(after, expected)
115
+ }