uniweb 0.87.4 → 0.88.1

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.
@@ -155,11 +155,6 @@ export async function scaffoldSite(targetDir, context, options = {}) {
155
155
  * @param {string} targetDir - Target directory to overlay onto
156
156
  * @param {Object} context - Handlebars context for .hbs files
157
157
  * @param {Object} [options] - Processing options
158
- * @param {Object} [options.renames] - Top-level filename remapping
159
- * (e.g. `{ 'foundation.js': 'main.js' }`). Applied only at depth 0
160
- * so renames don't accidentally rewrite same-named files in nested
161
- * directories. Used to migrate legacy `foundation/foundation.js`
162
- * templates onto the new flat `src/main.js` layout.
163
158
  */
164
159
  export async function applyContent(
165
160
  contentDir,
@@ -221,11 +216,8 @@ const INHERITED_STATE = new Set(['sync.json', 'deploy.yml', '.uniweb'])
221
216
  /**
222
217
  * Recursively copy content files, skipping structural files.
223
218
  *
224
- * `depth` is tracked so the `renames` map (passed via options) only
225
- * applies at depth 0 — the top of the content directory. Without this
226
- * guard, a rename like `foundation.js → main.js` would also rewrite a
227
- * nested `sections/foo/foundation.js` if one existed, which is not the
228
- * intent.
219
+ * `depth` is tracked so the project-state names (INHERITED_STATE) are
220
+ * skipped only at the top of the content directory.
229
221
  */
230
222
  async function copyContentRecursive(
231
223
  sourceDir,
@@ -239,7 +231,6 @@ async function copyContentRecursive(
239
231
  await fs.mkdir(targetDir, { recursive: true })
240
232
 
241
233
  const entries = readdirSync(sourceDir, { withFileTypes: true })
242
- const renames = (depth === 0 && options.renames) || null
243
234
 
244
235
  for (const entry of entries) {
245
236
  const sourcePath = join(sourceDir, entry.name)
@@ -258,18 +249,10 @@ async function copyContentRecursive(
258
249
  )
259
250
  } else {
260
251
  // Determine the output filename (strip .hbs extension)
261
- let outputName = entry.name.endsWith('.hbs')
252
+ const outputName = entry.name.endsWith('.hbs')
262
253
  ? entry.name.slice(0, -4)
263
254
  : entry.name
264
255
 
265
- // Apply top-level rename (e.g. legacy `foundation.js` → `main.js`)
266
- if (
267
- renames &&
268
- Object.prototype.hasOwnProperty.call(renames, outputName)
269
- ) {
270
- outputName = renames[outputName]
271
- }
272
-
273
256
  // Skip structural files
274
257
  if (structuralFiles.has(outputName)) continue
275
258
  if (depth === 0 && INHERITED_STATE.has(outputName)) continue
@@ -57,36 +57,22 @@ function writeState(state) {
57
57
 
58
58
  /**
59
59
  * Print update notification to stderr (doesn't interfere with piped output).
60
- * `tone` controls the lead-in: 'soft' (default — trailing notice for finished
61
- * commands) vs 'eager' (leading notice for staleness-sensitive commands like
62
- * `create`, where the user is about to scaffold files from CLI-bundled
63
- * templates and a stale CLI means stale starter content).
60
+ * A trailing notice for a finished command; `create` and `clone` do not get
61
+ * one, they stop first (refuseOutdatedProjectStart).
64
62
  */
65
- function printNotification(current, latest, tone = 'soft') {
63
+ function printNotification(current, latest) {
66
64
  const yellow = '\x1b[33m'
67
65
  const cyan = '\x1b[36m'
68
66
  const dim = '\x1b[2m'
69
67
  const reset = '\x1b[0m'
70
68
  const updateCmd = globalCliUpdateCmd(detectGlobalCliPm())
71
69
  console.error('')
72
- if (tone === 'eager') {
73
- console.error(
74
- `${yellow}Heads up:${reset} this CLI is ${dim}${current}${reset}; latest is ${cyan}${latest}${reset}.`
75
- )
76
- console.error(
77
- `${dim}Templates ship with the CLI — consider updating first:${reset} ${updateCmd}`
78
- )
79
- console.error(
80
- `${dim}Or run a one-shot fresh:${reset} npx uniweb@latest <command>`
81
- )
82
- } else {
83
- console.error(
84
- `${yellow}Update available:${reset} ${dim}${current}${reset} → ${cyan}${latest}${reset}`
85
- )
86
- console.error(
87
- `${dim}Run${reset} ${updateCmd} ${dim}to update the CLI${reset}`
88
- )
89
- }
70
+ console.error(
71
+ `${yellow}Update available:${reset} ${dim}${current}${reset} → ${cyan}${latest}${reset}`
72
+ )
73
+ console.error(
74
+ `${dim}Run${reset} ${updateCmd} ${dim}to update the CLI${reset}`
75
+ )
90
76
  }
91
77
 
92
78
  /**
@@ -94,28 +80,78 @@ function printNotification(current, latest, tone = 'soft') {
94
80
  * version is known. No network fetch — only reads what `startUpdateCheck`
95
81
  * has previously cached. Returns true if a notification was printed.
96
82
  *
97
- * Two call sites today, with different tone needs:
98
- * - `create` (tone='eager'): loud leading notice — templates ship with
99
- * the CLI, the user is about to scaffold files, this matters.
100
- * - `--version` / `-v` (tone='soft'): brief trailing notice — the user
101
- * was already asking about version, mention staleness while we're
102
- * here. Goes to stderr so scripts capturing stdout aren't affected.
83
+ * For `--version` / `-v` off a terminal: the user was already asking about
84
+ * the version, so staleness is mentioned while we're here. Goes to stderr so
85
+ * scripts capturing stdout aren't affected.
103
86
  *
104
87
  * @param {string} currentVersion
105
- * @param {'eager'|'soft'} [tone='eager']
106
88
  * @returns {boolean} true if a notification was printed
107
89
  */
108
- export function maybeNotifyFromCache(currentVersion, tone = 'eager') {
90
+ export function maybeNotifyFromCache(currentVersion) {
109
91
  const state = readState()
110
92
  if (!state.latestVersion) return false
111
93
  if (compareSemver(state.latestVersion, currentVersion) <= 0) return false
112
- printNotification(currentVersion, state.latestVersion, tone)
94
+ printNotification(currentVersion, state.latestVersion)
113
95
  return true
114
96
  }
115
97
 
116
- // Old name preserved as alias — `create` calls it without a tone arg
117
- // and gets the eager default. Keeps that call site unchanged.
118
- export const maybeEagerNotification = maybeNotifyFromCache
98
+ /**
99
+ * Whether a release newer than `current` is published. Unknown (`latest` null:
100
+ * offline, or a slow registry) is not behind.
101
+ *
102
+ * @param {string} current
103
+ * @param {string|null} latest
104
+ * @returns {boolean}
105
+ */
106
+ export function isBehind(current, latest) {
107
+ return Boolean(latest) && compareSemver(latest, current) > 0
108
+ }
109
+
110
+ /** Arguments as a shell would need them typed again. */
111
+ export function shellJoin(args) {
112
+ return args
113
+ .map((a) => (/^[\w@%+=:,./-]+$/.test(a) ? a : `'${String(a).replace(/'/g, `'\\''`)}'`))
114
+ .join(' ')
115
+ }
116
+
117
+ /**
118
+ * Stop `create` or `clone` when a global CLI has been replaced by a newer
119
+ * release — before anything is written.
120
+ *
121
+ * ⭐ A PROJECT STARTS ON THE CLI THAT CREATES IT: its version table pins the
122
+ * `@uniweb/*` packages, and its official templates come from the release it was
123
+ * published with. Inside a project a global install only launches the project's
124
+ * own CLI; outside one, for `create` and `clone`, it does the work itself — so
125
+ * an outdated one starts the project outdated, and says nothing. `npx uniweb`
126
+ * with no version runs a global install too, which is how a 0.12 CLI went on
127
+ * offering the ten templates it knew while sixteen shipped (2026-10-08).
128
+ *
129
+ * Any newer release stops it, not only a newer minor: the version table moves
130
+ * whenever any package does — a patch of the CLI can pin a new breaking line of
131
+ * one — and the fix is one command either way. Unknown proceeds: this stops a
132
+ * known mistake, it does not require the network.
133
+ *
134
+ * @param {string} current - this CLI's version
135
+ * @param {string[]} argv - the command and its arguments, repeated in the fix
136
+ * @returns {Promise<boolean>} true when it refused, the reason printed
137
+ */
138
+ export async function refuseOutdatedProjectStart(current, argv) {
139
+ const latest = await getLatestVersion({ maxAgeMs: 60 * 60 * 1000 })
140
+ if (!isBehind(current, latest)) return false
141
+ const red = '\x1b[31m'
142
+ const cyan = '\x1b[36m'
143
+ const dim = '\x1b[2m'
144
+ const reset = '\x1b[0m'
145
+ console.error(`${red}✗${reset} This uniweb is ${current}; the latest release is ${latest}.`)
146
+ console.error(
147
+ ` ${dim}A project starts on the packages and templates of the CLI that creates it.${reset}`
148
+ )
149
+ console.error(` Run the latest: ${cyan}npx uniweb@latest ${shellJoin(argv)}${reset}`)
150
+ console.error(
151
+ ` Or update this one: ${cyan}${globalCliUpdateCmd(detectGlobalCliPm())}${reset}`
152
+ )
153
+ return true
154
+ }
119
155
 
120
156
  /**
121
157
  * Resolve the latest published CLI version WITHOUT printing anything.
@@ -188,12 +224,11 @@ export async function getLatestVersion({
188
224
  * @param {object} [opts]
189
225
  * @param {number} [opts.timeoutMs=1500] Network timeout. Slow / offline
190
226
  * calls return silently — never block the verb for long.
191
- * @param {'eager'|'soft'} [opts.tone='soft'] Notification copy.
192
227
  * @returns {Promise<boolean>} true if a notice was printed.
193
228
  */
194
229
  export async function fetchAndNotifyIfNewer(
195
230
  currentVersion,
196
- { timeoutMs = 1500, tone = 'soft' } = {}
231
+ { timeoutMs = 1500 } = {}
197
232
  ) {
198
233
  const controller = new AbortController()
199
234
  const timer = setTimeout(() => controller.abort(), timeoutMs)
@@ -216,7 +251,7 @@ export async function fetchAndNotifyIfNewer(
216
251
  // Refresh the cache so other code paths see this fresh result.
217
252
  writeState({ lastCheck: Date.now(), latestVersion: latest })
218
253
  if (compareSemver(latest, currentVersion) <= 0) return false
219
- printNotification(currentVersion, latest, tone)
254
+ printNotification(currentVersion, latest)
220
255
  return true
221
256
  }
222
257
 
package/src/versions.js CHANGED
@@ -240,8 +240,11 @@ function readWorkspaceVersion(packageName) {
240
240
  * Returns null if the file doesn't exist, is unparseable, or has a
241
241
  * schema version we don't understand. Callers treat null as "no
242
242
  * snapshot available" and fall back to their next source.
243
+ *
244
+ * Also the official templates' one source (`templates/resolver.js`): their
245
+ * roster, and the `@uniweb/templates` release their content downloads from.
243
246
  */
244
- function loadFrameworkIndex() {
247
+ export function loadFrameworkIndex() {
245
248
  const indexPath = join(__dirname, 'framework-index.json')
246
249
  try {
247
250
  const raw = readFileSync(indexPath, 'utf8')