@uniweb/build 0.16.16 → 0.16.17
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 +5 -5
- package/src/dev/plugin.js +8 -0
- package/src/uwx/data-schema.js +83 -8
- package/src/vite-foundation-plugin.js +32 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.17",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -59,15 +59,15 @@
|
|
|
59
59
|
"js-yaml": "^4.1.0",
|
|
60
60
|
"sharp": "^0.35.3",
|
|
61
61
|
"yaml": "^2.5.0",
|
|
62
|
-
"@uniweb/
|
|
62
|
+
"@uniweb/theming": "0.1.15",
|
|
63
63
|
"@uniweb/projections": "0.2.5",
|
|
64
|
-
"@uniweb/
|
|
64
|
+
"@uniweb/content-writer": "0.3.3"
|
|
65
65
|
},
|
|
66
66
|
"optionalDependencies": {
|
|
67
67
|
"@uniweb/content-reader": "1.2.2",
|
|
68
|
-
"@uniweb/
|
|
68
|
+
"@uniweb/semantic-parser": "1.2.1",
|
|
69
69
|
"@uniweb/schemas": "0.2.5",
|
|
70
|
-
"@uniweb/
|
|
70
|
+
"@uniweb/runtime": "0.9.7"
|
|
71
71
|
},
|
|
72
72
|
"peerDependencies": {
|
|
73
73
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
|
package/src/dev/plugin.js
CHANGED
|
@@ -26,6 +26,7 @@ import { readFile } from 'node:fs/promises'
|
|
|
26
26
|
import { existsSync, readdirSync } from 'node:fs'
|
|
27
27
|
import { build } from 'vite'
|
|
28
28
|
import { resolveFoundationSrcPath } from '../utils/foundation-source-root.js'
|
|
29
|
+
import { DEV_REBUILD_MARKER } from '../vite-foundation-plugin.js'
|
|
29
30
|
|
|
30
31
|
/** Directories that never hold foundation source and must never be walked. */
|
|
31
32
|
const UNWATCHABLE_DIRS = new Set(['node_modules', 'dist', 'build', 'coverage'])
|
|
@@ -126,6 +127,13 @@ export function foundationDevPlugin(options = {}) {
|
|
|
126
127
|
root: resolvedFoundationPath,
|
|
127
128
|
configFile: existsSync(configPath) ? configPath : false,
|
|
128
129
|
logLevel: 'warn',
|
|
130
|
+
// Marks this as a dev rebuild so the foundation plugin skips the
|
|
131
|
+
// `entry-ssr.js` sub-build — a second full Vite pass, on every save,
|
|
132
|
+
// producing something nothing in the dev loop reads. A marker plugin
|
|
133
|
+
// rather than an option because the foundation's own vite.config.js
|
|
134
|
+
// constructs the plugin, not us; and rather than `mode`/`command`,
|
|
135
|
+
// which both say "build" here since this IS a real build.
|
|
136
|
+
plugins: [{ name: DEV_REBUILD_MARKER }],
|
|
129
137
|
build: {
|
|
130
138
|
outDir: 'dist',
|
|
131
139
|
emptyOutDir: true,
|
package/src/uwx/data-schema.js
CHANGED
|
@@ -29,6 +29,14 @@ const TEXT_KINDS = new Set(['string', 'text'])
|
|
|
29
29
|
// carrying one round-trips as the RAW source string (no ProseMirror conversion).
|
|
30
30
|
const CONTENT_TEXT_FORMATS = new Set(['markdown', 'html'])
|
|
31
31
|
|
|
32
|
+
// The field an open map's key lowers into. `name` matches the idiom already in use
|
|
33
|
+
// for this shape (the backend's site-content `collections` section), so an open map
|
|
34
|
+
// and a hand-authored row set produce the same wire shape rather than two spellings
|
|
35
|
+
// of one thing. Not configurable on purpose: a second way to spell it would be a
|
|
36
|
+
// vocabulary addition for a collision that does not exist yet — a value schema that
|
|
37
|
+
// declares its own `name` is an error instead, which names the problem precisely.
|
|
38
|
+
const OPEN_MAP_KEY = 'name'
|
|
39
|
+
|
|
32
40
|
/**
|
|
33
41
|
* A `text` field marked as rich content (`format: markdown` or `html`): the
|
|
34
42
|
* file-based body target. Round-trips as the raw source string — what the retired
|
|
@@ -119,7 +127,7 @@ function lowerSectionsForm(sectionsMap, resolve, optResolve) {
|
|
|
119
127
|
let explicit = null
|
|
120
128
|
let firstSingle = null
|
|
121
129
|
for (const [secName, def] of Object.entries(sectionsMap)) {
|
|
122
|
-
sections[secName] = lowerSection(def, resolve, optResolve)
|
|
130
|
+
sections[secName] = lowerSection(def, resolve, optResolve, secName)
|
|
123
131
|
if (def.brief === true) explicit = secName
|
|
124
132
|
if (!firstSingle && (def.kind || 'single') === 'single') firstSingle = secName
|
|
125
133
|
}
|
|
@@ -138,7 +146,7 @@ function lowerSectionsForm(sectionsMap, resolve, optResolve) {
|
|
|
138
146
|
// are type: section"); `nestable` → `self_nesting`; `append_only` (insert-only
|
|
139
147
|
// records) passes through; authored cross-cutting `constraints` pass through as a
|
|
140
148
|
// bare array. Leaves and nested sections share one ordered `fields:` namespace.
|
|
141
|
-
function lowerSection(def, resolve, optResolve) {
|
|
149
|
+
function lowerSection(def, resolve, optResolve, path = '') {
|
|
142
150
|
const out = {}
|
|
143
151
|
if ((def.kind || 'single') === 'multi') out.multiple = true
|
|
144
152
|
if (def.brief === true) out.brief = true
|
|
@@ -147,14 +155,29 @@ function lowerSection(def, resolve, optResolve) {
|
|
|
147
155
|
|
|
148
156
|
const fields = {}
|
|
149
157
|
for (const [key, rawField] of Object.entries(def.fields || {})) {
|
|
150
|
-
fields[key] = lowerField(rawField, resolve, optResolve)
|
|
158
|
+
fields[key] = lowerField(rawField, resolve, optResolve, path ? `${path}/${key}` : key)
|
|
151
159
|
}
|
|
152
160
|
// Explicit child sections (sections-form, e.g. under a binder) → `type: section`
|
|
153
161
|
// fields, in the same ordered namespace as the leaves.
|
|
154
162
|
for (const [childName, childDef] of Object.entries(def.sections || {})) {
|
|
155
|
-
|
|
163
|
+
const childPath = path ? `${path}/${childName}` : childName
|
|
164
|
+
fields[childName] = {
|
|
165
|
+
type: 'section',
|
|
166
|
+
...lowerSection(childDef, resolve, optResolve, childPath)
|
|
167
|
+
}
|
|
156
168
|
}
|
|
157
|
-
|
|
169
|
+
// A section with neither leaves nor sub-sections carries nothing, and is not a
|
|
170
|
+
// valid section on this wire by either party's reckoning. Refusing here fails at
|
|
171
|
+
// the schema author's screen; emitting it fails in a consumer's restore, which is
|
|
172
|
+
// the last possible moment and the wrong screen (2026-08-04: `@std/form` shipped
|
|
173
|
+
// exactly this, because `values:` had no lowering and silently produced no fields).
|
|
174
|
+
if (!Object.keys(fields).length) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`Data schema: section '${path || '(root)'}' declares no fields and no sub-sections. ` +
|
|
177
|
+
`A section must carry at least one leaf or child section.`
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
out.fields = fields
|
|
158
181
|
if (Array.isArray(def.constraints) && def.constraints.length) out.constraints = def.constraints
|
|
159
182
|
return out
|
|
160
183
|
}
|
|
@@ -166,17 +189,69 @@ function lowerSection(def, resolve, optResolve) {
|
|
|
166
189
|
// array of ref → entity_ref + multiple
|
|
167
190
|
// array of scalar → the scalar kind + multiple
|
|
168
191
|
// ref → entity_ref (model by name)
|
|
169
|
-
function lowerField(rawField, resolve, optResolve) {
|
|
192
|
+
function lowerField(rawField, resolve, optResolve, path = '') {
|
|
170
193
|
const field = asField(rawField)
|
|
171
194
|
const type = field.type
|
|
172
195
|
|
|
173
196
|
if (type === 'object') {
|
|
174
|
-
|
|
197
|
+
// An OPEN MAP (`values:`) is ROWS, not a singleton. Its keys belong to the
|
|
198
|
+
// author, which makes them data — so the map lowers to a `multi` section whose
|
|
199
|
+
// key field carries what was the object key, with a section-scoped uniqueness
|
|
200
|
+
// rule making that key the row's identity. This is the same shape `array of
|
|
201
|
+
// object` already lowers to, and the idiom the backend's own site-content
|
|
202
|
+
// `collections` section uses; no new wire construct is involved.
|
|
203
|
+
//
|
|
204
|
+
// Identity is the KEY, never row position — a round-trip that rebuilds the map
|
|
205
|
+
// from order looks correct and drifts the first time rows are reordered.
|
|
206
|
+
// Authoring order is still preserved into row order, because for a form the
|
|
207
|
+
// field order is what the visitor sees.
|
|
208
|
+
if (field.values !== undefined) {
|
|
209
|
+
const value = asField(field.values)
|
|
210
|
+
if (value.type !== 'object' || !value.fields) {
|
|
211
|
+
throw new Error(
|
|
212
|
+
`Data schema: open map at '${path}' declares 'values' that is not an object with ` +
|
|
213
|
+
`'fields'. An open map lowers to rows, and a row needs declared columns.`
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
if (value.fields[OPEN_MAP_KEY]) {
|
|
217
|
+
throw new Error(
|
|
218
|
+
`Data schema: open map at '${path}' has a value field named '${OPEN_MAP_KEY}', which ` +
|
|
219
|
+
`is the field the map's key lowers into. Rename that field.`
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
type: 'section',
|
|
224
|
+
...lowerSection(
|
|
225
|
+
{
|
|
226
|
+
kind: 'multi',
|
|
227
|
+
// `translatable: false` is load-bearing, not tidiness: a string field is
|
|
228
|
+
// localized by default, and a localized key could differ per locale —
|
|
229
|
+
// which would destroy the identity the key exists to carry. The key is an
|
|
230
|
+
// identifier, never content.
|
|
231
|
+
fields: {
|
|
232
|
+
[OPEN_MAP_KEY]: { type: 'string', required: true, translatable: false },
|
|
233
|
+
...value.fields
|
|
234
|
+
},
|
|
235
|
+
constraints: [{ kind: 'unique_field', field: OPEN_MAP_KEY, scope: 'section' }]
|
|
236
|
+
},
|
|
237
|
+
resolve,
|
|
238
|
+
optResolve,
|
|
239
|
+
path
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
type: 'section',
|
|
245
|
+
...lowerSection({ kind: 'single', fields: field.fields }, resolve, optResolve, path)
|
|
246
|
+
}
|
|
175
247
|
}
|
|
176
248
|
if (type === 'array') {
|
|
177
249
|
const items = field.items ? asField(field.items) : null
|
|
178
250
|
if (items && items.type === 'object') {
|
|
179
|
-
return {
|
|
251
|
+
return {
|
|
252
|
+
type: 'section',
|
|
253
|
+
...lowerSection({ kind: 'multi', fields: items.fields }, resolve, optResolve, path)
|
|
254
|
+
}
|
|
180
255
|
}
|
|
181
256
|
if (items && items.type === 'ref') {
|
|
182
257
|
// Multi-valued reference — the per-field `multiple` flag (the `array` Kind
|
|
@@ -466,6 +466,22 @@ async function buildEntrySSR(foundationRoot, entrySourcePath, outDir) {
|
|
|
466
466
|
}
|
|
467
467
|
}
|
|
468
468
|
|
|
469
|
+
/**
|
|
470
|
+
* Marks a foundation build as a DEV-server rebuild.
|
|
471
|
+
*
|
|
472
|
+
* The dev server runs a real Vite `build()` of the foundation on every watched
|
|
473
|
+
* change (`dev/plugin.js`), so `command`, `mode` and `isProduction` cannot tell
|
|
474
|
+
* a dev rebuild from a shipping build — all of them say "build". An injected
|
|
475
|
+
* marker plugin can, and it survives the foundation's own `vite.config.js`
|
|
476
|
+
* being merged in, which a plugin *option* would not (the dev server does not
|
|
477
|
+
* construct the foundation plugin — the foundation's config file does).
|
|
478
|
+
*
|
|
479
|
+
* Used to skip the `entry-ssr.js` sub-build in dev: it is a second full Vite
|
|
480
|
+
* pass per save, and nothing in the dev loop reads it (dev never ships, and the
|
|
481
|
+
* SSR lanes that run locally — SSG prerender and unipress — load `entry.js`).
|
|
482
|
+
*/
|
|
483
|
+
export const DEV_REBUILD_MARKER = 'uniweb:dev-foundation-rebuild'
|
|
484
|
+
|
|
469
485
|
/**
|
|
470
486
|
* Vite plugin for foundation builds
|
|
471
487
|
*/
|
|
@@ -481,6 +497,7 @@ export function foundationBuildPlugin(options = {}) {
|
|
|
481
497
|
let resolvedOutDir
|
|
482
498
|
let resolvedRoot
|
|
483
499
|
let isProduction
|
|
500
|
+
let isDevRebuild = false
|
|
484
501
|
|
|
485
502
|
return {
|
|
486
503
|
name: 'uniweb-foundation-build',
|
|
@@ -500,6 +517,7 @@ export function foundationBuildPlugin(options = {}) {
|
|
|
500
517
|
resolvedOutDir = config.build.outDir
|
|
501
518
|
resolvedRoot = config.root
|
|
502
519
|
isProduction = config.mode === 'production'
|
|
520
|
+
isDevRebuild = (config.plugins || []).some((p) => p?.name === DEV_REBUILD_MARKER)
|
|
503
521
|
},
|
|
504
522
|
|
|
505
523
|
async writeBundle() {
|
|
@@ -554,8 +572,20 @@ export function foundationBuildPlugin(options = {}) {
|
|
|
554
572
|
//
|
|
555
573
|
// (The legacy self-contained buildSSRBundle() — React + runtime INLINED,
|
|
556
574
|
// ~14 MB with Shiki — is retained below, unused, for reference only.)
|
|
557
|
-
|
|
558
|
-
|
|
575
|
+
// Skipped on a DEV rebuild: this is a second full Vite pass, it runs on
|
|
576
|
+
// every save, and nothing in the dev loop reads its output. The local SSR
|
|
577
|
+
// lanes load `entry.js` (SSG prerender via `import()`, unipress the same);
|
|
578
|
+
// only the edge isolate needs the single-file twin, and dev ships nothing
|
|
579
|
+
// to it.
|
|
580
|
+
//
|
|
581
|
+
// Shipping lanes are unaffected — `uniweb build`, and `register`/`publish`
|
|
582
|
+
// which build through it. `register`'s build-if-stale check also requires
|
|
583
|
+
// `entry-ssr.js`, so a dist left behind by a dev session is treated as
|
|
584
|
+
// stale and rebuilt rather than uploaded without one.
|
|
585
|
+
if (!isDevRebuild) {
|
|
586
|
+
const entrySourcePath = join(resolvedSrcDir, entryFileName)
|
|
587
|
+
await buildEntrySSR(resolvedRoot, entrySourcePath, outDir)
|
|
588
|
+
}
|
|
559
589
|
},
|
|
560
590
|
|
|
561
591
|
async closeBundle() {
|