uniweb 0.26.1 → 0.26.3

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.26.1",
3
+ "version": "0.26.3",
4
4
  "description": "Create structured Vite + React sites with content/code separation",
5
5
  "type": "module",
6
6
  "bin": {
@@ -41,10 +41,10 @@
41
41
  "js-yaml": "^4.1.0",
42
42
  "prompts": "^2.4.2",
43
43
  "tar": "^7.0.0",
44
- "@uniweb/core": "^0.10.1",
45
- "@uniweb/kit": "^0.12.3",
46
- "@uniweb/semantic-parser": "^1.2.3",
47
- "@uniweb/runtime": "^0.12.2"
44
+ "@uniweb/core": "^0.10.2",
45
+ "@uniweb/kit": "^0.13.0",
46
+ "@uniweb/runtime": "^0.12.4",
47
+ "@uniweb/semantic-parser": "^1.2.3"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "@uniweb/build": "^0.24.5",
@@ -1983,8 +1983,19 @@ runtime loads once, after consent when a gate is declared, and never in a frame
1983
1983
  or during prerender. That is a **separate path with no connection to the stream
1984
1984
  below** — the vendor measures its own way, and nothing you `track()` reaches it.
1985
1985
 
1986
- **The runtime reports `page_view` on every route change, including the first.**
1987
- That is the only thing it emits on its own — everything else is yours to report:
1986
+ **Some events you get for free.** These are reported for you, with no call from
1987
+ your foundation and regardless of how it renders:
1988
+
1989
+ | event | when |
1990
+ |---|---|
1991
+ | `page_view` | every route change, including the first |
1992
+ | `outbound_click` | a visitor follows a link off the site — the destination **host** only, never the full URL |
1993
+ | `section_view` | a section first becomes half-visible, on pages that ask for it (`trackSections`, below) |
1994
+
1995
+ ⛔ **So don't write your own link-click or scroll-into-view listener for these.**
1996
+ A second one double-counts against a collector that already has them.
1997
+
1998
+ Everything else is yours to report:
1988
1999
 
1989
2000
  ```jsx
1990
2001
  // In a section type, the block is already in your props.
@@ -2001,12 +2012,79 @@ const { track } = useTracker()
2001
2012
  <button onClick={() => track('brochure_download', { file: 'specs.pdf' })}>…</button>
2002
2013
  ```
2003
2014
 
2004
- The event name is yours — there is no list of permitted names. Three are already
2005
- in use, so reach for these rather than inventing a synonym: **`page_view`** (the
2006
- runtime), **`scroll_depth`** (kit's `useScrollDepth()`, with a `depth` of 25 /
2007
- 50 / 75 / 100) and **`video_milestone`** (kit's `<Media>`, with `milestone` and
2008
- `src`). Put the varying part in a **field**, never in the name — four names for
2009
- one event turn a collector's event dimension into a cardinality problem.
2015
+ The event name is yours — there is no list of permitted names. Five are already
2016
+ in use, so reach for these rather than inventing a synonym: the three automatic
2017
+ ones above **`page_view`**, **`outbound_click`**, **`section_view`** plus
2018
+ **`video_milestone`**, which kit's `<Media>` reports for any video you render
2019
+ through it, and **`read_depth`** from `useReadingDepth()` below. Put the
2020
+ varying part in a **field**, never in the name four names for one event turn a
2021
+ collector's event dimension into a cardinality problem.
2022
+
2023
+ ### Choosing what a site sends
2024
+
2025
+ By default a site sends `page_view`, `outbound_click` and `section_view`. Narrow
2026
+ or widen that with `emit`:
2027
+
2028
+ ```yaml
2029
+ # site.yml — your own collector
2030
+ tracking:
2031
+ endpoint: https://collector.example.com/events
2032
+ emit: standard # minimal | standard | all — or a list of event names
2033
+
2034
+ # site.yml — a host that supplies the collector: say what to send, not where
2035
+ tracking:
2036
+ emit: minimal
2037
+ ```
2038
+
2039
+ ⭐ **`emit` needs no endpoint of its own.** Where a host provides one, the site
2040
+ declares only what it wants sent and the address comes from the host. The two
2041
+ are read key by key, so naming `emit` alone overrides nothing else the host
2042
+ declared. And declaring your own `endpoint:` always wins, so a site pointing at
2043
+ its own collector keeps working on any host, including none.
2044
+
2045
+ `minimal` is `page_view` alone. `standard` is the default. `all` is a standing
2046
+ yes, so an event added in a later framework release is included without you
2047
+ changing anything — which is exactly why `standard` exists as well: it is a
2048
+ curated set that a release cannot grow behind your back.
2049
+
2050
+ ⚠️ **`emit` never limits what YOU send.** `block.track()` and `useTracker()` are
2051
+ not filtered by it — the registry is open, and your events are yours. It governs
2052
+ only the ones the framework emits on its own. A host may narrow the list further
2053
+ if it will not store an event, and it can never widen past what you asked for.
2054
+
2055
+ ### Reading depth in a long section
2056
+
2057
+ `section_view` tells you a reader *arrived* at a section. For a long-form one —
2058
+ an article, a report, a case study — the question is how far they got:
2059
+
2060
+ ```jsx
2061
+ import { useRef } from 'react'
2062
+ import { useReadingDepth } from '@uniweb/kit'
2063
+
2064
+ export default function Article({ content, block }) {
2065
+ const ref = useRef(null)
2066
+ useReadingDepth({ ref, block })
2067
+ return <article ref={ref}>{/* … */}</article>
2068
+ }
2069
+ ```
2070
+
2071
+ That reports `read_depth` at 25 / 50 / 75 / 100% **of that element**, once each.
2072
+ Measuring the element rather than the page is the point: two long sections on
2073
+ one page report independently, and adding a section above them changes neither.
2074
+
2075
+ ⭐ **This one is a hook rather than automatic because only you know a section is
2076
+ long-form reading.** The framework cannot tell an essay from a row of logos, so
2077
+ it does not guess — and a foundation that never calls this pays nothing for it.
2078
+
2079
+ **`trackSections`** is one page's answer on `section_view`, and it overrides the
2080
+ site in **both** directions — instrument one page of a site that sends `minimal`,
2081
+ or exempt a noisy page of a site that sends `standard`. Say nothing and the
2082
+ site's `emit` decides.
2083
+
2084
+ ```yaml
2085
+ # page.yml
2086
+ trackSections: true # or false to exempt this page
2087
+ ```
2010
2088
 
2011
2089
  ⛔ **Never guard a `track()` call.** A site with **no** tracking destination is
2012
2090
  the default and the majority: the call returns having done nothing, opened no
@@ -241,6 +241,21 @@ async function bringLocalCodeAlong({
241
241
  args.includes('--force') ||
242
242
  args.includes('--no-verify')
243
243
 
244
+ // `--no-release`: ship the site against the code that is ALREADY released, and do
245
+ // not release the local changes.
246
+ //
247
+ // The intent is ordinary and had no name until 2026-08-19 — a developer edits
248
+ // content and a component in one sitting, and wants the copy fix live without
249
+ // shipping a half-finished component. Until now the way to get it was `--yes`,
250
+ // which means "do not ask me": the behaviour was reachable only as a side effect of
251
+ // a confirmation-skipper, which is discovery by accident.
252
+ //
253
+ // ⛔ It skips the RELEASE, never the REGISTRATION REQUIREMENT. A site referencing a
254
+ // foundation no deployment can resolve cannot be opened in the app at all, so where
255
+ // this flag cannot be honoured it REFUSES rather than doing the opposite of what it
256
+ // says (see the `!reg` branch).
257
+ const noRelease = args.includes('--no-release')
258
+
244
259
  // The pinned ref to stamp on the pushed site — read at RETURN time (after any
245
260
  // release), so it reflects the released version + the scope register derived.
246
261
  // null when no scoped ref can be formed (then the site.yml ref is forwarded).
@@ -250,6 +265,15 @@ async function bringLocalCodeAlong({
250
265
  return s && v ? `${s}@${v}` : null
251
266
  }
252
267
 
268
+ // The ref for a run that releases NOTHING — the version the catalog actually holds,
269
+ // which is not always the local one. ⚠️ `pinnedRef()` reads the LOCAL package.json,
270
+ // so on a bumped-but-unreleased foundation it names a version nobody can serve. Any
271
+ // branch that skips a release must bind to this instead.
272
+ const registeredRef = (reg) =>
273
+ local.scopedName && reg?.latest_version
274
+ ? `${local.scopedName}@${reg.latest_version}`
275
+ : null
276
+
253
277
  // Dry-run reports the intent WITHOUT touching the network — it must not force
254
278
  // a login (the digest read is auth-gated). The real run does the compare.
255
279
  if (dryRun) {
@@ -277,6 +301,18 @@ async function bringLocalCodeAlong({
277
301
  : null
278
302
 
279
303
  if (!reg) {
304
+ // ⛔ Nothing to bind to. Releasing anyway would be the opposite of what was asked,
305
+ // and shipping anyway would leave a site the app cannot open — so stop and say so.
306
+ if (noRelease) {
307
+ say.err(
308
+ `--no-release, but ${label} has never been released — there is no registered version to bind to.`
309
+ )
310
+ say.dim(
311
+ `A site referencing an unreleased ${kind} cannot be opened in the app, so this cannot be skipped.`
312
+ )
313
+ say.dim(`Drop \`--no-release\` to release it now.`)
314
+ return { released: false, proceed: false, refused: true, ref: null }
315
+ }
280
316
  say.info(`Releasing the ${kind} ${label} (not yet registered)…`)
281
317
  return {
282
318
  released: releaseFoundation(local, args, cliBin, say),
@@ -299,6 +335,15 @@ async function bringLocalCodeAlong({
299
335
 
300
336
  // A different version locally → a new version to release.
301
337
  if (local.version && local.version !== reg.latest_version) {
338
+ if (noRelease) {
339
+ // ⚠️ Bind to the REGISTERED version, not the local one. `pinnedRef()` would
340
+ // return the bumped-but-unreleased `local.version` here — a ref no deployment
341
+ // can serve, which is the very failure this flag must not create.
342
+ say.info(
343
+ `Keeping the released ${kind} ${reg.latest_version} — local ${local.version} not released (\`--no-release\`).`
344
+ )
345
+ return { released: false, proceed: true, ref: registeredRef(reg) }
346
+ }
302
347
  say.info(
303
348
  `Releasing the ${kind} ${label} (new version; registered latest is ${reg.latest_version})…`
304
349
  )
@@ -313,6 +358,12 @@ async function bringLocalCodeAlong({
313
358
  if (!reg.digest) {
314
359
  // Degrade: the backend doesn't return the stored digest yet, so we can't
315
360
  // be sure the registered version matches local. Offer to re-deliver.
361
+ if (noRelease) {
362
+ say.info(
363
+ `Keeping the released ${kind} ${reg.latest_version} — nothing released (\`--no-release\`).`
364
+ )
365
+ return { released: false, proceed: true, ref: registeredRef(reg) }
366
+ }
316
367
  say.warn(
317
368
  `Can't verify the registered ${label} matches your local copy (backend returned no digest).`
318
369
  )
@@ -338,6 +389,14 @@ async function bringLocalCodeAlong({
338
389
  // Case 3 (§4): the code was edited but the version wasn't bumped. The
339
390
  // registered version is immutable, so we never silently ship the old code —
340
391
  // the deliberate release gate is a version bump (§3.1).
392
+ // Asked for explicitly: the case this flag exists to name.
393
+ if (noRelease) {
394
+ say.info(
395
+ `Keeping the released ${kind} ${reg.latest_version} — your local changes are not released (\`--no-release\`).`
396
+ )
397
+ return { released: false, proceed: true, ref: registeredRef(reg) }
398
+ }
399
+
341
400
  // ⭐ THREE OUTCOMES, NOT TWO — `--yes` and "no TTY" are not the same answer.
342
401
  //
343
402
  // They were one condition until 2026-08-19, and conflating them meant the only
@@ -372,7 +431,12 @@ async function bringLocalCodeAlong({
372
431
  )
373
432
  say.dim(`Nothing was sent. Either release the change, or ship without it:`)
374
433
  say.dim(` • bump the ${kind}'s version in package.json, then re-run \`uniweb ${verb}\``)
375
- say.dim(` • \`uniweb ${verb} --yes\` sends content bound to the registered ${reg.latest_version}`)
434
+ // Teach the flag that NAMES this, not `--yes`. Both work, but `--yes` means "do
435
+ // not ask me" and only does this as a side effect — pointing a stuck user at it
436
+ // teaches a blunt instrument for a precise job.
437
+ say.dim(
438
+ ` • \`uniweb ${verb} --no-release\` — sends content bound to the released ${reg.latest_version}`
439
+ )
376
440
  return { released: false, proceed: false, refused: true, ref: null }
377
441
  }
378
442
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-08-19T04:22:45.477Z",
3
+ "generatedAt": "2026-08-19T16:10:23.333Z",
4
4
  "packages": {
5
5
  "@uniweb/build": {
6
6
  "version": "0.24.5",
@@ -29,7 +29,7 @@
29
29
  "deps": []
30
30
  },
31
31
  "@uniweb/core": {
32
- "version": "0.10.1",
32
+ "version": "0.10.2",
33
33
  "path": "framework/core",
34
34
  "deps": [
35
35
  "@uniweb/semantic-parser",
@@ -49,7 +49,7 @@
49
49
  ]
50
50
  },
51
51
  "@uniweb/kit": {
52
- "version": "0.12.3",
52
+ "version": "0.13.0",
53
53
  "path": "framework/kit",
54
54
  "deps": [
55
55
  "@uniweb/core",
@@ -76,7 +76,7 @@
76
76
  ]
77
77
  },
78
78
  "@uniweb/runtime": {
79
- "version": "0.12.2",
79
+ "version": "0.12.4",
80
80
  "path": "framework/runtime",
81
81
  "deps": [
82
82
  "@uniweb/core",
package/src/index.js CHANGED
@@ -1316,6 +1316,11 @@ FOUNDATION on its own use \`uniweb register\` (alias \`uniweb release\`).
1316
1316
  ${colors.bright}Options:${colors.reset}
1317
1317
  --dry-run Resolve everything; release/sync/POST nothing
1318
1318
  --yes Skip confirmations (CI); never block on a prompt
1319
+ --no-release Ship the content against the code already released; release
1320
+ nothing. For editing content and a component in one sitting
1321
+ when only the content should go live. Refused if the
1322
+ foundation has never been released — there is nothing to
1323
+ bind to, and the app cannot open such a site.
1319
1324
  --no-save Skip the deploy.yml lastDeploy auto-save
1320
1325
  --no-validate Skip the content-conformance check (it only warns)
1321
1326
  --org @org Publish under @org (membership-gated; alias: --as-org). Read
@@ -1753,6 +1758,7 @@ ${colors.bright}Global Options:${colors.reset}
1753
1758
  ${colors.bright}Publish Options:${colors.reset}
1754
1759
  --dry-run Resolve everything; release/sync/POST nothing
1755
1760
  --yes Skip confirmations (CI); never block on a prompt
1761
+ --no-release Ship content against the already-released code; release nothing
1756
1762
  --org @org Publish under @org (first publish only; then remembered)
1757
1763
  --personal Own the new site personally, deliberately (first publish only)
1758
1764
  --no-save Skip the deploy.yml lastDeploy auto-save
@@ -63,14 +63,16 @@ const VERBS = {
63
63
  // one of the three flags that skip its prompts. Found by
64
64
  // flag-guard-coverage.test.js the moment push gained the import, which is
65
65
  // exactly the hand-enumeration failure that test exists to catch.
66
- '--no-verify', ...VIA_DEPLOY
66
+ '--no-verify',
67
+ // ship content against the already-released code, releasing nothing
68
+ '--no-release', ...VIA_DEPLOY
67
69
  ],
68
70
  publish: [
69
71
  '--as-org', '--org', '--backend', '--dry-run', '--force', '--foundation',
70
72
  '--personal', '--registry', '--token',
71
73
  // read in utils/conformance.js, backend/site-sync.js, and
72
74
  // backend/foundation-bring-along.js — none appear in publish.js
73
- '--no-validate', '--yes', '--no-verify', ...VIA_DEPLOY
75
+ '--no-validate', '--yes', '--no-verify', '--no-release', ...VIA_DEPLOY
74
76
  ],
75
77
  pull: [
76
78
  '--backend', '--content-only', '--dry-run', '--force', '--merge',
@@ -90,7 +92,9 @@ const VERBS = {
90
92
  status: [
91
93
  '--backend', '--json', '--registry', '--remote', '--token', '--dry-run',
92
94
  '--force', '--no-verify', '--no-validate', '--yes', '--org', '--as-org',
93
- ...VIA_DEPLOY
95
+ // inert here, reachable through the bring-along module status imports for
96
+ // `resolveLocalFoundation` — listed per the over-approximation note above
97
+ '--no-release', ...VIA_DEPLOY
94
98
  ],
95
99
  /**
96
100
  * `refresh` = `git pull`, then a DELEGATED `pull --merge`.