@uniweb/build 1.10.6 → 1.10.8

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": "1.10.6",
3
+ "version": "1.10.8",
4
4
  "description": "Build tooling for the Uniweb Component Web Platform",
5
5
  "type": "module",
6
6
  "exports": {
@@ -57,14 +57,14 @@
57
57
  "sharp": "^0.35.5",
58
58
  "yaml": "^2.5.0",
59
59
  "@uniweb/content-reader": "~1.5.7",
60
+ "@uniweb/content-writer": "~1.3.1",
60
61
  "@uniweb/schemas": "~1.6.5",
61
62
  "@uniweb/semantic-parser": "~1.13.0",
62
- "@uniweb/theming": "~1.2.1",
63
- "@uniweb/projections": "~1.2.2",
64
- "@uniweb/content-writer": "~1.3.1"
63
+ "@uniweb/projections": "~1.2.3",
64
+ "@uniweb/theming": "~1.2.1"
65
65
  },
66
66
  "optionalDependencies": {
67
- "@uniweb/runtime": "~1.2.4"
67
+ "@uniweb/runtime": "~1.2.5"
68
68
  },
69
69
  "peerDependencies": {
70
70
  "vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
@@ -73,8 +73,8 @@
73
73
  "@tailwindcss/vite": "^4.0.0",
74
74
  "@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
75
75
  "vite-plugin-svgr": "^4.0.0",
76
- "@uniweb/core": "~1.4.8",
77
- "@uniweb/runtime": "~1.2.4"
76
+ "@uniweb/core": "~1.4.9",
77
+ "@uniweb/runtime": "~1.2.5"
78
78
  },
79
79
  "peerDependenciesMeta": {
80
80
  "vite": {
@@ -55,6 +55,7 @@ export { nodeEnvDefine }
55
55
  * @param {boolean} [options.tailwind] - Include Tailwind CSS v4 plugin (default: true)
56
56
  * @param {boolean} [options.sourcemap] - Generate sourcemaps (default: true)
57
57
  * @param {Object} [options.build] - Additional build options to merge
58
+ * @param {Object} [options.experimental] - Vite experimental options to merge
58
59
  * @returns {Promise<Object>} Vite configuration
59
60
  */
60
61
  export async function defineFoundationConfig(options = {}) {
@@ -76,6 +77,7 @@ export async function defineFoundationConfig(options = {}) {
76
77
  tailwind = true,
77
78
  sourcemap = true,
78
79
  build: buildOverrides = {},
80
+ experimental: experimentalOverrides = {},
79
81
  ...restOptions
80
82
  } = options
81
83
 
@@ -126,6 +128,15 @@ export async function defineFoundationConfig(options = {}) {
126
128
  return {
127
129
  plugins,
128
130
 
131
+ // ⭐ A FOUNDATION NAMES ITS OWN FILES FROM WHERE ITS CODE IS. A host serves it from an
132
+ // address of its own, never the page's, so a file its code references — a worker; library
133
+ // mode inlines every other asset — is found beside the code, not at the page's root.
134
+ // ⛔ Until 2026-10-27 that URL was `/assets/<file>`, which a published site does not have.
135
+ experimental: {
136
+ renderBuiltUrl: (filename, { hostType }) => (hostType === 'js' ? { relative: true } : undefined),
137
+ ...experimentalOverrides
138
+ },
139
+
129
140
  build: {
130
141
  lib: {
131
142
  entry: resolve(foundationRoot, entry),
package/src/i18n/index.js CHANGED
@@ -454,6 +454,14 @@ export async function buildLocalizedContent(siteRoot, options = {}) {
454
454
  await writeFile(siteContentPath, JSON.stringify(siteContent, null, 2))
455
455
  }
456
456
 
457
+ // The paths the site's source holds a section at, drafts included — read once, and only
458
+ // when a translation translates nothing this build publishes (`warnAboutFreeformIssues`).
459
+ let sourcePaths
460
+ const inSource = async (path) => {
461
+ sourcePaths ??= freeformSourceIndex(await collectSourceQuietly(siteRoot)).validPaths
462
+ return sourcePaths.has(path)
463
+ }
464
+
457
465
  for (const locale of locales) {
458
466
  // ⛔ THE DEFAULT LOCALE IS THE ROOT TREE, NEVER A SUBDIRECTORY. Emitting
459
467
  // `dist/<default>/` mints a byte-identical duplicate of every page at a second
@@ -496,7 +504,7 @@ export async function buildLocalizedContent(siteRoot, options = {}) {
496
504
  })
497
505
 
498
506
  // Check for stale/orphaned free-form translations and warn
499
- await warnAboutFreeformIssues(locale, freeformDir, siteContent)
507
+ await warnAboutFreeformIssues(locale, freeformDir, siteContent, inSource)
500
508
  } else {
501
509
  // Use sync merge (original behavior)
502
510
  translated = mergeTranslations(siteContent, translations, {
@@ -534,8 +542,10 @@ export async function buildLocalizedContent(siteRoot, options = {}) {
534
542
  * @param {string} locale - Locale code
535
543
  * @param {string} freeformDir - Path to locale's freeform directory
536
544
  * @param {Object} siteContent - Site content for building source hashes
545
+ * @param {(path: string) => Promise<boolean>} inSource - whether the site's source holds a
546
+ * section at a translation's path
537
547
  */
538
- async function warnAboutFreeformIssues(locale, freeformDir, siteContent) {
548
+ async function warnAboutFreeformIssues(locale, freeformDir, siteContent, inSource) {
539
549
  try {
540
550
  // Every path the renderer reads a section's translation from, for every section it
541
551
  // translates, with the hash of each source — the index the CLI's free-form commands
@@ -550,9 +560,14 @@ async function warnAboutFreeformIssues(locale, freeformDir, siteContent) {
550
560
 
551
561
  // Check for orphaned translations — only those this content can judge: never a
552
562
  // record's (`records/…`), which is read from records the site content does not hold.
563
+ // ⭐ AND ONLY ONE THE SITE'S SOURCE DOES NOT HOLD EITHER. A published build carries no
564
+ // drafts, so a translation of a `hidden:` section or page translates nothing here while
565
+ // its section is still in the site — the source says, as `uniweb i18n status --freeform`
566
+ // judges. ⛔ Until 2026-10-27 each such translation was reported orphaned at every build.
553
567
  const orphaned = await getOrphanedTranslations(freeformDir, validPaths)
554
568
  for (const item of orphaned) {
555
569
  if (!canJudge(item.path)) continue
570
+ if (await inSource(item.path)) continue
556
571
  console.warn(`[i18n] Free-form translation orphaned: ${locale}/${item.path}`)
557
572
  }
558
573
 
@@ -574,6 +589,23 @@ async function warnAboutFreeformIssues(locale, freeformDir, siteContent) {
574
589
  }
575
590
  }
576
591
 
592
+ /**
593
+ * The site's content as its source says it, drafts included — what `uniweb i18n` judges a
594
+ * free-form translation by. This build collected the site and reported what it found, so
595
+ * this second reading says nothing.
596
+ */
597
+ async function collectSourceQuietly(siteRoot) {
598
+ const { collectSiteContent } = await import('../site/content-collector.js')
599
+ const { log, warn } = console
600
+ console.log = console.warn = () => {}
601
+ try {
602
+ return await collectSiteContent(siteRoot)
603
+ } finally {
604
+ console.log = log
605
+ console.warn = warn
606
+ }
607
+ }
608
+
577
609
  /**
578
610
  * Format translation status for console output
579
611
  * @param {Object} status - Status from getTranslationStatus
@@ -225,11 +225,16 @@ export function backfillBibFile(filePath, uuidBySlug) {
225
225
 
226
226
  // Unwrap a localized wire value `{ <locale>: v }` back to the authored bare value
227
227
  // (the source locale, else the first present). Non-localized values pass through.
228
+ // ⛔ One with no locale at all, `{}`, is no value: undefined, and the field is not written.
229
+ // Until 2026-10-26 it passed through as the empty map, so a record's emptied caption was
230
+ // written `caption: {}` — an object where its schema says string, which `uniweb validate`
231
+ // refuses, and a publish stopped on fifteen of them.
228
232
  export function unwrapLocalized(value, sourceLocale) {
229
233
  if (value && typeof value === 'object' && !Array.isArray(value)) {
230
234
  if (Object.prototype.hasOwnProperty.call(value, sourceLocale)) return value[sourceLocale]
231
235
  const keys = Object.keys(value)
232
236
  if (keys.length) return value[keys[0]]
237
+ return undefined
233
238
  }
234
239
  return value
235
240
  }
@@ -238,7 +243,8 @@ export function unwrapLocalized(value, sourceLocale) {
238
243
  // field element-wise. An array unwraps each element; a bare localized map unwraps to
239
244
  // one value. The container shape is preserved (mirrors how the producer built it).
240
245
  export function unwrapLocalizedList(value, sourceLocale) {
241
- if (Array.isArray(value)) return value.map((v) => unwrapLocalized(v, sourceLocale))
246
+ // An element with no locale at all is no element (`unwrapLocalized`).
247
+ if (Array.isArray(value)) return value.map((v) => unwrapLocalized(v, sourceLocale)).filter((v) => v !== undefined)
242
248
  return unwrapLocalized(value, sourceLocale)
243
249
  }
244
250
 
@@ -347,7 +353,8 @@ function decodeRecord(fields, value, dec, sectionName) {
347
353
  const isBody = Boolean(dec.bodyAt) && sectionName === dec.bodyAt.section && key === dec.bodyAt.key
348
354
  const decoded = decodeLeaf(raw, field, dec, isBody)
349
355
  if (isBody) dec.body = typeof decoded === 'string' ? decoded : String(decoded ?? '')
350
- else out[key] = decoded
356
+ // A translatable value with no text in any language is no value: the field is not written.
357
+ else if (decoded !== undefined) out[key] = decoded
351
358
  }
352
359
  return out
353
360
  }