uniweb 0.12.40 → 0.12.42

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 CHANGED
@@ -347,10 +347,11 @@ Markdown in a git repo and content in the Uniweb apps can share the same site. D
347
347
  | Command | What it does |
348
348
  | --- | --- |
349
349
  | `uniweb add ci --host=<adapter>` | Scaffold a CI workflow in your repo (today: `github-pages`). The host runs `uniweb build` on each push. |
350
- | `uniweb deploy` | Deploy to Uniweb hosting (default). With `--host=<adapter>`, push directly to a static host builds, uploads, invalidates in one step. |
350
+ | `uniweb publish` | Go live on Uniweb hosting (paid) syncs content, brings the site's foundation along, and serves dynamically. The canonical verb for Uniweb hosting. |
351
+ | `uniweb deploy --host=<adapter>` | Ship to a third-party static host in one step — builds `dist/`, uploads, invalidates. |
351
352
  | `uniweb export` | Produce a self-contained `dist/` for any static host. You upload it yourself. `--host=<adapter>` adds host-specific helper files. |
352
353
  | `uniweb register --scope @org` | Register a foundation to the registry (path 2). |
353
- | `uniweb build` | Inspect a build locally. For shipping, use `deploy` or `export`. |
354
+ | `uniweb build` | Inspect a build locally. For shipping, use `publish` (Uniweb hosting) or `deploy`/`export` (static hosts). |
354
355
  | `uniweb update` | Align this project with the CLI you're running: bump `@uniweb/*` deps in every `package.json` to the CLI's matrix (then install), and refresh `AGENTS.md`. Pins to *this* CLI's matrix — run `npx uniweb@latest update` to align to the latest release. Updating the CLI itself is your package manager's job (`npm i -g uniweb@latest`). |
355
356
 
356
357
  `--host=<adapter>` is the same option across `deploy`, `export`, and `add ci`. Built-in adapters: `cloudflare-pages`, `netlify`, `github-pages`, `vercel`, `s3-cloudfront`, `generic-static`. Each adapter implements only the operations it supports — `add ci` is currently `github-pages`-only because it's the only one that needs a workflow file in the repo.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "uniweb",
3
- "version": "0.12.40",
3
+ "version": "0.12.42",
4
4
  "description": "Create structured Vite + React sites with content/code separation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -46,8 +46,8 @@
46
46
  "@uniweb/runtime": "0.8.22"
47
47
  },
48
48
  "peerDependencies": {
49
- "@uniweb/build": "0.14.23",
50
49
  "@uniweb/content-reader": "1.1.12",
50
+ "@uniweb/build": "0.14.24",
51
51
  "@uniweb/semantic-parser": "1.1.17"
52
52
  },
53
53
  "peerDependenciesMeta": {
@@ -31,7 +31,7 @@ import { detectPackageManager, 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 } from '../versions.js'
34
+ import { getVersionsForTemplates, PNPM_VERSION } from '../versions.js'
35
35
 
36
36
  // Colors for terminal output
37
37
  const colors = {
@@ -952,7 +952,7 @@ export default function ${name}({ content, params }) {
952
952
  * Add a CI deploy workflow for a host adapter.
953
953
  *
954
954
  * Wires through the host-adapter registry: each adapter optionally
955
- * exports `initCi({ rootDir, site, packageManager, nodeVersion })`
955
+ * exports `initCi({ rootDir, site, packageManager, nodeVersion, pnpmVersion })`
956
956
  * returning `{ files, postInstructions }`. The CLI handles file writes,
957
957
  * --force overwrite, and consistent output. Today only github-pages
958
958
  * implements initCi; the registry's other adapters (cloudflare-pages,
@@ -1069,6 +1069,7 @@ async function addCi(rootDir, opts, pm = 'pnpm') {
1069
1069
  site,
1070
1070
  packageManager: pm,
1071
1071
  nodeVersion,
1072
+ pnpmVersion: PNPM_VERSION,
1072
1073
  domain: resolvedDomain,
1073
1074
  })
1074
1075
 
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-07-13T17:29:11.603Z",
3
+ "generatedAt": "2026-07-14T12:34:40.728Z",
4
4
  "packages": {
5
5
  "@uniweb/build": {
6
- "version": "0.14.23",
6
+ "version": "0.14.24",
7
7
  "path": "framework/build",
8
8
  "deps": [
9
9
  "@uniweb/content-reader",
@@ -89,7 +89,7 @@
89
89
  "deps": []
90
90
  },
91
91
  "@uniweb/templates": {
92
- "version": "0.7.42",
92
+ "version": "0.7.44",
93
93
  "path": "framework/templates",
94
94
  "deps": []
95
95
  },
@@ -99,7 +99,7 @@
99
99
  "deps": []
100
100
  },
101
101
  "@uniweb/unipress": {
102
- "version": "0.4.30",
102
+ "version": "0.4.32",
103
103
  "path": "framework/unipress",
104
104
  "deps": [
105
105
  "@uniweb/build",
@@ -112,7 +112,7 @@
112
112
  },
113
113
  "templates": {
114
114
  "marketing": {
115
- "name": "Marketing Starter",
115
+ "name": "Marketing",
116
116
  "description": "Landing pages and marketing sites with Tailwind CSS v4",
117
117
  "tags": [
118
118
  "marketing",
@@ -130,7 +130,7 @@
130
130
  ]
131
131
  },
132
132
  "docs": {
133
- "name": "Documentation",
133
+ "name": "Docs",
134
134
  "description": "Documentation sites with navigation levels",
135
135
  "tags": [
136
136
  "docs",
@@ -139,7 +139,7 @@
139
139
  ]
140
140
  },
141
141
  "international": {
142
- "name": "International Business",
142
+ "name": "International",
143
143
  "description": "Multilingual corporate website with English, Spanish, and French",
144
144
  "tags": [
145
145
  "i18n",
@@ -161,7 +161,7 @@
161
161
  ]
162
162
  },
163
163
  "dynamic": {
164
- "name": "Dynamic Data",
164
+ "name": "Dynamic",
165
165
  "description": "Live API data fetching with loading states, transforms, and the portable data pattern",
166
166
  "tags": [
167
167
  "data",
@@ -172,7 +172,7 @@
172
172
  ]
173
173
  },
174
174
  "store": {
175
- "name": "Solis Artisans - Store",
175
+ "name": "Store",
176
176
  "description": "Artisan e-commerce with product collections, Shopify Buy Button, journal blog, and stone-amber design",
177
177
  "tags": [
178
178
  "store",
@@ -183,7 +183,7 @@
183
183
  ]
184
184
  },
185
185
  "learning": {
186
- "name": "Learning Platform",
186
+ "name": "Learning",
187
187
  "description": "Course-based learning site with quizzes, code challenges, and AI grading integration",
188
188
  "tags": [
189
189
  "learning",
@@ -195,7 +195,7 @@
195
195
  ]
196
196
  },
197
197
  "extensions": {
198
- "name": "Extensions Demo",
198
+ "name": "Extensions",
199
199
  "description": "Primary foundation with a visual effects extension, demonstrating multi-foundation sites",
200
200
  "tags": [
201
201
  "extensions",
@@ -205,7 +205,7 @@
205
205
  ]
206
206
  },
207
207
  "cv-loom": {
208
- "name": "CV (Loom-instantiated)",
208
+ "name": "CV (Loom)",
209
209
  "description": "A single-page narrative CV whose prose is written with {placeholder} expressions and instantiated at render time via the foundation content handler and @uniweb/loom. Reference implementation of the handler hook.",
210
210
  "tags": [
211
211
  "cv",
@@ -217,7 +217,7 @@
217
217
  ]
218
218
  },
219
219
  "monograph": {
220
- "name": "Monograph (Press showcase)",
220
+ "name": "Monograph",
221
221
  "description": "A richly illustrated long-form document — numbered chapters with nested subsections, figures with captions, data-driven specimen and measurement tables, and a citestyle bibliography. The same JSX drives the themed web preview and a branded .docx with headers, footers, and page numbers. Ships with Darwin's Galapagos observations as sample content.",
222
222
  "tags": [
223
223
  "monograph",
@@ -230,7 +230,7 @@
230
230
  ]
231
231
  },
232
232
  "academic-metrics": {
233
- "name": "Academic Metrics (xlsx showcase)",
233
+ "name": "Academic Metrics",
234
234
  "description": "A docusite whose content is also a downloadable Excel workbook. Aggregates academic metrics (publications, funding, supervisions) across multiple members of a university unit. Rich web previews + flat tabular xlsx sheets from the same data. Ships with three 19th-century naturalists as sample members.",
235
235
  "tags": [
236
236
  "academic-metrics",
@@ -244,7 +244,7 @@
244
244
  ]
245
245
  },
246
246
  "business-docs": {
247
- "name": "Business documents",
247
+ "name": "Business Documents",
248
248
  "description": "A complete business-document site: invoice and SOW collections, per-record preview pages, and a filtered engagement report with XLSX export. Built on @uniweb/business-docs and demonstrates the same foundation that ships the unipress invoice template.",
249
249
  "tags": [
250
250
  "business-docs",
@@ -256,6 +256,17 @@
256
256
  "docx",
257
257
  "pagedjs"
258
258
  ]
259
+ },
260
+ "paste": {
261
+ "name": "Paste a Design (Level 0)",
262
+ "description": "Bring an existing React design (AI-generated or hand-built) into Uniweb with zero rewiring — replace App.jsx and run. Ships lucide-react preinstalled and SPA-first. The Level 0 conversion starting point.",
263
+ "tags": [
264
+ "paste",
265
+ "level-0",
266
+ "convert",
267
+ "ai-generated",
268
+ "starter"
269
+ ]
259
270
  }
260
271
  }
261
272
  }
package/src/index.js CHANGED
@@ -41,10 +41,11 @@ import { template } from './commands/template.js'
41
41
  import {
42
42
  resolveTemplate,
43
43
  parseTemplateId,
44
+ buildTemplateChoices,
44
45
  } from './templates/index.js'
45
46
  import { validateTemplate } from './templates/validator.js'
46
47
  import { scaffoldWorkspace, scaffoldFoundation, scaffoldSite, applyContent, applyStarter, mergeTemplateDependencies, getWorkspaceTemplateOutputs } from './utils/scaffold.js'
47
- import { detectPackageManager, filterCmd, installCmd, runCmd } from './utils/pm.js'
48
+ import { detectPackageManager, filterCmd, installCmd, runCmd, isPnpmAvailable } from './utils/pm.js'
48
49
  import { isNonInteractive, getCliPrefix, stripNonInteractiveFlag, formatOptions } from './utils/interactive.js'
49
50
  import { findWorkspaceRoot } from './utils/workspace.js'
50
51
 
@@ -59,19 +60,11 @@ const colors = {
59
60
  red: '\x1b[31m',
60
61
  }
61
62
 
62
- // Template choices for interactive prompt
63
- const TEMPLATE_CHOICES = [
64
- { title: 'None', value: 'none', description: 'Foundation + site with no content' },
65
- { title: 'Starter', value: 'starter', description: 'Foundation + site + sample content' },
66
- { title: 'Marketing', value: 'marketing', description: 'Landing page, features, pricing, testimonials' },
67
- { title: 'Docs', value: 'docs', description: 'Documentation with sidebar and search' },
68
- { title: 'Academic', value: 'academic', description: 'Research site with publications and team' },
69
- { title: 'Dynamic', value: 'dynamic', description: 'Live API data fetching with loading states' },
70
- { title: 'International', value: 'international', description: 'Multilingual site with i18n and blog' },
71
- { title: 'Store', value: 'store', description: 'E-commerce with product grid' },
72
- { title: 'Extensions', value: 'extensions', description: 'Multi-foundation with visual effects extension' },
73
- { title: 'Blank workspace', value: 'blank', description: 'Empty workspace — grow with uniweb add' },
74
- ]
63
+ // Template choices for the interactive prompt — built-ins first, then every
64
+ // official template from the shared manifest (so the list never drifts from
65
+ // framework/templates/manifest.json), with "Blank" last. Composed by
66
+ // buildTemplateChoices() in templates/resolver.js.
67
+ const TEMPLATE_CHOICES = buildTemplateChoices()
75
68
 
76
69
  // Files that may pre-exist in the target dir during `uniweb create .` and
77
70
  // will be silently overwritten by the scaffold. Anything else colliding
@@ -1054,23 +1047,28 @@ async function main() {
1054
1047
  // Success message
1055
1048
  title('Project created successfully!')
1056
1049
 
1050
+ // Recommend pnpm (the framework's package manager) when it's installed,
1051
+ // otherwise fall back to however the user invoked the CLI. The scaffold
1052
+ // supports both — the root package.json declares npm `workspaces` and a
1053
+ // pnpm-workspace.yaml ships next to it.
1054
+ const recPm = isPnpmAvailable() ? 'pnpm' : pm
1055
+
1057
1056
  if (isBlank) {
1058
1057
  log(`Next steps:\n`)
1059
1058
  if (!inPlace) log(` ${colors.cyan}cd ${projectName}${colors.reset}`)
1060
1059
  log(` ${colors.cyan}${prefix} add project${colors.reset}`)
1061
- log(` ${colors.cyan}${installCmd(pm)}${colors.reset}`)
1062
- log(` ${colors.cyan}${prefix} dev${colors.reset} ${colors.dim}# Start dev server${colors.reset}`)
1060
+ log(` ${colors.cyan}${installCmd(recPm)}${colors.reset}`)
1061
+ log(` ${colors.cyan}${runCmd(recPm, 'dev')}${colors.reset} ${colors.dim}# start the dev server${colors.reset}`)
1063
1062
  } else {
1064
1063
  log(`Next steps:\n`)
1065
1064
  if (!inPlace) log(` ${colors.cyan}cd ${projectName}${colors.reset}`)
1066
- log(` ${colors.cyan}${installCmd(pm)}${colors.reset}`)
1067
- log(` ${colors.cyan}${prefix} dev${colors.reset} ${colors.dim}# Start dev server${colors.reset}`)
1065
+ log(` ${colors.cyan}${installCmd(recPm)}${colors.reset}`)
1066
+ log(` ${colors.cyan}${runCmd(recPm, 'dev')}${colors.reset} ${colors.dim}# start the dev server${colors.reset}`)
1067
+ }
1068
+ if (recPm !== 'pnpm') {
1069
+ log('')
1070
+ log(` ${colors.dim}Tip: pnpm is recommended (https://pnpm.io) — npm works too.${colors.reset}`)
1068
1071
  }
1069
- log('')
1070
- log(`When ready to ship:\n`)
1071
- log(` ${colors.cyan}${prefix} deploy${colors.reset} ${colors.dim}# Uniweb hosting (default; uniweb login first)${colors.reset}`)
1072
- log(` ${colors.cyan}${prefix} deploy --host=<adapter>${colors.reset} ${colors.dim}# cloudflare-pages, netlify, vercel, github-pages, s3-cloudfront${colors.reset}`)
1073
- log(` ${colors.cyan}${prefix} export${colors.reset} ${colors.dim}# Build dist/ for any static host (no Uniweb account)${colors.reset}`)
1074
1072
  log('')
1075
1073
  log(` ${colors.dim}See ${colors.reset}${colors.cyan}${prefix} <command> --help${colors.reset}${colors.dim} for command-specific options.${colors.reset}`)
1076
1074
  log('')
@@ -7,7 +7,7 @@ import { readFile, rm } from 'node:fs/promises'
7
7
  import { join, resolve } from 'node:path'
8
8
  import { homedir } from 'node:os'
9
9
 
10
- import { parseTemplateId, getTemplateDisplayName, BUILTIN_TEMPLATES, OFFICIAL_TEMPLATES } from './resolver.js'
10
+ import { parseTemplateId, getTemplateDisplayName, BUILTIN_TEMPLATES, OFFICIAL_TEMPLATES, buildTemplateChoices } from './resolver.js'
11
11
  import { fetchNpmTemplate } from './fetchers/npm.js'
12
12
  import { fetchGitHubTemplate } from './fetchers/github.js'
13
13
  import { fetchOfficialTemplate, listOfficialTemplates } from './fetchers/release.js'
@@ -182,4 +182,4 @@ export async function listAvailableTemplates() {
182
182
  }
183
183
 
184
184
  // Re-export for convenience
185
- export { parseTemplateId, getTemplateDisplayName, BUILTIN_TEMPLATES, OFFICIAL_TEMPLATES }
185
+ export { parseTemplateId, getTemplateDisplayName, BUILTIN_TEMPLATES, OFFICIAL_TEMPLATES, buildTemplateChoices }
@@ -12,7 +12,8 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
12
12
  export const BUILTIN_TEMPLATES = ['blank', 'starter', 'none']
13
13
 
14
14
  /**
15
- * Load the list of official template names.
15
+ * Load the official template metadata map — `{ id: { name, description,
16
+ * tags } }`, keyed by template id.
16
17
  *
17
18
  * There are two sources of truth depending on where the CLI is running:
18
19
  *
@@ -24,59 +25,93 @@ export const BUILTIN_TEMPLATES = ['blank', 'starter', 'none']
24
25
  *
25
26
  * 2. **Published CLI (npm-installed)** — the monorepo isn't on disk, so
26
27
  * we read the vendored framework index at `../framework-index.json`,
27
- * which the publish pipeline's pre-publish hook rewrites just before
28
- * `pnpm publish` runs. The framework index is a single snapshot file
29
- * that also carries `@uniweb/*` package versions (consumed by
30
- * versions.js), so both the template list and the version resolver
31
- * share one source of truth.
28
+ * which the publish pipeline rewrites just before `pnpm publish` runs,
29
+ * copying the manifest's `templates` verbatim. The framework index is a
30
+ * single snapshot file that also carries `@uniweb/*` package versions
31
+ * (consumed by versions.js), so both the template list and the version
32
+ * resolver share one source of truth.
32
33
  *
33
34
  * When both sources are available (local dev with a committed snapshot),
34
35
  * the live workspace manifest wins so newly-added templates are visible
35
36
  * without waiting for a CLI republish.
36
37
  *
37
- * The previous implementation hardcoded a string array here, which
38
- * silently duplicated `framework/templates/manifest.json` and required
39
- * a two-repo edit whenever a template was added. The one before THAT
40
- * (v0.9.3) used a separate `./official-templates.snapshot.json` file;
41
- * it has been retired in favor of the shared framework index.
38
+ * Both files store `templates` as the same `{ id: { name, description,
39
+ * tags } }` shape, so one map feeds both name-resolution
40
+ * (OFFICIAL_TEMPLATES) and the interactive picker (buildTemplateChoices)
41
+ * neither hardcodes a list. (An earlier version returned only the keys and
42
+ * left the `create` picker with its own hardcoded array, which silently
43
+ * drifted out of sync whenever a template was added.)
42
44
  */
43
- function loadOfficialTemplateList() {
45
+ function loadOfficialTemplateMap() {
44
46
  // Local dev: framework/templates/manifest.json relative to this file
45
47
  // at framework/cli/src/templates/resolver.js
46
48
  const workspaceManifest = join(__dirname, '..', '..', '..', 'templates', 'manifest.json')
47
- const picked = tryReadTemplateKeys(workspaceManifest)
49
+ const picked = tryReadTemplateMap(workspaceManifest)
48
50
  if (picked) return picked
49
51
 
50
52
  // Published CLI fallback: the framework index snapshot, one directory
51
53
  // up at framework/cli/src/framework-index.json.
52
54
  const indexPath = join(__dirname, '..', 'framework-index.json')
53
- const fromIndex = tryReadTemplateKeys(indexPath)
55
+ const fromIndex = tryReadTemplateMap(indexPath)
54
56
  if (fromIndex) return fromIndex
55
57
 
56
- // If both sources fail, return an empty list rather than a stale
57
- // hardcoded array. An unknown template name then falls through to
58
+ // If both sources fail, return an empty map rather than a stale
59
+ // hardcoded list. An unknown template name then falls through to
58
60
  // the npm `@uniweb/template-<name>` lookup path, which is the
59
61
  // intended behavior for third-party templates.
60
- return []
62
+ return {}
61
63
  }
62
64
 
63
- function tryReadTemplateKeys(path) {
65
+ function tryReadTemplateMap(path) {
64
66
  try {
65
67
  if (!statSync(path).isFile()) return null
66
68
  const data = JSON.parse(readFileSync(path, 'utf8'))
67
69
  if (data && data.templates && typeof data.templates === 'object') {
68
- return Object.keys(data.templates)
70
+ return data.templates
69
71
  }
70
72
  } catch {}
71
73
  return null
72
74
  }
73
75
 
74
- // Official templates from the templates repo. Derived from manifest.json
75
- // (local dev) or framework-index.json (published CLI) at module load
76
- // time — see loadOfficialTemplateList() for details. If the list needs
77
- // to reflect a just-added template, restart the CLI process or rerun
78
- // the scaffolder; this constant is read once per process.
79
- export const OFFICIAL_TEMPLATES = loadOfficialTemplateList()
76
+ // Official template metadata keyed by id. Derived from manifest.json (local
77
+ // dev) or framework-index.json (published CLI) at module load time — see
78
+ // loadOfficialTemplateMap() for details. Read once per process; to reflect a
79
+ // just-added template, restart the CLI or rerun the scaffolder.
80
+ export const OFFICIAL_TEMPLATE_MAP = loadOfficialTemplateMap()
81
+
82
+ // Official template ids, e.g. ['marketing', 'docs', 'academic', …].
83
+ export const OFFICIAL_TEMPLATES = Object.keys(OFFICIAL_TEMPLATE_MAP)
84
+
85
+ // Built-in (programmatic) picker entries — not in the manifest; the CLI
86
+ // generates these itself. "Blank" trails the official templates.
87
+ const BUILTIN_LEAD_CHOICES = [
88
+ { title: 'None', value: 'none', description: 'Foundation + site with no content' },
89
+ { title: 'Starter', value: 'starter', description: 'Foundation + site + sample content' }
90
+ ]
91
+ const BLANK_CHOICE = {
92
+ title: 'Blank workspace',
93
+ value: 'blank',
94
+ description: 'Empty workspace — grow with uniweb add'
95
+ }
96
+
97
+ /**
98
+ * Build the choices for the interactive `create` template prompt: the
99
+ * built-in leads (None, Starter), then every official template from the
100
+ * shared manifest in manifest order, then Blank last. Deriving the official
101
+ * entries from OFFICIAL_TEMPLATE_MAP keeps the picker in lockstep with
102
+ * framework/templates/manifest.json — adding a template there (and
103
+ * republishing the CLI) is all it takes for it to appear here.
104
+ *
105
+ * @returns {Array<{title: string, value: string, description: string}>}
106
+ */
107
+ export function buildTemplateChoices() {
108
+ const official = Object.entries(OFFICIAL_TEMPLATE_MAP).map(([id, info]) => ({
109
+ title: info?.name || id,
110
+ value: id,
111
+ description: info?.description || ''
112
+ }))
113
+ return [...BUILTIN_LEAD_CHOICES, ...official, BLANK_CHOICE]
114
+ }
80
115
 
81
116
  /**
82
117
  * Parse a template identifier and determine its source type
package/src/utils/pm.js CHANGED
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { existsSync } from 'node:fs'
9
9
  import { join } from 'node:path'
10
+ import { execSync } from 'node:child_process'
10
11
 
11
12
  /**
12
13
  * Detect which package manager invoked the CLI.
@@ -84,6 +85,22 @@ export function filterCmd(pm, pkg, cmd) {
84
85
  : `npm -w ${pkg} run ${cmd}`
85
86
  }
86
87
 
88
+ /**
89
+ * Whether pnpm is available on the current PATH. Used to recommend pnpm
90
+ * (the framework's package manager) in post-scaffold next steps while still
91
+ * falling back gracefully when it isn't installed. Mirrors the `git --version`
92
+ * probe the create flow already uses to feature-detect git.
93
+ * @returns {boolean}
94
+ */
95
+ export function isPnpmAvailable() {
96
+ try {
97
+ execSync('pnpm --version', { stdio: 'ignore' })
98
+ return true
99
+ } catch {
100
+ return false
101
+ }
102
+ }
103
+
87
104
  /**
88
105
  * Generate an install command.
89
106
  * @param {'pnpm' | 'yarn' | 'npm'} pm
package/src/versions.js CHANGED
@@ -52,6 +52,18 @@ let resolvedVersions = null
52
52
  */
53
53
  export const REACT_VERSION = '^19.0.0'
54
54
 
55
+ /**
56
+ * The pnpm major the framework's generated CI standardizes on.
57
+ *
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).
64
+ */
65
+ export const PNPM_VERSION = '11'
66
+
55
67
  /**
56
68
  * Get the CLI's own package.json
57
69
  */