uniweb 0.12.19 → 0.12.21
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/README.md +1 -1
- package/package.json +5 -5
- package/src/commands/doctor.js +24 -5
- package/src/commands/publish.js +12 -6
- package/src/commands/rename.js +10 -5
- package/src/commands/update.js +211 -245
- package/src/framework-index.json +8 -8
- package/src/index.js +34 -25
- package/src/utils/config.js +11 -6
- package/src/utils/dep-survey.js +99 -0
- package/src/utils/json-file.js +68 -0
- package/src/utils/pm.js +29 -0
- package/src/utils/scaffold.js +4 -2
- package/src/utils/update-check.js +4 -2
- package/src/versions.js +11 -4
package/src/framework-index.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-05-
|
|
3
|
+
"generatedAt": "2026-05-13T05:21:17.590Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/build": {
|
|
6
|
-
"version": "0.14.
|
|
6
|
+
"version": "0.14.5",
|
|
7
7
|
"path": "framework/build",
|
|
8
8
|
"deps": [
|
|
9
9
|
"@uniweb/content-reader",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
]
|
|
15
15
|
},
|
|
16
16
|
"@uniweb/content-reader": {
|
|
17
|
-
"version": "1.1.
|
|
17
|
+
"version": "1.1.11",
|
|
18
18
|
"path": "framework/content-reader",
|
|
19
19
|
"deps": []
|
|
20
20
|
},
|
|
@@ -42,24 +42,24 @@
|
|
|
42
42
|
"deps": []
|
|
43
43
|
},
|
|
44
44
|
"@uniweb/kit": {
|
|
45
|
-
"version": "0.9.
|
|
45
|
+
"version": "0.9.13",
|
|
46
46
|
"path": "framework/kit",
|
|
47
47
|
"deps": [
|
|
48
48
|
"@uniweb/core"
|
|
49
49
|
]
|
|
50
50
|
},
|
|
51
51
|
"@uniweb/loom": {
|
|
52
|
-
"version": "0.2.
|
|
52
|
+
"version": "0.2.3",
|
|
53
53
|
"path": "framework/loom",
|
|
54
54
|
"deps": []
|
|
55
55
|
},
|
|
56
56
|
"@uniweb/press": {
|
|
57
|
-
"version": "0.4.
|
|
57
|
+
"version": "0.4.6",
|
|
58
58
|
"path": "framework/press",
|
|
59
59
|
"deps": []
|
|
60
60
|
},
|
|
61
61
|
"@uniweb/runtime": {
|
|
62
|
-
"version": "0.8.
|
|
62
|
+
"version": "0.8.14",
|
|
63
63
|
"path": "framework/runtime",
|
|
64
64
|
"deps": [
|
|
65
65
|
"@uniweb/core",
|
|
@@ -92,7 +92,7 @@
|
|
|
92
92
|
"deps": []
|
|
93
93
|
},
|
|
94
94
|
"@uniweb/unipress": {
|
|
95
|
-
"version": "0.4.
|
|
95
|
+
"version": "0.4.10",
|
|
96
96
|
"path": "framework/unipress",
|
|
97
97
|
"deps": [
|
|
98
98
|
"@uniweb/build",
|
package/src/index.js
CHANGED
|
@@ -128,9 +128,10 @@ function getCliVersion() {
|
|
|
128
128
|
* Commands that always run from the global CLI (no project context needed)
|
|
129
129
|
*/
|
|
130
130
|
// Commands that always run from the global CLI, never delegating to a
|
|
131
|
-
// project-local copy. `update` is here because
|
|
132
|
-
//
|
|
133
|
-
//
|
|
131
|
+
// project-local copy. `update` is here because it reconciles the project
|
|
132
|
+
// against *this* CLI's version matrix — when run from a (newer) global
|
|
133
|
+
// install that's the whole point; delegating to the project-local copy
|
|
134
|
+
// would align the project to the version it already has, i.e. a no-op.
|
|
134
135
|
const STANDALONE_COMMANDS = new Set([
|
|
135
136
|
'create', '--help', '-h', '--version', '-v', 'login', 'update',
|
|
136
137
|
])
|
|
@@ -1205,12 +1206,12 @@ ${colors.cyan}${colors.bright}uniweb login${colors.reset} ${colors.dim}— Log i
|
|
|
1205
1206
|
${colors.bright}Usage:${colors.reset}
|
|
1206
1207
|
uniweb login [options]
|
|
1207
1208
|
|
|
1208
|
-
Opens a browser to
|
|
1209
|
+
Opens a browser to www.uniweb.app for OAuth-style login, then captures
|
|
1209
1210
|
the token via a loopback callback. Falls back to a paste-token prompt
|
|
1210
1211
|
if the browser flow fails.
|
|
1211
1212
|
|
|
1212
1213
|
${colors.bright}Options:${colors.reset}
|
|
1213
|
-
--backend <url> Override the auth backend (default: https://
|
|
1214
|
+
--backend <url> Override the auth backend (default: https://www.uniweb.app)
|
|
1214
1215
|
|
|
1215
1216
|
In non-interactive mode (CI / no TTY / --non-interactive), this command
|
|
1216
1217
|
errors out — set the \`UNIWEB_TOKEN\` env var instead, or run \`login\`
|
|
@@ -1284,39 +1285,47 @@ Prints the parsed content shape of a markdown file or folder — the
|
|
|
1284
1285
|
Useful for debugging "why isn't my section getting X?".
|
|
1285
1286
|
`,
|
|
1286
1287
|
update: `
|
|
1287
|
-
${colors.cyan}${colors.bright}uniweb update${colors.reset} ${colors.dim}—
|
|
1288
|
+
${colors.cyan}${colors.bright}uniweb update${colors.reset} ${colors.dim}— Align this project with the running CLI${colors.reset}
|
|
1288
1289
|
|
|
1289
1290
|
${colors.bright}Usage:${colors.reset}
|
|
1290
|
-
uniweb update
|
|
1291
|
+
uniweb update Align deps + refresh AGENTS.md
|
|
1291
1292
|
uniweb update --deps-only Only align workspace @uniweb/* deps
|
|
1292
1293
|
uniweb update --agents-only Only refresh AGENTS.md
|
|
1293
1294
|
uniweb update --no-deps Skip the deps-alignment step
|
|
1294
1295
|
uniweb update --no-agents Skip the AGENTS.md step
|
|
1295
1296
|
uniweb update --dry-run Print survey + would-be writes; no mutations
|
|
1296
1297
|
uniweb update --allow-mismatch Refresh AGENTS.md even if declared deps lag
|
|
1297
|
-
uniweb update --yes
|
|
1298
|
+
uniweb update --yes Don't prompt — apply edits and run the install
|
|
1298
1299
|
|
|
1299
1300
|
${colors.bright}What it does:${colors.reset}
|
|
1300
1301
|
Prints a version survey first (CLI version, AGENTS.md stamp, every
|
|
1301
1302
|
@uniweb/* + uniweb dep declared in workspace package.json files,
|
|
1302
|
-
marked aligned / behind / ahead). Then
|
|
1303
|
-
|
|
1304
|
-
1. ${colors.bright}
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
pnpm
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
not in your
|
|
1303
|
+
marked aligned / behind / ahead). Then two steps:
|
|
1304
|
+
|
|
1305
|
+
1. ${colors.bright}Align workspace deps${colors.reset} to this CLI's bundled version matrix —
|
|
1306
|
+
rewrites the @uniweb/* + uniweb keys in each package.json that lags
|
|
1307
|
+
(deps ahead of the matrix are left alone — never downgraded;
|
|
1308
|
+
existing indentation is preserved), then offers to run the
|
|
1309
|
+
workspace's package manager (lockfile-detected: pnpm-lock.yaml →
|
|
1310
|
+
pnpm, yarn.lock → yarn, package-lock.json → npm). If the install
|
|
1311
|
+
fails, the package.json edits are kept and a revert command printed.
|
|
1312
|
+
2. ${colors.bright}Refresh AGENTS.md${colors.reset} from this CLI's bundled partial. Won't run
|
|
1313
|
+
if deps were edited but not installed (node_modules would be behind
|
|
1314
|
+
package.json), or while declared deps still lag the CLI (would
|
|
1315
|
+
document features not in your packages) — pass --allow-mismatch for
|
|
1316
|
+
the latter.
|
|
1317
|
+
|
|
1318
|
+
${colors.bright}Which matrix?${colors.reset}
|
|
1319
|
+
\`update\` pins to the version matrix *this* CLI shipped with — not
|
|
1320
|
+
necessarily the latest release. To reconcile against the latest release
|
|
1321
|
+
without touching a global install, run \`npx uniweb@latest update\`. This
|
|
1322
|
+
command does NOT update the CLI itself; use your package manager
|
|
1323
|
+
(\`npm i -g uniweb@latest\`, \`pnpm add -g uniweb@latest\`, …).
|
|
1315
1324
|
|
|
1316
1325
|
${colors.bright}Project-local installs:${colors.reset}
|
|
1317
|
-
When
|
|
1318
|
-
|
|
1319
|
-
|
|
1326
|
+
When run from a project-local CLI (in node_modules), it aligns the
|
|
1327
|
+
project to that pinned version — bump \`uniweb\` in package.json (or use
|
|
1328
|
+
\`npx uniweb@latest update\`) to align to something newer.
|
|
1320
1329
|
`,
|
|
1321
1330
|
}
|
|
1322
1331
|
|
|
@@ -1346,7 +1355,7 @@ ${colors.bright}Commands:${colors.reset}
|
|
|
1346
1355
|
inspect <path> Inspect parsed content shape of a markdown file or folder
|
|
1347
1356
|
docs Generate component documentation
|
|
1348
1357
|
doctor Diagnose project configuration issues
|
|
1349
|
-
update
|
|
1358
|
+
update Align workspace deps + AGENTS.md to the running CLI
|
|
1350
1359
|
i18n <cmd> Internationalization (extract, sync, status)
|
|
1351
1360
|
template publish Publish a site as a cloud template
|
|
1352
1361
|
login Log in to your Uniweb account
|
package/src/utils/config.js
CHANGED
|
@@ -20,6 +20,7 @@ import { join } from 'node:path'
|
|
|
20
20
|
import { homedir } from 'node:os'
|
|
21
21
|
import yaml from 'js-yaml'
|
|
22
22
|
import { filterCmd } from './pm.js'
|
|
23
|
+
import { writeJsonPreservingStyleAsync } from './json-file.js'
|
|
23
24
|
|
|
24
25
|
// ── Platform URLs ──────────────────────────────────────────────
|
|
25
26
|
|
|
@@ -27,10 +28,8 @@ import { filterCmd } from './pm.js'
|
|
|
27
28
|
// REGISTRY hosts platform operations (publish, foundations, runtime, admin):
|
|
28
29
|
// moved to hosting.uniweb.app in the CDN migration (Phase 4c, 2026-05-04).
|
|
29
30
|
// BACKEND hosts the PHP user-facing surface (login, account, orgs, billing,
|
|
30
|
-
// publish-authorize)
|
|
31
|
-
|
|
32
|
-
// uniweb.app/api/* when v4 ships. Until then, it stays at hub.uniweb.app.
|
|
33
|
-
const PRODUCTION_BACKEND_URL = 'https://hub.uniweb.app'
|
|
31
|
+
// publish-authorize).
|
|
32
|
+
const PRODUCTION_BACKEND_URL = 'https://www.uniweb.app'
|
|
34
33
|
const PRODUCTION_REGISTRY_URL = 'https://hosting.uniweb.app'
|
|
35
34
|
|
|
36
35
|
/**
|
|
@@ -156,13 +155,19 @@ export async function readRootPackageJson(rootDir) {
|
|
|
156
155
|
}
|
|
157
156
|
|
|
158
157
|
/**
|
|
159
|
-
* Write root package.json
|
|
158
|
+
* Write root package.json, preserving the file's existing indentation
|
|
159
|
+
* (newly scaffolded workspaces use 2-space; we don't reflow whatever's
|
|
160
|
+
* already there).
|
|
160
161
|
* @param {string} rootDir - Workspace root directory
|
|
161
162
|
* @param {Object} pkg - Package.json object
|
|
162
163
|
*/
|
|
163
164
|
export async function writeRootPackageJson(rootDir, pkg) {
|
|
164
165
|
const pkgPath = join(rootDir, 'package.json')
|
|
165
|
-
|
|
166
|
+
if (existsSync(pkgPath)) {
|
|
167
|
+
await writeJsonPreservingStyleAsync(pkgPath, pkg)
|
|
168
|
+
} else {
|
|
169
|
+
await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
|
|
170
|
+
}
|
|
166
171
|
}
|
|
167
172
|
|
|
168
173
|
/**
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace `@uniweb/*` dependency survey.
|
|
3
|
+
*
|
|
4
|
+
* Compares the `@uniweb/*` + `uniweb` versions *declared* in every
|
|
5
|
+
* package.json across a workspace against the running CLI's bundled
|
|
6
|
+
* version matrix (`getResolvedVersions`). Shared by `uniweb update`
|
|
7
|
+
* (which fixes the drift) and `uniweb doctor` (which only reports it) so
|
|
8
|
+
* the two never disagree about what "out of date" means.
|
|
9
|
+
*
|
|
10
|
+
* Comparison is on declared specs, not installed (node_modules) versions
|
|
11
|
+
* — that's what's committed and what `git diff` will show after a fix.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
15
|
+
import { join } from 'node:path'
|
|
16
|
+
import { getResolvedVersions } from '../versions.js'
|
|
17
|
+
import { getWorkspacePackages } from './workspace.js'
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Strip a leading semver range operator (^, ~, >=, <, …) so two specs can
|
|
21
|
+
* be compared by their underlying version. Range expressions like
|
|
22
|
+
* ">=0.5 <0.7" aren't fully parsed — the first version-shaped token wins.
|
|
23
|
+
* Sufficient for `@uniweb/*` deps, which use `^x.y.z` / `x.y.z`.
|
|
24
|
+
* @param {string} spec
|
|
25
|
+
* @returns {string}
|
|
26
|
+
*/
|
|
27
|
+
export function stripVersionRange(spec) {
|
|
28
|
+
return (spec || '').replace(/^[\^~>=<\s]+/, '').trim().split(/\s+/)[0] || ''
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Compare two version specs (range prefix tolerated). Returns 1 / -1 / 0.
|
|
33
|
+
* @param {string} a
|
|
34
|
+
* @param {string} b
|
|
35
|
+
* @returns {number}
|
|
36
|
+
*/
|
|
37
|
+
export function compareSemver(a, b) {
|
|
38
|
+
const pa = stripVersionRange(a).split('.').map(Number)
|
|
39
|
+
const pb = stripVersionRange(b).split('.').map(Number)
|
|
40
|
+
for (let i = 0; i < 3; i++) {
|
|
41
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return 1
|
|
42
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return -1
|
|
43
|
+
}
|
|
44
|
+
return 0
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @typedef {object} DepRow
|
|
49
|
+
* @property {string} relDir Workspace-relative dir, or '(root)'.
|
|
50
|
+
* @property {string} section 'dependencies' | 'devDependencies' | 'peerDependencies'
|
|
51
|
+
* @property {string} name Package name (e.g. '@uniweb/core' or 'uniweb').
|
|
52
|
+
* @property {string} current The spec declared in package.json.
|
|
53
|
+
* @property {string} target The spec the running CLI's matrix wants.
|
|
54
|
+
* @property {'aligned'|'behind'|'ahead'} status current vs target.
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Survey a workspace's declared `@uniweb/*` + `uniweb` deps against the
|
|
59
|
+
* running CLI's bundled matrix.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} workspaceDir Absolute path to the workspace root.
|
|
62
|
+
* @returns {Promise<{ targets: Record<string,string>, rows: DepRow[], anyDrift: boolean, anyAhead: boolean }>}
|
|
63
|
+
* `anyDrift` — at least one dep lags the matrix. `anyAhead` — at least
|
|
64
|
+
* one dep is newer than the matrix.
|
|
65
|
+
*/
|
|
66
|
+
export async function surveyWorkspaceDeps(workspaceDir) {
|
|
67
|
+
const targets = getResolvedVersions()
|
|
68
|
+
const packages = await getWorkspacePackages(workspaceDir)
|
|
69
|
+
const dirs = ['', ...packages]
|
|
70
|
+
const rows = []
|
|
71
|
+
let anyDrift = false
|
|
72
|
+
let anyAhead = false
|
|
73
|
+
|
|
74
|
+
for (const relDir of dirs) {
|
|
75
|
+
const pkgDir = relDir ? join(workspaceDir, relDir) : workspaceDir
|
|
76
|
+
const pkgPath = join(pkgDir, 'package.json')
|
|
77
|
+
if (!existsSync(pkgPath)) continue
|
|
78
|
+
let pkg
|
|
79
|
+
try { pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) } catch { continue }
|
|
80
|
+
|
|
81
|
+
for (const sectionName of ['dependencies', 'devDependencies', 'peerDependencies']) {
|
|
82
|
+
const section = pkg[sectionName]
|
|
83
|
+
if (!section) continue
|
|
84
|
+
for (const [name, current] of Object.entries(section)) {
|
|
85
|
+
if (!(name.startsWith('@uniweb/') || name === 'uniweb')) continue
|
|
86
|
+
const target = targets[name]
|
|
87
|
+
if (!target) continue
|
|
88
|
+
const cmp = compareSemver(target, current)
|
|
89
|
+
let status
|
|
90
|
+
if (cmp > 0) { status = 'behind'; anyDrift = true }
|
|
91
|
+
else if (cmp < 0) { status = 'ahead'; anyAhead = true }
|
|
92
|
+
else { status = 'aligned' }
|
|
93
|
+
rows.push({ relDir: relDir || '(root)', section: sectionName, name, current, target, status })
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { targets, rows, anyDrift, anyAhead }
|
|
99
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Style-preserving JSON file writes.
|
|
3
|
+
*
|
|
4
|
+
* The CLI rewrites `package.json` in several places (`update`, `doctor`,
|
|
5
|
+
* `rename`, `add`). A naive `JSON.stringify(obj, null, 2)` reflows the
|
|
6
|
+
* *entire* file whenever the project happened to use tabs or 4-space
|
|
7
|
+
* indentation — turning a one-key version bump into a hundred-line diff
|
|
8
|
+
* (and a needless merge-conflict surface). `framework/CLAUDE.md` calls
|
|
9
|
+
* this out as an anti-pattern for human commits; the tooling shouldn't do
|
|
10
|
+
* it either. These helpers detect the file's existing indentation and
|
|
11
|
+
* trailing-newline convention and preserve both.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
15
|
+
import { readFile, writeFile } from 'node:fs/promises'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Detect the indentation unit a JSON source string uses.
|
|
19
|
+
* @param {string} src
|
|
20
|
+
* @returns {number|string} A space count, or '\t' for tab-indented files.
|
|
21
|
+
* Defaults to 2 when the file has no indented lines (e.g. `{}`).
|
|
22
|
+
*/
|
|
23
|
+
export function detectJsonIndent(src) {
|
|
24
|
+
const m = src.match(/\n([ \t]+)\S/)
|
|
25
|
+
if (!m) return 2
|
|
26
|
+
const lead = m[1]
|
|
27
|
+
if (lead.includes('\t')) return '\t'
|
|
28
|
+
return lead.length
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Serialize `obj` to JSON using the indentation and trailing-newline
|
|
33
|
+
* convention of `originalSrc` (or of the file currently at `filePath`).
|
|
34
|
+
* @param {object} obj
|
|
35
|
+
* @param {string} originalSrc - The file's current text.
|
|
36
|
+
* @returns {string}
|
|
37
|
+
*/
|
|
38
|
+
export function stringifyJsonLike(obj, originalSrc) {
|
|
39
|
+
const indent = detectJsonIndent(originalSrc)
|
|
40
|
+
const body = JSON.stringify(obj, null, indent)
|
|
41
|
+
return originalSrc.endsWith('\n') ? body + '\n' : body
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Write `obj` to `filePath` as JSON, preserving the file's existing
|
|
46
|
+
* indentation and trailing-newline convention. Pass `originalSrc` when
|
|
47
|
+
* the caller already has the file contents in hand (avoids a re-read);
|
|
48
|
+
* otherwise the file is read to sniff its style.
|
|
49
|
+
* @param {string} filePath
|
|
50
|
+
* @param {object} obj
|
|
51
|
+
* @param {string|null} [originalSrc]
|
|
52
|
+
*/
|
|
53
|
+
export function writeJsonPreservingStyle(filePath, obj, originalSrc = null) {
|
|
54
|
+
const src = originalSrc ?? readFileSync(filePath, 'utf8')
|
|
55
|
+
writeFileSync(filePath, stringifyJsonLike(obj, src))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Async counterpart to {@link writeJsonPreservingStyle}, for the CLI's
|
|
60
|
+
* many `node:fs/promises`-based call sites.
|
|
61
|
+
* @param {string} filePath
|
|
62
|
+
* @param {object} obj
|
|
63
|
+
* @param {string|null} [originalSrc]
|
|
64
|
+
*/
|
|
65
|
+
export async function writeJsonPreservingStyleAsync(filePath, obj, originalSrc = null) {
|
|
66
|
+
const src = originalSrc ?? await readFile(filePath, 'utf8')
|
|
67
|
+
await writeFile(filePath, stringifyJsonLike(obj, src))
|
|
68
|
+
}
|
package/src/utils/pm.js
CHANGED
|
@@ -40,6 +40,35 @@ export function detectWorkspacePm(workspaceRoot) {
|
|
|
40
40
|
return null
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Detect which package manager owns a *globally installed* `uniweb` CLI
|
|
45
|
+
* binary, by inspecting its install path (`process.argv[1]`). pnpm and
|
|
46
|
+
* yarn keep global packages under recognizable directory segments;
|
|
47
|
+
* everything else is assumed to be npm. Only meaningful when the CLI is
|
|
48
|
+
* actually a global install (see index.js::isGlobalInstall) — a
|
|
49
|
+
* project-local or npx-launched copy is updated differently.
|
|
50
|
+
*
|
|
51
|
+
* @returns {'pnpm' | 'yarn' | 'npm'}
|
|
52
|
+
*/
|
|
53
|
+
export function detectGlobalCliPm() {
|
|
54
|
+
const path = (process.argv[1] || '').toLowerCase().replace(/\\/g, '/')
|
|
55
|
+
if (path.includes('/pnpm/')) return 'pnpm'
|
|
56
|
+
if (path.includes('/yarn/')) return 'yarn'
|
|
57
|
+
return 'npm'
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The command to (re)install the latest `uniweb` CLI globally with a
|
|
62
|
+
* given package manager.
|
|
63
|
+
* @param {'pnpm' | 'yarn' | 'npm'} pm
|
|
64
|
+
* @returns {string}
|
|
65
|
+
*/
|
|
66
|
+
export function globalCliUpdateCmd(pm) {
|
|
67
|
+
if (pm === 'pnpm') return 'pnpm add -g uniweb@latest'
|
|
68
|
+
if (pm === 'yarn') return 'yarn global add uniweb@latest'
|
|
69
|
+
return 'npm i -g uniweb@latest'
|
|
70
|
+
}
|
|
71
|
+
|
|
43
72
|
/**
|
|
44
73
|
* Generate a workspace-filtered command.
|
|
45
74
|
* pnpm: "pnpm --filter site dev"
|
package/src/utils/scaffold.js
CHANGED
|
@@ -13,6 +13,7 @@ import yaml from 'js-yaml'
|
|
|
13
13
|
import Handlebars from 'handlebars'
|
|
14
14
|
import { copyTemplateDirectory, enumerateTemplateOutputs, registerVersions } from '../templates/processor.js'
|
|
15
15
|
import { getVersionsForTemplates, getCliVersion } from '../versions.js'
|
|
16
|
+
import { writeJsonPreservingStyleAsync } from './json-file.js'
|
|
16
17
|
|
|
17
18
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
18
19
|
const TEMPLATES_DIR = join(__dirname, '..', '..', 'templates')
|
|
@@ -380,12 +381,13 @@ function resolveDependencyVersion(rawValue) {
|
|
|
380
381
|
*/
|
|
381
382
|
export async function mergeTemplateDependencies(packageJsonPath, deps) {
|
|
382
383
|
if (!deps || Object.keys(deps).length === 0) return
|
|
383
|
-
const
|
|
384
|
+
const src = await fs.readFile(packageJsonPath, 'utf-8')
|
|
385
|
+
const pkg = JSON.parse(src)
|
|
384
386
|
if (!pkg.dependencies) pkg.dependencies = {}
|
|
385
387
|
for (const [name, version] of Object.entries(deps)) {
|
|
386
388
|
if (!pkg.dependencies[name] && !pkg.devDependencies?.[name]) {
|
|
387
389
|
pkg.dependencies[name] = resolveDependencyVersion(version)
|
|
388
390
|
}
|
|
389
391
|
}
|
|
390
|
-
await
|
|
392
|
+
await writeJsonPreservingStyleAsync(packageJsonPath, pkg, src)
|
|
391
393
|
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import { homedir } from 'node:os'
|
|
10
10
|
import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
11
11
|
import { join } from 'node:path'
|
|
12
|
+
import { detectGlobalCliPm, globalCliUpdateCmd } from './pm.js'
|
|
12
13
|
|
|
13
14
|
const CHECK_INTERVAL = 24 * 60 * 60 * 1000 // 1 day
|
|
14
15
|
const STATE_DIR = join(homedir(), '.uniweb')
|
|
@@ -62,14 +63,15 @@ function printNotification(current, latest, tone = 'soft') {
|
|
|
62
63
|
const cyan = '\x1b[36m'
|
|
63
64
|
const dim = '\x1b[2m'
|
|
64
65
|
const reset = '\x1b[0m'
|
|
66
|
+
const updateCmd = globalCliUpdateCmd(detectGlobalCliPm())
|
|
65
67
|
console.error('')
|
|
66
68
|
if (tone === 'eager') {
|
|
67
69
|
console.error(`${yellow}Heads up:${reset} this CLI is ${dim}${current}${reset}; latest is ${cyan}${latest}${reset}.`)
|
|
68
|
-
console.error(`${dim}Templates ship with the CLI — consider updating first:${reset}
|
|
70
|
+
console.error(`${dim}Templates ship with the CLI — consider updating first:${reset} ${updateCmd}`)
|
|
69
71
|
console.error(`${dim}Or run a one-shot fresh:${reset} npx uniweb@latest <command>`)
|
|
70
72
|
} else {
|
|
71
73
|
console.error(`${yellow}Update available:${reset} ${dim}${current}${reset} → ${cyan}${latest}${reset}`)
|
|
72
|
-
console.error(`${dim}Run${reset}
|
|
74
|
+
console.error(`${dim}Run${reset} ${updateCmd} ${dim}to update the CLI${reset}`)
|
|
73
75
|
}
|
|
74
76
|
}
|
|
75
77
|
|
package/src/versions.js
CHANGED
|
@@ -76,9 +76,16 @@ function getFrameworkRoot() {
|
|
|
76
76
|
|
|
77
77
|
/**
|
|
78
78
|
* Read the current on-disk version of a specific `@uniweb/*` package by
|
|
79
|
-
* looking up `framework/<last-segment>/package.json`. Returns
|
|
80
|
-
*
|
|
81
|
-
* disk (i.e. the CLI is running from npm, not from the workspace).
|
|
79
|
+
* looking up `framework/<last-segment>/package.json`. Returns the version
|
|
80
|
+
* string verbatim (e.g. `0.7.11`), or null if the package isn't present
|
|
81
|
+
* on disk (i.e. the CLI is running from npm, not from the workspace).
|
|
82
|
+
*
|
|
83
|
+
* Returns the version *exact*, not as a caret range, to match the shape
|
|
84
|
+
* published-CLI direct deps take after pnpm resolves `workspace:*` at
|
|
85
|
+
* publish time. Both modes converge on identical specs in
|
|
86
|
+
* `getResolvedVersions()`, so `uniweb update` and the scaffolder produce
|
|
87
|
+
* the same `package.json` whether the CLI ran from npm or from this
|
|
88
|
+
* monorepo.
|
|
82
89
|
*/
|
|
83
90
|
function readWorkspaceVersion(packageName) {
|
|
84
91
|
const root = getFrameworkRoot()
|
|
@@ -90,7 +97,7 @@ function readWorkspaceVersion(packageName) {
|
|
|
90
97
|
try {
|
|
91
98
|
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
92
99
|
if (pkg.name === packageName && pkg.version) {
|
|
93
|
-
return
|
|
100
|
+
return pkg.version
|
|
94
101
|
}
|
|
95
102
|
} catch {}
|
|
96
103
|
return null
|