@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 +2 -2
- package/src/index.js +18 -7
- package/src/schemas.js +22 -87
- package/src/uniweb.js +22 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/core",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
46
|
-
|
|
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
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* Rich schemas live under `data.schemas` in a component's meta.js. They
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
-
// ⛔
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
//
|
|
82
|
-
//
|
|
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
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
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 =
|
|
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
|