uniweb 0.12.47 → 0.12.49

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.47",
3
+ "version": "0.12.49",
4
4
  "description": "Create structured Vite + React sites with content/code separation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -46,9 +46,9 @@
46
46
  "@uniweb/runtime": "0.8.26"
47
47
  },
48
48
  "peerDependencies": {
49
+ "@uniweb/build": "0.14.31",
49
50
  "@uniweb/content-reader": "1.1.12",
50
- "@uniweb/semantic-parser": "1.1.17",
51
- "@uniweb/build": "0.14.29"
51
+ "@uniweb/semantic-parser": "1.1.17"
52
52
  },
53
53
  "peerDependenciesMeta": {
54
54
  "@uniweb/build": {
@@ -27,11 +27,11 @@ import {
27
27
  import { discoverFoundations, discoverSites } from '../utils/discover.js'
28
28
  import { validatePackageName, getExistingPackageNames, resolveUniqueName } from '../utils/names.js'
29
29
  import { findWorkspaceRoot } from '../utils/workspace.js'
30
- import { detectPackageManager, filterCmd, installCmd } from '../utils/pm.js'
30
+ import { detectPackageManager, detectWorkspacePm, filterCmd, installCmd } from '../utils/pm.js'
31
31
  import { isNonInteractive, getCliPrefix, stripNonInteractiveFlag, formatOptions } from '../utils/interactive.js'
32
32
  import { resolveTemplate } from '../templates/index.js'
33
33
  import { validateTemplate } from '../templates/validator.js'
34
- import { getVersionsForTemplates, PNPM_VERSION } from '../versions.js'
34
+ import { getVersionsForTemplates, resolveCiPnpmVersion, resolveCiNodeVersion } from '../versions.js'
35
35
 
36
36
  // Colors for terminal output
37
37
  const colors = {
@@ -76,10 +76,41 @@ function parseArgs(args) {
76
76
  // doesn't accidentally swallow the next positional.
77
77
  const BOOLEAN_FLAGS = new Set(['--force', '--no-previews'])
78
78
 
79
+ // Value flags, mapped to their result key. Both spellings are accepted:
80
+ // `--host github-pages` and `--host=github-pages`.
81
+ //
82
+ // The `=` form used to be dropped silently — every branch matched on an
83
+ // exact `args[i] === '--flag'`, so `--host=github-pages` fell through
84
+ // and left `host` null. It went unnoticed because `add ci` defaulted to
85
+ // github-pages when no host was given, making the one command our docs
86
+ // print (`uniweb add ci --host=github-pages`) appear to work while
87
+ // actually ignoring the flag. Requiring a real host surfaced it.
88
+ const VALUE_FLAGS = {
89
+ '--path': 'path',
90
+ '--project': 'project',
91
+ '--foundation': 'foundation',
92
+ '--site': 'site',
93
+ '--from': 'from',
94
+ '--host': 'host',
95
+ '--domain': 'domain',
96
+ '--target': 'target',
97
+ '--project-name': 'projectName',
98
+ }
99
+
100
+ /** Split `--flag=value` into [flag, value]; `--flag` into [flag, null]. */
101
+ const splitFlag = (arg) => {
102
+ const eq = arg.indexOf('=')
103
+ return eq === -1 ? [arg, null] : [arg.slice(0, eq), arg.slice(eq + 1)]
104
+ }
105
+
79
106
  // Find positional name (first arg after subcommand that's not a flag).
80
107
  for (let i = 1; i < args.length; i++) {
81
108
  if (args[i].startsWith('--')) {
82
- if (!BOOLEAN_FLAGS.has(args[i])) i++ // skip flag value
109
+ const [flag, inlineValue] = splitFlag(args[i])
110
+ // Only skip the next arg when the value is genuinely separate.
111
+ // Previously an `=`-form flag also consumed the following arg,
112
+ // which could swallow the positional name.
113
+ if (!BOOLEAN_FLAGS.has(flag) && inlineValue === null) i++
83
114
  continue
84
115
  }
85
116
  if (!result.name) {
@@ -89,27 +120,17 @@ function parseArgs(args) {
89
120
 
90
121
  // Parse flags
91
122
  for (let i = 1; i < args.length; i++) {
92
- if (args[i] === '--path' && args[i + 1]) {
93
- result.path = args[++i]
94
- } else if (args[i] === '--project' && args[i + 1]) {
95
- result.project = args[++i]
96
- } else if (args[i] === '--foundation' && args[i + 1]) {
97
- result.foundation = args[++i]
98
- } else if (args[i] === '--site' && args[i + 1]) {
99
- result.site = args[++i]
100
- } else if (args[i] === '--from' && args[i + 1]) {
101
- result.from = args[++i]
102
- } else if (args[i] === '--host' && args[i + 1]) {
103
- result.host = args[++i]
104
- } else if (args[i] === '--domain' && args[i + 1]) {
105
- result.domain = args[++i]
106
- } else if (args[i] === '--target' && args[i + 1]) {
107
- result.target = args[++i]
108
- } else if (args[i] === '--project-name' && args[i + 1]) {
109
- result.projectName = args[++i]
110
- } else if (args[i] === '--force') {
123
+ const [flag, inlineValue] = splitFlag(args[i])
124
+ const key = VALUE_FLAGS[flag]
125
+ if (key) {
126
+ if (inlineValue !== null) {
127
+ result[key] = inlineValue
128
+ } else if (args[i + 1] !== undefined && !args[i + 1].startsWith('--')) {
129
+ result[key] = args[++i]
130
+ }
131
+ } else if (flag === '--force') {
111
132
  result.force = true
112
- } else if (args[i] === '--no-previews') {
133
+ } else if (flag === '--no-previews') {
113
134
  result.previews = false
114
135
  }
115
136
  }
@@ -204,7 +225,16 @@ export async function add(rawArgs) {
204
225
  await addSection(rootDir, parsed)
205
226
  break
206
227
  case 'ci':
207
- await addCi(rootDir, parsed, pm)
228
+ // The scaffolded workflow must install the way THIS WORKSPACE
229
+ // installs, which is a property of the repo (its lockfile), not of
230
+ // how the CLI happened to be launched. `detectPackageManager()`
231
+ // reads npm_config_user_agent, so `npx uniweb add ci` in a pnpm
232
+ // workspace produced a workflow running `npm ci` — which fails on
233
+ // the first CI run, because there is no package-lock.json to
234
+ // install from. npx is a normal way to run the CLI, so this broke
235
+ // the common case. Fall back to the invocation only when the repo
236
+ // has no lockfile to read.
237
+ await addCi(rootDir, parsed, detectWorkspacePm(rootDir) || pm)
208
238
  break
209
239
  default:
210
240
  error(`Unknown subcommand: ${parsed.subcommand}`)
@@ -1123,7 +1153,11 @@ async function addCi(rootDir, opts, pm = 'pnpm') {
1123
1153
  const rootPkg = JSON.parse(
1124
1154
  await readFile(join(rootDir, 'package.json'), 'utf-8').catch(() => '{}')
1125
1155
  )
1126
- const nodeVersion = parseNodeMajor(rootPkg.engines?.node) || '20'
1156
+ // CI must run the project's own toolchain. The pnpm major comes from
1157
+ // its `packageManager` field when declared; the node major is the
1158
+ // project's floor, raised if that pnpm needs more.
1159
+ const pnpmVersion = resolveCiPnpmVersion(rootPkg)
1160
+ const nodeVersion = resolveCiNodeVersion(rootPkg.engines?.node, pm, pnpmVersion)
1127
1161
 
1128
1162
  const siteDir = join(rootDir, site.path)
1129
1163
  if (!resolvedDomain) {
@@ -1145,7 +1179,7 @@ async function addCi(rootDir, opts, pm = 'pnpm') {
1145
1179
  target: 'site',
1146
1180
  packageManager: pm,
1147
1181
  nodeVersion,
1148
- pnpmVersion: PNPM_VERSION,
1182
+ pnpmVersion,
1149
1183
  domain: resolvedDomain,
1150
1184
  previews: opts.previews !== false,
1151
1185
  projectName: resolveHostProjectName(rootDir, rootPkg, site, sites.length, opts),
@@ -1303,7 +1337,11 @@ async function addFoundationCi(rootDir, opts, adapter, pm) {
1303
1337
  const rootPkg = JSON.parse(
1304
1338
  await readFile(join(rootDir, 'package.json'), 'utf-8').catch(() => '{}')
1305
1339
  )
1306
- const nodeVersion = parseNodeMajor(rootPkg.engines?.node) || '20'
1340
+ // CI must run the project's own toolchain. The pnpm major comes from
1341
+ // its `packageManager` field when declared; the node major is the
1342
+ // project's floor, raised if that pnpm needs more.
1343
+ const pnpmVersion = resolveCiPnpmVersion(rootPkg)
1344
+ const nodeVersion = resolveCiNodeVersion(rootPkg.engines?.node, pm, pnpmVersion)
1307
1345
 
1308
1346
  let result
1309
1347
  try {
@@ -1313,7 +1351,7 @@ async function addFoundationCi(rootDir, opts, adapter, pm) {
1313
1351
  target: 'foundation',
1314
1352
  packageManager: pm,
1315
1353
  nodeVersion,
1316
- pnpmVersion: PNPM_VERSION,
1354
+ pnpmVersion,
1317
1355
  })
1318
1356
  } catch (err) {
1319
1357
  error(err.message)
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-07-21T14:39:07.990Z",
3
+ "generatedAt": "2026-07-21T21:33:51.867Z",
4
4
  "packages": {
5
5
  "@uniweb/build": {
6
- "version": "0.14.29",
6
+ "version": "0.14.31",
7
7
  "path": "framework/build",
8
8
  "deps": [
9
9
  "@uniweb/content-reader",
@@ -99,7 +99,7 @@
99
99
  "deps": []
100
100
  },
101
101
  "@uniweb/unipress": {
102
- "version": "0.4.36",
102
+ "version": "0.4.38",
103
103
  "path": "framework/unipress",
104
104
  "deps": [
105
105
  "@uniweb/build",
package/src/versions.js CHANGED
@@ -53,16 +53,86 @@ let resolvedVersions = null
53
53
  export const REACT_VERSION = '^19.0.0'
54
54
 
55
55
  /**
56
- * The pnpm major the framework's generated CI standardizes on.
56
+ * Default pnpm major for generated CI, used only when the project does not
57
+ * say which pnpm it uses (see `resolveCiPnpmVersion`).
57
58
  *
58
- * Scaffolded CI workflows (`uniweb add ci --host=github-pages`) pin pnpm
59
- * through `pnpm/action-setup`. This is the one place that value lives, so the
60
- * pin tracks a single supported major instead of drifting as a hardcoded
61
- * literal inside each host adapter. A bare major installs the latest patch of
62
- * that major at CI run time. Bump this when the framework moves to a new pnpm
63
- * major (latest stable is 11.x as of this writing; pnpm 12 is still alpha).
59
+ * **This is deliberately 10, not the newest release.** CI must run the same
60
+ * toolchain the developer runs, or it reports failures that have nothing to
61
+ * do with their code. Pinning 11 while projects were on 10 produced exactly
62
+ * that, twice over, on a real repo:
63
+ *
64
+ * - pnpm 11 refuses any dependency published in the last 24 hours
65
+ * (`ERR_PNPM_MINIMUM_RELEASE_AGE_VIOLATION`). Publish a package and its
66
+ * consumer's CI fails until the next day.
67
+ * - pnpm 11 changed build-script approval: `onlyBuiltDependencies` in
68
+ * `pnpm-workspace.yaml` no longer suffices, and the install exits 1 with
69
+ * `ERR_PNPM_IGNORED_BUILDS`. Uniweb sites use `sharp` for image
70
+ * processing, so this breaks essentially every site's first CI run.
71
+ *
72
+ * Neither is a bug in pnpm — both are reasonable hardening. They are simply
73
+ * not things a generated workflow should impose on a project that has not
74
+ * opted into that major.
75
+ *
76
+ * A bare major installs the latest patch of that major at CI run time. Before
77
+ * bumping this, check the new major's release notes for install-time policy
78
+ * changes, and update `PNPM_MIN_NODE` in the same commit.
64
79
  */
65
- export const PNPM_VERSION = '11'
80
+ export const PNPM_VERSION = '10'
81
+
82
+ /**
83
+ * Minimum Node major each supported pnpm major will run on, from that
84
+ * release's own `engines.node`.
85
+ *
86
+ * pnpm 11 declares `>=22.13` and imports `node:sqlite`, which does not exist
87
+ * before Node 22 — pairing it with Node 20 fails at `pnpm install` with
88
+ * `ERR_UNKNOWN_BUILTIN_MODULE` before the build is attempted. That was the
89
+ * default until 2026-07-21, because the CI node major came from the
90
+ * project's `engines.node` and the workspace template declares `>=20.19`.
91
+ *
92
+ * Verify with `npm view pnpm@<major> engines` when adding an entry.
93
+ */
94
+ export const PNPM_MIN_NODE = { '10': 18, '11': 22 }
95
+
96
+ /**
97
+ * Resolve the pnpm major a generated CI workflow should install.
98
+ *
99
+ * `packageManager` (the corepack field) is authoritative when present — it is
100
+ * the project stating which pnpm it is developed and locked against, so CI
101
+ * should honour it rather than impose a different major.
102
+ *
103
+ * `pnpm-lock.yaml`'s `lockfileVersion` is NOT usable as a signal: pnpm 10 and
104
+ * 11 both write `9.0`, so it cannot distinguish them. Checked, not assumed.
105
+ *
106
+ * @param {object|null} rootPkg — the workspace root's parsed package.json.
107
+ * @returns {string} pnpm major, as a string for YAML interpolation.
108
+ */
109
+ export function resolveCiPnpmVersion(rootPkg) {
110
+ const declared = rootPkg?.packageManager
111
+ const match = typeof declared === 'string' ? declared.match(/^pnpm@(\d+)/) : null
112
+ return match ? match[1] : PNPM_VERSION
113
+ }
114
+
115
+ /**
116
+ * Resolve the Node major a generated CI workflow should install.
117
+ *
118
+ * Respects the project's own floor (`engines.node`) but never drops below
119
+ * what the pinned package manager needs. The project's declared minimum is a
120
+ * *lower* bound on what its code needs, not a ceiling — running CI on a newer
121
+ * Node is fine; running it on one the package manager refuses to start on is
122
+ * not.
123
+ *
124
+ * @param {string|null|undefined} enginesNode — the project's `engines.node`.
125
+ * @param {'pnpm'|'npm'|'yarn'} packageManager
126
+ * @param {string} [pnpmVersion] — the resolved pnpm major, when pnpm.
127
+ * @param {string} [fallback='20'] — used when engines.node is absent/unparseable.
128
+ * @returns {string} Node major, as a string for YAML interpolation.
129
+ */
130
+ export function resolveCiNodeVersion(enginesNode, packageManager, pnpmVersion = PNPM_VERSION, fallback = '20') {
131
+ const match = enginesNode ? String(enginesNode).match(/(\d+)/) : null
132
+ const declared = match ? Number(match[1]) : Number(fallback)
133
+ const floor = packageManager === 'pnpm' ? (PNPM_MIN_NODE[pnpmVersion] ?? 0) : 0
134
+ return String(Math.max(declared, floor))
135
+ }
66
136
 
67
137
  /**
68
138
  * Get the CLI's own package.json
@@ -3,8 +3,24 @@ packages:
3
3
  - "{{this}}"
4
4
  {{/each}}
5
5
 
6
- # Permit install scripts for these native deps. pnpm reads this from
7
- # pnpm-workspace.yaml; the package.json "pnpm" field is ignored by pnpm 11+.
6
+ # Permit install scripts for these native deps (esbuild compiles, sharp
7
+ # resolves its platform binary). Both spellings are listed on purpose:
8
+ #
9
+ # onlyBuiltDependencies — pnpm 10's name for this setting (a list)
10
+ # allowBuilds — pnpm 11 renamed it (a map)
11
+ #
12
+ # pnpm 11 ignores the old name entirely, so with only the list a fresh
13
+ # install fails with ERR_PNPM_IGNORED_BUILDS and exits 1 — before anything
14
+ # is built. pnpm 10 understands both, so keeping the pair means the project
15
+ # installs on either major. Drop `onlyBuiltDependencies` once pnpm 10 is no
16
+ # longer in use.
17
+ #
18
+ # Neither applies to npm or yarn, which run install scripts by default;
19
+ # they read `workspaces` in package.json and ignore this file.
8
20
  onlyBuiltDependencies:
9
21
  - esbuild
10
22
  - sharp
23
+
24
+ allowBuilds:
25
+ esbuild: true
26
+ sharp: true