@uniweb/core 0.14.0 → 0.15.0

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,12 +1,13 @@
1
1
  {
2
2
  "name": "@uniweb/core",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "Core classes for the Uniweb platform - Uniweb, Website, Page, Block",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": "./src/index.js",
8
8
  "./base-path": "./src/base-path.js",
9
- "./collection-address": "./src/collection-address.js",
9
+ "./collection-address": "./src/query-address.js",
10
+ "./query-address": "./src/query-address.js",
10
11
  "./data-paths": "./src/data-paths.js",
11
12
  "./detail-url": "./src/detail-url.js",
12
13
  "./fetch-config": "./src/fetch-config.js",
@@ -41,7 +42,7 @@
41
42
  "vitest": "^4.1.7"
42
43
  },
43
44
  "dependencies": {
44
- "@uniweb/semantic-parser": "^1.3.1",
45
+ "@uniweb/semantic-parser": "^1.4.0",
45
46
  "@uniweb/theming": "^0.1.15"
46
47
  },
47
48
  "scripts": {
package/src/datastore.js CHANGED
@@ -62,7 +62,7 @@ export default class DataStore {
62
62
  *
63
63
  * Two forms:
64
64
  * - `subscribe(fn)` — fires after every successful `set()` (all keys).
65
- * - `subscribe(key, fn)` — fires only when `set(key, ...)` is called.
65
+ * - `subscribe(key, fn)` — fires only when `set(key, ...)` or `delete(key)` is called.
66
66
  *
67
67
  * The global form is useful for debugging / blanket observers. The keyed
68
68
  * form is what Layer-3 kit hooks (`useFetched`, `useCacheEntry`) use so
@@ -131,6 +131,29 @@ export default class DataStore {
131
131
  }
132
132
  }
133
133
 
134
+ /**
135
+ * Drop one entry, and any in-flight record for the same key.
136
+ *
137
+ * Fires the key's subscribers (and the global ones) so an observer re-reads
138
+ * and sees the absence, exactly as it would see a write. Exists for the case
139
+ * `clear()` is too blunt for: one consumer's entries must leave memory — a
140
+ * viewer's records at sign-out — while everything else stays warm.
141
+ *
142
+ * @param {string} key
143
+ * @returns {boolean} true if an entry was removed
144
+ */
145
+ delete(key) {
146
+ const had = this._cache.delete(key)
147
+ this._inflight.delete(key)
148
+ if (!had) return false
149
+ for (const fn of this._listeners) fn()
150
+ const keyed = this._keyedListeners.get(key)
151
+ if (keyed) {
152
+ for (const fn of keyed) fn()
153
+ }
154
+ return true
155
+ }
156
+
134
157
  /**
135
158
  * In-flight fetch registry — used by the dispatcher to dedup concurrent
136
159
  * requests and collect abort signals so the underlying fetch is cancelled
package/src/index.js CHANGED
@@ -20,15 +20,21 @@ export { default as ObservableState } from './observable-state.js'
20
20
 
21
21
  // Utilities
22
22
  export { substitutePlaceholders } from './substitute-placeholders.js'
23
- export {
24
- resolveQueryAddress,
25
- resolveRecordAddressPattern,
26
- } from './query-address.js'
23
+ // ⛔ `resolveQueryAddress` / `resolveRecordAddressPattern` are NOT re-exported
24
+ // here. They are read by `./fetch-config.js` and by nothing else in any repo
25
+ // (measured 2026-09-01 over every .js/.jsx/.mjs/.ts/.tsx file in the
26
+ // workspace). A consumer that needs them imports `@uniweb/core/query-address`,
27
+ // which is a declared subpath and a zero-dependency leaf — the same reach
28
+ // `route-match` and `section-id` already have. Re-exporting an internal from
29
+ // the package entry is not free: the import-map bridge enumerates this file's
30
+ // surface and emits a live named re-export for every one, so nothing here can
31
+ // ever be tree-shaken on the hosted lane.
27
32
  export { resolveFetchConfigs } from './fetch-config.js'
28
33
  export { buildDetailConfig } from './detail-url.js'
34
+ // `isWildcardLanguages` is likewise internal — `./locale-config.js` reads it
35
+ // and nothing else does. Same subpath escape hatch: `@uniweb/core/locale-config`.
29
36
  export {
30
37
  normalizeLanguageList,
31
- isWildcardLanguages,
32
38
  resolveDefaultLocale,
33
39
  resolvePublishableLocales,
34
40
  validateLanguageConfig
@@ -42,8 +48,18 @@ export {
42
48
  isDataUrl
43
49
  } from './data-paths.js'
44
50
  export { evaluate as evaluateWhere, match as matchWhere } from './where.js'
45
- export { isRichSchema, normalizeSchema } from './schemas.js'
46
- export { default as Tracker } from './tracker.js'
51
+ export { isRichSchema } from './schemas.js'
52
+ // `normalizeSchema` MOVED to `@uniweb/schemas/editor-form` (2026-09-01)
53
+ // it is editor-only and never called `isRichSchema`. Still exported here for
54
+ // ONE more release so `frontend` (a `workspace:*` pin, so a removal is live
55
+ // at commit time) can migrate its single import. Delete both lines then.
56
+ export { normalizeSchema } from './schemas.js'
57
+ // ⛔ `Tracker` is NOT on the package entry. It is a FEATURE, not part of the
58
+ // object graph this package exists to define, and putting it here made every
59
+ // consumer of core carry 1,576 gzip of it -- press, unipress, `@uniweb/api`
60
+ // and every SSR isolate included -- for a class only the browser runtime ever
61
+ // wires. Its one importer already reaches it correctly, through the
62
+ // `@uniweb/core/tracker` subpath (`runtime/src/wire-foundation.js`).
47
63
  // Also available as the zero-dependency leaves `@uniweb/core/services` and
48
64
  // `@uniweb/core/base-path` — which is how `@uniweb/runtime` reaches them,
49
65
  // since it must not pull the package root into an SSR/Worker bundle.
package/src/schemas.js CHANGED
@@ -37,6 +37,18 @@ export function isRichSchema(schema) {
37
37
  }
38
38
 
39
39
  /**
40
+ * ⛔ **DEPRECATED — MOVED to `@uniweb/schemas/editor-form` (2026-09-01).**
41
+ * This copy stays only so `frontend`, which pins core as `workspace:*` and
42
+ * would break at commit time, can migrate its one import. Delete it once that
43
+ * lands; nothing else in any repo reads it.
44
+ *
45
+ * ⭐ **It never belonged here.** It does not call `isRichSchema` and never did
46
+ * — it re-inlines the same three checks — so nothing but adjacency held it in
47
+ * this file. The two answer different questions for different audiences:
48
+ * `isRichSchema` is a render-time dispatch predicate (`prepare-props.js`,
49
+ * `runtime-schema.js`) and stays; this is read only by an editor, while core
50
+ * ships to every site in every lane and is not tree-shaken on the hosted one.
51
+ *
40
52
  * Normalize any authored `data:` schema shape to the rich form the editor
41
53
  * renders, or null when it is not a single form at all.
42
54
  *
@@ -48,8 +60,11 @@ export function isRichSchema(schema) {
48
60
  * { fields: { name: spec } } a RESOLVED NAMED REF → FALSE
49
61
  * { name: spec } meta.js inline field map → false
50
62
  *
51
- * The middle row is the important one and the reason this helper is in `core`
52
- * rather than in the editor. A named ref (`'@/article'`, `'@std/person'`) is the
63
+ * The middle row is the important one and the reason this helper exists at all
64
+ * (it once read "the reason it is in `core` rather than in the editor" — that
65
+ * was the wrong conclusion from a right observation: the gap is real, closing
66
+ * it in one shared place is right, and `@uniweb/schemas` is that place).
67
+ * A named ref (`'@/article'`, `'@std/person'`) is the
53
68
  * FIRST authoring form the docs show, and `validateAndNormalizeSchema` in the
54
69
  * build resolves it to `{ fields: <MAP> }` — a map, not an array. So filtering
55
70
  * with `isRichSchema` discards not merely "simple" schemas but the primary
package/src/uniweb.js CHANGED
@@ -12,7 +12,6 @@
12
12
  */
13
13
 
14
14
  import Website from './website.js'
15
- import Tracker from './tracker.js'
16
15
 
17
16
  export default class Uniweb {
18
17
  /**
@@ -72,21 +71,40 @@ export default class Uniweb {
72
71
  // Populated by prerender before rendering, read synchronously by Icon.
73
72
  this.iconCache = new Map()
74
73
 
75
- // Site tracking — one event stream.
74
+ // Site tracking — one event stream. The runtime installs it in L2
75
+ // (`wire-foundation.js` → `wireTracker`), and only for a site that declares
76
+ // a destination. It cannot be configured here even though `activeWebsite`
77
+ // already exists two lines up, because the address is resolved against
78
+ // `website.basePath` and **that is still `''` at this point** —
79
+ // `setBasePath()` runs later, from the runtime. Resolving here would
80
+ // silently drop the base prefix on every subdirectory deployment.
76
81
  //
77
- // ⛔ Deliberately constructed DISABLED, and the runtime replaces it in L2
78
- // (`wire-foundation.js` `wireTracker`). It cannot be configured here even
79
- // though `activeWebsite` already exists two lines up, because the address is
80
- // resolved against `website.basePath` and **that is still `''` at this
81
- // point** `setBasePath()` runs later, from the runtime. Resolving here
82
- // would silently drop the base prefix on every subdirectory deployment.
82
+ // ⛔ **Core does not construct a Tracker, and must not start.** Until
83
+ // 2026-09-01 this line was `new Tracker()` a working no-op, justified as
84
+ // letting `uniweb.tracking.track(…)` run unguarded in every lane. Two
85
+ // things were wrong with that. It put a *feature* in the object graph,
86
+ // which is not what this class is for; and the guard it bought was never
87
+ // taken up every reader in the workspace already optional-chains
88
+ // (`block.js`'s `track()`, and the runtime's `usePageView`,
89
+ // `useSectionViews`, `useOutboundClicks`, `useSectionClicks`), so the
90
+ // invariant lived only in the comment. Cost of carrying it: 1,576 gzip in
91
+ // core's chunk, which press, unipress, `@uniweb/api` and every SSR isolate
92
+ // paid for a class they never wire.
83
93
  //
84
- // The disabled instance is not a placeholder to null-check: it is a working
85
- // no-op, so `uniweb.tracking.track(…)` is safe in every lane press,
86
- // unipress, an SSR isolate, or before wiring — with no guard at the call
87
- // site. Same slot-declared-here-so-seal-permits-assignment pattern as
94
+ // **Read it as nullable.** `globalThis.uniweb?.tracking?.track(…)` is
95
+ // the call shape, and it is what every existing caller already writes.
96
+ // Same slot-declared-here-so-seal-permits-assignment pattern as
88
97
  // `defaultInsets` above.
89
- this.tracking = new Tracker()
98
+ this.tracking = null
99
+
100
+ // Reserved for `@uniweb/api` — the one client instance per page. That
101
+ // package is bundled into each foundation, so a page with a primary
102
+ // foundation and extensions holds several copies of its code; the first to
103
+ // need a client creates one and parks it here, and every other copy adopts
104
+ // it instead of creating another. Core implements nothing on it: declared
105
+ // null so the seal permits the assignment, nothing more. Same pattern as
106
+ // `defaultInsets` above.
107
+ this.api = null
90
108
 
91
109
  Object.seal(this)
92
110
  }