@uniweb/core 0.14.1 → 0.16.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,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/core",
3
- "version": "0.14.1",
3
+ "version": "0.16.0",
4
4
  "description": "Core classes for the Uniweb platform - Uniweb, Website, Page, Block",
5
5
  "type": "module",
6
6
  "exports": {
@@ -43,7 +43,7 @@
43
43
  },
44
44
  "dependencies": {
45
45
  "@uniweb/theming": "^0.1.15",
46
- "@uniweb/semantic-parser": "^1.3.1"
46
+ "@uniweb/semantic-parser": "^1.4.0"
47
47
  },
48
48
  "scripts": {
49
49
  "test": "vitest run"
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,13 @@ 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
+ // `Tracker` is NOT on the package entry. It is a FEATURE, not part of the
53
+ // object graph this package exists to define, and putting it here made every
54
+ // consumer of core carry 1,576 gzip of it -- press, unipress, `@uniweb/api`
55
+ // and every SSR isolate included -- for a class only the browser runtime ever
56
+ // wires. Its one importer already reaches it correctly, through the
57
+ // `@uniweb/core/tracker` subpath (`runtime/src/wire-foundation.js`).
47
58
  // Also available as the zero-dependency leaves `@uniweb/core/services` and
48
59
  // `@uniweb/core/base-path` — which is how `@uniweb/runtime` reaches them,
49
60
  // since it must not pull the package root into an SSR/Worker bundle.
package/src/schemas.js CHANGED
@@ -1,18 +1,26 @@
1
1
  /**
2
- * Shared helpers for rich form schemas.
3
- *
4
- * Rich schemas live under `data.schemas` in a component's meta.js. They
5
- * drive two author input paths that both land at `content.data[schema-id]`:
6
- *
7
- * 1. Tagged markdown blocks (``` ```yaml:<id> ``` ```)
8
- * 2. The FormBlock editor widget
9
- *
10
- * Detection is shared across the build pipeline (emit path), the runtime
11
- * (dispatch in applySchemas), and the editor (filter for FormBlock menu)
12
- * so all three agree on what counts as a rich schema.
13
- *
14
- * Conditional field visibility (a rich-schema feature) is an editor-only
15
- * concern and is not implemented here the editor owns its own evaluator.
2
+ * The rich-form-schema predicate. One function, and deliberately only one.
3
+ *
4
+ * Rich schemas live under `data.schemas` in a component's meta.js. They drive
5
+ * two author input paths that both land at `content.data[schema-id]`: tagged
6
+ * markdown blocks (``` ```yaml:<id> ``` ```) and the editor's FormBlock widget.
7
+ *
8
+ * `isRichSchema` is a **dispatch** predicate — "is this already the rich shape?"
9
+ * — and it is read at render by `runtime/src/prepare-props.js` (`applySchemas`)
10
+ * and at build by `build/src/runtime-schema.js`. Both are on the hot path of
11
+ * every site, which is what earns it a place in this package.
12
+ *
13
+ * ⛔ **Do not add editor-side schema code here.** `normalizeSchema` — "can this
14
+ * be edited, and as what?" sat beside this function until 2026-09-01 purely
15
+ * because the two were adjacent; it never called `isRichSchema` and had no
16
+ * consumer outside the editor. `@uniweb/core` loads on every site in every lane
17
+ * and is not tree-shaken on the hosted one, so an editor-only function on its
18
+ * entry is paid for by every visitor of every site. It now lives in the
19
+ * zero-dependency leaf that owns what a data-schema *means*:
20
+ * **`@uniweb/schemas/editor-form`**.
21
+ *
22
+ * Conditional field visibility (a rich-schema feature) is likewise editor-only
23
+ * and is not implemented here — the editor owns its own evaluator.
16
24
  */
17
25
 
18
26
  /**
@@ -35,76 +43,3 @@ export function isRichSchema(schema) {
35
43
  if (schema.childSchema && typeof schema.childSchema === 'object') return true
36
44
  return false
37
45
  }
38
-
39
- /**
40
- * Normalize any authored `data:` schema shape to the rich form the editor
41
- * renders, or null when it is not a single form at all.
42
- *
43
- * WHY THIS EXISTS. `isRichSchema` answers "is this already the rich shape?",
44
- * which is the right question for dispatch and the wrong one for "can this be
45
- * edited". There are THREE authored shapes and it accepts exactly one:
46
- *
47
- * { fields: [ {id, …} ] } meta.js inline rich-form → true
48
- * { fields: { name: spec } } a RESOLVED NAMED REF → FALSE
49
- * { name: spec } meta.js inline field map → false
50
- *
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
53
- * FIRST authoring form the docs show, and `validateAndNormalizeSchema` in the
54
- * build resolves it to `{ fields: <MAP> }` — a map, not an array. So filtering
55
- * with `isRichSchema` discards not merely "simple" schemas but the primary
56
- * documented one, and any consumer that wants to render it has to re-derive the
57
- * conversion. Three consumers re-deriving it is exactly the divergence the
58
- * shared predicate was introduced to prevent.
59
- *
60
- * A field map is an unordered `fields[]`, so the conversion is mechanical.
61
- * Ordering comes from `Object.entries`, which is insertion order for string keys
62
- * — i.e. the order the author wrote, which is the order a form should show.
63
- *
64
- * `sections` returns null on purpose: a sectioned data-schema describes a Model
65
- * with several sections, which is not one form. Flattening it would invent a
66
- * layout the author never expressed.
67
- *
68
- * @param {*} schema - any authored or resolved `data:` schema value
69
- * @returns {{ fields: Array<object> } | null} the rich shape, or null
70
- */
71
- export function normalizeSchema(schema) {
72
- if (!schema || typeof schema !== 'object' || Array.isArray(schema))
73
- return null
74
-
75
- // Already rich — hand back untouched. Composite/childSchema variants are rich
76
- // by `isRichSchema`'s definition and are not ours to reshape.
77
- if (Array.isArray(schema.fields)) return schema
78
- if (schema.isComposite === true || schema.childSchema) return schema
79
-
80
- // A sectioned Model is not a single form.
81
- if (schema.sections !== undefined) return null
82
-
83
- const mapToFields = (map) =>
84
- Object.entries(map).map(([id, spec]) =>
85
- typeof spec === 'string' ? { id, type: spec } : { id, ...spec }
86
- )
87
-
88
- // A resolved named ref: `fields` present, as a map.
89
- if (schema.fields && typeof schema.fields === 'object') {
90
- const { fields, ...rest } = schema
91
- return { ...rest, fields: mapToFields(fields) }
92
- }
93
-
94
- // An inline field map: no `fields` key, so every value must be an OBJECT
95
- // carrying `type`.
96
- //
97
- // The bare-type string shorthand (`{ cpu: 'string' }`) is deliberately NOT
98
- // accepted here, even though schema FILES support it. Without a `fields` key
99
- // there is nothing to distinguish it from ordinary data: `{ name: 'Acme' }` and
100
- // `{ cpu: 'string' }` are the same shape, and an earlier cut of this function
101
- // turned `{ name, description }` into a two-field form. Erring toward null
102
- // costs an author the object spelling; erring the other way invents a form out
103
- // of a config block.
104
- const entries = Object.entries(schema)
105
- if (!entries.length) return null
106
- const isFieldSpec = ([, v]) =>
107
- v && typeof v === 'object' && !Array.isArray(v) && v.type !== undefined
108
- if (!entries.every(isFieldSpec)) return null
109
- return { fields: mapToFields(schema) }
110
- }
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,31 @@ 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
90
99
 
91
100
  // Reserved for `@uniweb/api` — the one client instance per page. That
92
101
  // package is bundled into each foundation, so a page with a primary