uniweb 1.21.5 → 1.21.6
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 +4 -4
- package/partials/agents.md +2 -2
- package/src/commands/doctor.js +39 -4
- package/src/commands/status.js +70 -7
- package/src/framework-index.json +5 -5
- package/src/index.js +3 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.6",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,13 +41,13 @@
|
|
|
41
41
|
"js-yaml": "^4.3.2",
|
|
42
42
|
"prompts": "^2.4.2",
|
|
43
43
|
"tar": "^7.5.21",
|
|
44
|
-
"@uniweb/
|
|
44
|
+
"@uniweb/core": "~1.4.8",
|
|
45
45
|
"@uniweb/schemas": "~1.6.5",
|
|
46
46
|
"@uniweb/semantic-parser": "~1.13.0",
|
|
47
|
-
"@uniweb/
|
|
47
|
+
"@uniweb/content-writer": "~1.3.1"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@uniweb/build": "~1.10.
|
|
50
|
+
"@uniweb/build": "~1.10.6",
|
|
51
51
|
"@uniweb/semantic-parser": "~1.13.0",
|
|
52
52
|
"@uniweb/content-reader": "~1.5.7"
|
|
53
53
|
},
|
package/partials/agents.md
CHANGED
|
@@ -67,7 +67,7 @@ https://uniweb.io/llms.txt
|
|
|
67
67
|
|
|
68
68
|
It lists every documentation page with a one-line description and a fetchable link. Go there whenever your question isn't answered in this file — and *especially* when you don't know whether a feature exists at all. That's where guessing does the most damage: **a 404 on a path you invented tells you your guess was wrong, not that the feature is missing.** Concluding "the framework doesn't support X" from a failed fetch is a mistake worth actively guarding against; the index answers the existence question directly.
|
|
69
69
|
|
|
70
|
-
Then **follow the link the index gives you** rather than constructing one yourself. The index is the source of truth for where pages live, so it stays correct when the site's URL conventions change. Each link it gives is the page as Markdown
|
|
70
|
+
Then **follow the link the index gives you** rather than constructing one yourself. The index is the source of truth for where pages live, so it stays correct when the site's URL conventions change. Each link it gives is the page as Markdown, and a page's own address returns the same Markdown to a request sent with `Accept: text/markdown`.
|
|
71
71
|
|
|
72
72
|
**Clone the repo instead** when you expect several lookups, or may lose network access later:
|
|
73
73
|
|
|
@@ -2862,7 +2862,7 @@ uniweb <command> --help # Per-command flags — no side effects. Prefe
|
|
|
2862
2862
|
| Any static host at all | `uniweb export` | full control; no Uniweb account needed |
|
|
2863
2863
|
| Uniweb Cloud | `uniweb publish` | teams with non-technical content authors, or client work |
|
|
2864
2864
|
|
|
2865
|
-
`uniweb deploy` never assumes a host: with nothing configured it opens a picker listing only destinations it can act on, and records the choice in `deploy.yml` so later runs go straight there. On Cloudflare Pages / Netlify / Vercel, `add ci` also adds per-PR previews that comment the URL. Adapters: `github-pages`, `cloudflare-pages`, `netlify`, `vercel`, plus `s3-cloudfront` for `deploy`. That record is `deploy.yml`, beside `site.yml` — **the CLI writes it, you don't create it**: the first successful deploy scaffolds the whole file, and later deploys rewrite only its `deploys:` block. Edit `targets:` when you want to change where a site ships. Host credentials come from the environment, never from that committed file.
|
|
2865
|
+
`uniweb deploy` never assumes a host: with nothing configured it opens a picker listing only destinations it can act on, and records the choice in `deploy.yml` so later runs go straight there. On Cloudflare Pages / Netlify / Vercel, `add ci` also adds per-PR previews that comment the URL. GitHub runs workflows only from the top of the repository: in a project that is a folder of a larger repository, the project's own `.github/` never runs, and `uniweb status` says so — as it says when a target in `deploy.yml` was never deployed from this copy. Adapters: `github-pages`, `cloudflare-pages`, `netlify`, `vercel`, plus `s3-cloudfront` for `deploy`. That record is `deploy.yml`, beside `site.yml` — **the CLI writes it, you don't create it**: the first successful deploy scaffolds the whole file, and later deploys rewrite only its `deploys:` block. Edit `targets:` when you want to change where a site ships. Host credentials come from the environment, never from that committed file.
|
|
2866
2866
|
|
|
2867
2867
|
Foundations have their own free path too: `uniweb add ci --target foundation` publishes to permanent versioned URLs on GitHub Pages.
|
|
2868
2868
|
|
package/src/commands/doctor.js
CHANGED
|
@@ -1928,6 +1928,38 @@ async function runDoctor(args = []) {
|
|
|
1928
1928
|
try {
|
|
1929
1929
|
deployYml = await loadDeployYml(sitePath)
|
|
1930
1930
|
} catch (err) {
|
|
1931
|
+
// `saveDeploys:` holding a value of the retired `autoSave:` — the key renamed by hand, its
|
|
1932
|
+
// value kept (measured 2026-10-26 on a copied project) — is the retired key's to repair.
|
|
1933
|
+
let kept = null
|
|
1934
|
+
try {
|
|
1935
|
+
const v = yaml.load(readFileSync(deployYmlPath, 'utf8'))?.saveDeploys
|
|
1936
|
+
kept = v === 'lastDeploy' || v === 'off' ? v : null
|
|
1937
|
+
} catch {
|
|
1938
|
+
kept = null
|
|
1939
|
+
}
|
|
1940
|
+
if (kept) {
|
|
1941
|
+
const id = 'deploy-yml-retired-key'
|
|
1942
|
+
const issue = { id, type: 'error', site: siteName, message: `deploy.yml has \`saveDeploys: ${kept}\`, a value of the retired \`autoSave:\`` }
|
|
1943
|
+
issues.push(issue)
|
|
1944
|
+
error(`[${id}] ${siteName}: deploy.yml has \`saveDeploys: ${kept}\` — a value of the retired \`autoSave:\`, where \`saveDeploys\` is true or false.`)
|
|
1945
|
+
log(
|
|
1946
|
+
` ${colors.dim}${kept === 'off' ? '`autoSave: off` is `saveDeploys: false`.' : '`autoSave: lastDeploy` is what happens anyway — deploys are recorded — so the line goes.'}${colors.reset}`
|
|
1947
|
+
)
|
|
1948
|
+
if (shouldFix(id)) {
|
|
1949
|
+
try {
|
|
1950
|
+
const { retireDeployYmlKeys } = await import('@uniweb/build/site')
|
|
1951
|
+
if (await retireDeployYmlKeys(sitePath)) {
|
|
1952
|
+
issue.fixed = true
|
|
1953
|
+
fixed(`${relative(workspaceDir, deployYmlPath)}: ${kept === 'off' ? 'wrote `saveDeploys: false`' : 'removed `saveDeploys: lastDeploy`'}`)
|
|
1954
|
+
}
|
|
1955
|
+
} catch (fixErr) {
|
|
1956
|
+
warn(`[${id}] could not rewrite ${relative(workspaceDir, deployYmlPath)}: ${fixErr.message}`)
|
|
1957
|
+
}
|
|
1958
|
+
} else {
|
|
1959
|
+
log(` ${colors.dim}Run \`uniweb doctor --fix ${id}\` to write what it meant, keeping your comments.${colors.reset}`)
|
|
1960
|
+
}
|
|
1961
|
+
continue
|
|
1962
|
+
}
|
|
1931
1963
|
issues.push({
|
|
1932
1964
|
id: 'deploy-yml-malformed',
|
|
1933
1965
|
type: 'error',
|
|
@@ -2013,10 +2045,13 @@ async function runDoctor(args = []) {
|
|
|
2013
2045
|
if (names.length) lost.push(`the last deploy of ${names.map((n) => `\`${n}\``).join(', ')} is recorded only there, where nothing sees it`)
|
|
2014
2046
|
}
|
|
2015
2047
|
if (String(deployYml.autoSave) === 'off') lost.push('`autoSave: off` is ignored, so deploys are recorded')
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2048
|
+
// What each one means now — said by value, not as a rename: "`autoSave:` is `saveDeploys:`"
|
|
2049
|
+
// read as one, and a copy came back holding `saveDeploys: lastDeploy` (2026-10-26).
|
|
2050
|
+
const means = {
|
|
2051
|
+
lastDeploy: '`lastDeploy:` is `deploys:` now',
|
|
2052
|
+
autoSave: '`autoSave:` goes — `autoSave: off` is `saveDeploys: false`, and `autoSave: lastDeploy` is what happens anyway'
|
|
2053
|
+
}
|
|
2054
|
+
log(` ${colors.dim}${retired.map((k) => means[k]).join('; ')}${lost.length ? ` — ${lost.join('; ')}` : ''}.${colors.reset}`)
|
|
2020
2055
|
if (shouldFix(id)) {
|
|
2021
2056
|
try {
|
|
2022
2057
|
const { retireDeployYmlKeys } = await import('@uniweb/build/site')
|
package/src/commands/status.js
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
*/
|
|
44
44
|
|
|
45
45
|
import { execFileSync } from 'node:child_process'
|
|
46
|
-
import { existsSync, readFileSync, statSync } from 'node:fs'
|
|
46
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
47
47
|
import { basename, dirname, join, relative, resolve, sep } from 'node:path'
|
|
48
48
|
import yaml from 'js-yaml'
|
|
49
49
|
|
|
@@ -712,7 +712,7 @@ function changeChecks(siteDir, changes, unpushedFiles, proposals) {
|
|
|
712
712
|
if (!moved) {
|
|
713
713
|
return [
|
|
714
714
|
unknown.length
|
|
715
|
-
? { id: 'app.changes', state: 'unknown', summary: `Could not tell what changed on the backend: ${unknown.map((l) => l.detail).join('; ')}.` }
|
|
715
|
+
? { id: 'app.changes', state: 'unknown', summary: `Could not tell what changed on the backend: ${[...new Set(unknown.map((l) => l.detail))].join('; ')}.` }
|
|
716
716
|
: { id: 'app.changes', state: 'ok', summary: 'Nothing changed on the backend since this copy last synced.', detail: { source: 'units' } }
|
|
717
717
|
]
|
|
718
718
|
}
|
|
@@ -1215,18 +1215,34 @@ async function deployChecks(siteDir) {
|
|
|
1215
1215
|
host: t.host,
|
|
1216
1216
|
title,
|
|
1217
1217
|
default: name === defaultName,
|
|
1218
|
+
deployed: Boolean(ci || receipt || retired),
|
|
1218
1219
|
ci: ci ? { file: ci.file, branches: ci.branches } : null,
|
|
1219
1220
|
last: receipt ? { at: receipt.at ?? null, sha: receipt.git?.sha ?? null, url: receipt.url ?? null } : null,
|
|
1220
1221
|
behind,
|
|
1221
1222
|
...(retired ? { retired: true } : {})
|
|
1222
1223
|
})
|
|
1223
1224
|
}
|
|
1225
|
+
// ⭐ A TARGET IS A DESTINATION, NOT A DEPLOY. Only CI or a receipt says the site went there;
|
|
1226
|
+
// a target with neither is named as what it is. ⛔ Until 2026-10-26 every target was "Also
|
|
1227
|
+
// deployed to", and the status opened with it — on a project copied from a repository whose
|
|
1228
|
+
// CI deployed it, after the site had moved to Uniweb Cloud.
|
|
1229
|
+
const deployed = targets.filter((t) => t.deployed)
|
|
1230
|
+
const never = targets.filter((t) => !t.deployed)
|
|
1231
|
+
const parts = []
|
|
1232
|
+
if (deployed.length) parts.push(`Also deployed to ${deployed.map((t) => t.title).join(' and ')}.`)
|
|
1233
|
+
if (never.length) {
|
|
1234
|
+
parts.push(`deploy.yml names ${never.map((t) => t.title).join(' and ')}, never deployed from this copy.`)
|
|
1235
|
+
attention = true
|
|
1236
|
+
lines.push(
|
|
1237
|
+
'A target never deployed from here is a host the site has not gone to yet, or one it left — when it left, remove the target from deploy.yml, and `default:` when it names it.'
|
|
1238
|
+
)
|
|
1239
|
+
}
|
|
1224
1240
|
return {
|
|
1225
1241
|
checks: [
|
|
1226
1242
|
{
|
|
1227
1243
|
id: 'hosting.deploys',
|
|
1228
1244
|
state: attention ? 'attention' : 'ok',
|
|
1229
|
-
summary:
|
|
1245
|
+
summary: parts.join(' '),
|
|
1230
1246
|
lines,
|
|
1231
1247
|
detail: { targets }
|
|
1232
1248
|
}
|
|
@@ -1236,6 +1252,40 @@ async function deployChecks(siteDir) {
|
|
|
1236
1252
|
}
|
|
1237
1253
|
}
|
|
1238
1254
|
|
|
1255
|
+
/**
|
|
1256
|
+
* `hosting.workflows`: GitHub Actions workflows between this site and the top of its
|
|
1257
|
+
* repository — a project's own `.github/workflows` — which GitHub never runs: it runs the
|
|
1258
|
+
* repository's top-level workflows alone, so these are files, not CI.
|
|
1259
|
+
*
|
|
1260
|
+
* ⭐ A project copied into another repository brings its `.github/` along (`uniweb add ci`
|
|
1261
|
+
* writes one). Measured 2026-10-26 on a repository of client projects: a site that had moved to
|
|
1262
|
+
* Uniweb Cloud still carried the GitHub Pages workflow that used to deploy it.
|
|
1263
|
+
*/
|
|
1264
|
+
function workflowsCheck(siteDir) {
|
|
1265
|
+
const repo = repoOf(siteDir)
|
|
1266
|
+
if (!repo) return null
|
|
1267
|
+
const found = []
|
|
1268
|
+
for (let d = siteDir; d !== repo.top && d.startsWith(repo.top + sep); d = dirname(d)) {
|
|
1269
|
+
const dir = join(d, '.github', 'workflows')
|
|
1270
|
+
let names
|
|
1271
|
+
try {
|
|
1272
|
+
names = readdirSync(dir).filter((n) => /\.ya?ml$/.test(n))
|
|
1273
|
+
} catch {
|
|
1274
|
+
continue
|
|
1275
|
+
}
|
|
1276
|
+
for (const n of names.sort()) found.push(relative(siteDir, join(dir, n)))
|
|
1277
|
+
}
|
|
1278
|
+
if (!found.length) return { id: 'hosting.workflows', state: 'ok', summary: 'No workflow here that GitHub would not run.', quiet: true }
|
|
1279
|
+
const n = found.length
|
|
1280
|
+
return {
|
|
1281
|
+
id: 'hosting.workflows',
|
|
1282
|
+
state: 'attention',
|
|
1283
|
+
summary: `${count(n, 'workflow')} here never ${n === 1 ? 'runs' : 'run'}: GitHub runs workflows only from the top of the repository, ${relative(siteDir, join(repo.top, '.github', 'workflows'))}.`,
|
|
1284
|
+
files: found,
|
|
1285
|
+
lines: ['Most likely left from the repository this project came from. When the site no longer deploys that way, the folder can go — ask the person first.']
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1239
1289
|
const list = (names) => names.map((n) => `\`${n}\``).join(', ')
|
|
1240
1290
|
|
|
1241
1291
|
/**
|
|
@@ -1305,7 +1355,8 @@ function summaryLine({ name, synced, live, targets, origin }) {
|
|
|
1305
1355
|
const parts = [synced.length ? `synced with ${synced.join(' and ')}` : 'not synced with a backend']
|
|
1306
1356
|
if (live?.published) parts.push(`live on ${cloudName(origin)}${live.url ? ` at ${live.url}` : ''}`)
|
|
1307
1357
|
else if (live) parts.push(`not live on ${cloudName(origin)} yet`)
|
|
1308
|
-
|
|
1358
|
+
const deployed = targets.filter((t) => t.deployed)
|
|
1359
|
+
if (deployed.length) parts.push(`${live?.published ? 'also deployed' : 'deployed'} to ${deployed.map((t) => t.title).join(' and ')}`)
|
|
1309
1360
|
else if (!synced.length) parts.push('not hosted anywhere yet')
|
|
1310
1361
|
return `${name} — ${parts.join(' · ')}`
|
|
1311
1362
|
}
|
|
@@ -1399,7 +1450,8 @@ async function workspaceOverview(root, { json }) {
|
|
|
1399
1450
|
})
|
|
1400
1451
|
const languages = normalizeLanguageList(yml.languages)
|
|
1401
1452
|
const parts = [synced.length ? `synced with ${synced.join(' and ')}` : 'not synced']
|
|
1402
|
-
|
|
1453
|
+
const deployed = targets.filter((t) => t.deployed)
|
|
1454
|
+
if (deployed.length) parts.push(`deployed to ${deployed.map((t) => t.title).join(' and ')}`)
|
|
1403
1455
|
parts.push(
|
|
1404
1456
|
fnd.kind === 'local'
|
|
1405
1457
|
? `foundation ${fnd.name}`
|
|
@@ -1414,7 +1466,7 @@ async function workspaceOverview(root, { json }) {
|
|
|
1414
1466
|
name: s.name,
|
|
1415
1467
|
summary: `${s.path} — ${parts.join(' · ')}`,
|
|
1416
1468
|
synced,
|
|
1417
|
-
deploys: targets.map(({ name, host, title, default: isDefault, ci }) => ({ name, host, title, default: isDefault, ci })),
|
|
1469
|
+
deploys: targets.map(({ name, host, title, default: isDefault, deployed: was, ci }) => ({ name, host, title, default: isDefault, deployed: was, ci })),
|
|
1418
1470
|
foundation: { kind: fnd.kind, name: fnd.name ?? null, path: fnd.path ?? null },
|
|
1419
1471
|
mounts,
|
|
1420
1472
|
languages
|
|
@@ -1508,7 +1560,18 @@ export async function status(args = []) {
|
|
|
1508
1560
|
} finally {
|
|
1509
1561
|
Object.assign(console, saved)
|
|
1510
1562
|
}
|
|
1511
|
-
const
|
|
1563
|
+
const workflows = workflowsCheck(siteDir)
|
|
1564
|
+
const checks = [
|
|
1565
|
+
...project.checks,
|
|
1566
|
+
...git.checks,
|
|
1567
|
+
...app.checks,
|
|
1568
|
+
...deploys.checks,
|
|
1569
|
+
...(workflows ? [workflows] : []),
|
|
1570
|
+
...services.checks,
|
|
1571
|
+
...version.checks,
|
|
1572
|
+
...health.checks,
|
|
1573
|
+
dev
|
|
1574
|
+
]
|
|
1512
1575
|
const next = planNext([...git.proposals, ...app.proposals, ...deploys.proposals, ...services.proposals, ...version.proposals, ...health.proposals])
|
|
1513
1576
|
|
|
1514
1577
|
const root = findWorkspaceRoot(siteDir) || siteDir
|
package/src/framework-index.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-10-
|
|
3
|
+
"generatedAt": "2026-10-26T21:28:38.450Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/api": {
|
|
6
6
|
"version": "1.6.4"
|
|
7
7
|
},
|
|
8
8
|
"@uniweb/build": {
|
|
9
|
-
"version": "1.10.
|
|
9
|
+
"version": "1.10.6"
|
|
10
10
|
},
|
|
11
11
|
"@uniweb/content-reader": {
|
|
12
12
|
"version": "1.5.7"
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"version": "1.3.1"
|
|
16
16
|
},
|
|
17
17
|
"@uniweb/core": {
|
|
18
|
-
"version": "1.4.
|
|
18
|
+
"version": "1.4.8"
|
|
19
19
|
},
|
|
20
20
|
"@uniweb/frame-bridge": {
|
|
21
21
|
"version": "1.0.2"
|
|
@@ -33,10 +33,10 @@
|
|
|
33
33
|
"version": "1.1.4"
|
|
34
34
|
},
|
|
35
35
|
"@uniweb/projections": {
|
|
36
|
-
"version": "1.2.
|
|
36
|
+
"version": "1.2.2"
|
|
37
37
|
},
|
|
38
38
|
"@uniweb/runtime": {
|
|
39
|
-
"version": "1.2.
|
|
39
|
+
"version": "1.2.4"
|
|
40
40
|
},
|
|
41
41
|
"@uniweb/scene": {
|
|
42
42
|
"version": "1.0.1"
|
package/src/index.js
CHANGED
|
@@ -1789,8 +1789,9 @@ where, live where, deployed where — and checks:
|
|
|
1789
1789
|
app the session, what changed on the backend since this copy last synced —
|
|
1790
1790
|
the files a pull would change — what is not pushed, who has the site
|
|
1791
1791
|
open in the app, the foundation
|
|
1792
|
-
hosting where the site is live, the other hosts deploy.yml lists (by CI
|
|
1793
|
-
|
|
1792
|
+
hosting where the site is live, the other hosts deploy.yml lists (by CI, by hand
|
|
1793
|
+
and how far behind, or never from this copy), workflows GitHub never runs
|
|
1794
|
+
(a project's own .github/), and the services the live site has
|
|
1794
1795
|
uniweb whether a newer release is out
|
|
1795
1796
|
health doctor's findings
|
|
1796
1797
|
dev the dev server
|