@uniweb/build 0.74.1 → 0.74.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/build",
3
- "version": "0.74.1",
3
+ "version": "0.74.3",
4
4
  "description": "Build tooling for the Uniweb Component Web Platform",
5
5
  "type": "module",
6
6
  "exports": {
@@ -59,13 +59,13 @@
59
59
  "yaml": "^2.5.0",
60
60
  "@uniweb/content-reader": "^1.2.8",
61
61
  "@uniweb/content-writer": "^0.3.4",
62
+ "@uniweb/schemas": "^0.10.1",
62
63
  "@uniweb/projections": "^0.7.11",
63
- "@uniweb/semantic-parser": "^1.5.1",
64
- "@uniweb/schemas": "^0.10.0",
65
- "@uniweb/theming": "^0.2.0"
64
+ "@uniweb/theming": "^0.2.0",
65
+ "@uniweb/semantic-parser": "^1.5.1"
66
66
  },
67
67
  "optionalDependencies": {
68
- "@uniweb/runtime": "^0.28.5"
68
+ "@uniweb/runtime": "^0.28.6"
69
69
  },
70
70
  "peerDependencies": {
71
71
  "vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
@@ -74,8 +74,8 @@
74
74
  "@tailwindcss/vite": "^4.0.0",
75
75
  "@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
76
76
  "vite-plugin-svgr": "^4.0.0",
77
- "@uniweb/core": "^0.35.0",
78
- "@uniweb/runtime": "^0.28.5"
77
+ "@uniweb/core": "^0.35.1",
78
+ "@uniweb/runtime": "^0.28.6"
79
79
  },
80
80
  "peerDependenciesMeta": {
81
81
  "vite": {
package/src/prerender.js CHANGED
@@ -279,10 +279,11 @@ export async function executeAllFetches(siteContent, siteDir, onProgress, locale
279
279
  *
280
280
  * Emitting the canonical route raw sent a reader who asked for `/fr/<container>`
281
281
  * to the DEFAULT-locale page — a language switch they never asked for, from a
282
- * URL that was correct. The runtime's copy of this redirect (PageRenderer.jsx)
283
- * had the identical bug and was fixed first; this is its twin, and the two must
284
- * agree or a container behaves differently on a cold load than on an in-app
285
- * navigation.
282
+ * URL that was correct. The in-app redirect had the identical bug and was fixed
283
+ * first. ⭐ Since 2026-09-30 that one is `resolveRoute`'s (`@uniweb/core/resolve-route`,
284
+ * `localeUrl`); this is the static twin of its destination — the same prefix and
285
+ * translated slug, with the deployment base this pass writes under — and the two
286
+ * must agree or a container behaves differently on a cold load than in the app.
286
287
  *
287
288
  * The DECISION to redirect stays canonical (`target !== page.route`) — only the
288
289
  * destination is localized. Comparing a localized destination against a
@@ -1024,9 +1025,10 @@ export async function prerenderSite(siteDir, options = {}) {
1024
1025
  continue
1025
1026
  }
1026
1027
 
1027
- // Content-less containers: auto-redirect to first descendant with content.
1028
- // Mirrors the runtime's auto-redirect in PageRenderer.jsx so the redirect
1029
- // works without JS (via <meta http-equiv="refresh">).
1028
+ // Content-less containers: auto-redirect to first descendant with content —
1029
+ // `getNavigableRoute()`, which is `resolveRoute`'s landing rule
1030
+ // (`@uniweb/core/resolve-route`), so the stub goes where the app would.
1031
+ // A stub so the redirect works without JS (via <meta http-equiv="refresh">).
1030
1032
  if (!page.hasContent()) {
1031
1033
  const target = page.getNavigableRoute()
1032
1034
  if (target && target !== page.route) {
@@ -112,8 +112,8 @@ export function isContentBodyField(field) {
112
112
  * @param {(ref: string) => string} [opts.resolveOptions] - maps an `options`
113
113
  * (item_ref) ref to its full `@org/model/<section>` path. Falls back to
114
114
  * `resolveName` (model only) when not supplied.
115
- * @returns {Object} the declaration (`{ name, label?, description?, source_locale?,
116
- * linkable?, sections }`).
115
+ * @returns {Object} the declaration (`{ name, label?, plural?, description?,
116
+ * source_locale?, linkable?, sections }`).
117
117
  */
118
118
  export function toDataSchemaDeclaration(normalized, { name, resolveName, resolveOptions } = {}) {
119
119
  if (!name) throw new Error('toDataSchemaDeclaration: a registry name is required')
@@ -136,19 +136,23 @@ export function toDataSchemaDeclaration(normalized, { name, resolveName, resolve
136
136
 
137
137
  const decl = { name }
138
138
  if (normalized.label) decl.label = normalized.label
139
+ if (normalized.plural) decl.plural = normalized.plural
139
140
  if (normalized.description) decl.description = normalized.description
140
141
  if (normalized.sourceLocale) decl.source_locale = normalized.sourceLocale
141
142
  // ⛔ No `creatable_by`: retired 2026-09-23 — Models are open, and `@uniweb/schemas`
142
143
  // refuses the key. (It was emitted here when authored.)
143
144
 
144
145
  // `linkable` — AUTHORED ONLY (2026-09-27 [Diego]). A model is linkable unless it says
145
- // `linkable: false` (omitted ⇒ true), with or without a brief: only `linkable: false`
146
- // keeps its entries out of references and out of a site's folder. A reference to a
147
- // model with no brief carries no card, and that is not a reason to refuse one.
146
+ // `linkable: false` (omitted ⇒ true), with or without a brief. `linkable: false` keeps
147
+ // its entries out of references and out of a site's folder, and nothing else keeps them
148
+ // out of a folder. Whether a reference field may point at a model with no brief — no
149
+ // card to show — is the host's to decide, and nothing here derives it.
148
150
  //
149
151
  // ⛔ Until then a brief-less model was emitted `linkable: false` and `linkable: true`
150
152
  // on one was refused, on the reasoning that a reference would have no card to show —
151
153
  // which kept its records out of every site's folder.
154
+ // ⛔ And until 2026-09-30 this said a missing card "is not a reason to refuse" a
155
+ // reference, which read as a promise that one is accepted. A backend refuses it.
152
156
  if (normalized.linkable === false) decl.linkable = false
153
157
 
154
158
  decl.sections = sections
@@ -839,7 +839,7 @@ function writePagesTree(pages, pagesDir, sourceLocale, report, ctx, routePrefix
839
839
  // ⚖️ A pull adds and changes these, and never removes one: a page pushed by a CLI from before carries
840
840
  // none, and reading that as "none" would delete what the author wrote.
841
841
  function pageSlugToYml(record, canon, existing, sourceLocale, ctx) {
842
- if (record.is_dynamic) return null
842
+ if (record.is_dynamic) return dynamicOwnSlug(record, existing, sourceLocale)
843
843
  const targets = localizedTargets(record.slug, sourceLocale)
844
844
  const own = isPlainRecord(existing?.slug)
845
845
  if (!own && ctx.routes?.siteMap && !record.is_index) {
@@ -851,6 +851,20 @@ function pageSlugToYml(record, canon, existing, sourceLocale, ctx) {
851
851
  return own && stableJson(next) === stableJson(existing.slug) ? null : next
852
852
  }
853
853
 
854
+ // A parametric page's folder is named by its param (`pageDirName`), so its OWN slug — where a page
855
+ // made in an app gives it one of its own (`slug: detail`, `param_name: id`) — has no other place on
856
+ // disk. It is kept in `page.yml` as a plain string, which the build never reads for a route (only a
857
+ // per-language map names URL segments) and the push sends back as the page's slug. A string written
858
+ // here before is rewritten to the stored slug, so it is never left stale.
859
+ // ⛔ Until 2026-09-30 a pull dropped it, and the next push sent the param in its place: an authored
860
+ // value lost on a round trip (`uwx-format.md` § THE ROUND-TRIP LAW).
861
+ function dynamicOwnSlug(record, existing, sourceLocale) {
862
+ const own = unwrapLocalized(record.slug, sourceLocale)
863
+ if (typeof own !== 'string' || !own || own === '...path') return null
864
+ if (own !== (record.param_name || own)) return own
865
+ return typeof existing?.slug === 'string' ? own : null
866
+ }
867
+
854
868
  // The locales of a pulled localized `slug` other than the source one.
855
869
  function localizedTargets(slug, sourceLocale) {
856
870
  if (!isPlainRecord(slug)) return {}
package/src/uwx/site.js CHANGED
@@ -224,7 +224,15 @@ function localizeRouteSlugs(pages, siteYml, sourceLocale) {
224
224
  export function buildRouteOf(record, parentRoute, sourceLocale) {
225
225
  if (record?.is_index) return parentRoute
226
226
  const slug = unwrapLocalized(record?.slug, sourceLocale)
227
- const segment = record?.is_dynamic ? (slug === CATCH_ALL_MARKER ? ':path*' : `:${slug}`) : slug
227
+ // A parametric page's segment is its PARAM — the name the pull gives its folder, `[param_name]`
228
+ // (`site-project.js::pageDirName`), which is the route the build then reads. A page pushed from
229
+ // files has one word for both; a page made in an app may not (`slug: detail`, `param_name: id`).
230
+ // ⛔ Until 2026-09-30 this used the slug, so such a page was addressed `/…/:detail` while its
231
+ // folder built `/…/:id`, and its translated title and description were filed under a route no
232
+ // build reads.
233
+ const segment = record?.is_dynamic
234
+ ? (slug === CATCH_ALL_MARKER ? ':path*' : `:${record.param_name || slug}`)
235
+ : slug
228
236
  return parentRoute === '/' ? `/${segment}` : `${parentRoute}/${segment}`
229
237
  }
230
238
 
@@ -783,6 +791,12 @@ async function walkPagesNested(ctx, dirPath, parentSlugPath, inheritedMode, pare
783
791
  // A page's own localized URL segments, for `localizeRouteSlugs` — under a symbol, so they
784
792
  // are never serialized as a field of the record.
785
793
  if (!dyn && isPlainRecord(f.config?.slug)) record[AUTHORED_SLUG] = f.config.slug
794
+ // A parametric page's OWN slug, where it is not its param — a page made in an app — is kept in
795
+ // its page.yml by the pull as a plain string (`site-project.js::dynamicOwnSlug`), since its folder
796
+ // is named by its param. The route, the folder and the page's identity stay the param's.
797
+ if (dyn && dyn[1] !== CATCH_ALL_MARKER && typeof f.config?.slug === 'string' && f.config.slug) {
798
+ record.slug = { ...record.slug, [sourceLocale]: f.config.slug }
799
+ }
786
800
 
787
801
  if (mode === 'page') {
788
802
  const sections = await collectPageSectionsNested(f.path, siteRoot, f.config)