@uniweb/build 0.80.0 → 0.80.1

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.80.0",
3
+ "version": "0.80.1",
4
4
  "description": "Build tooling for the Uniweb Component Web Platform",
5
5
  "type": "module",
6
6
  "exports": {
@@ -57,11 +57,11 @@
57
57
  "js-yaml": "^4.1.0",
58
58
  "sharp": "^0.35.3",
59
59
  "yaml": "^2.5.0",
60
+ "@uniweb/content-reader": "^1.2.8",
60
61
  "@uniweb/semantic-parser": "^1.5.1",
61
62
  "@uniweb/projections": "^0.7.15",
62
- "@uniweb/content-writer": "^0.3.4",
63
63
  "@uniweb/schemas": "^0.13.0",
64
- "@uniweb/content-reader": "^1.2.8",
64
+ "@uniweb/content-writer": "^0.3.4",
65
65
  "@uniweb/theming": "^0.2.0"
66
66
  },
67
67
  "optionalDependencies": {
@@ -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/runtime": "^0.29.4",
78
- "@uniweb/core": "^0.38.0"
77
+ "@uniweb/core": "^0.38.0",
78
+ "@uniweb/runtime": "^0.29.4"
79
79
  },
80
80
  "peerDependenciesMeta": {
81
81
  "vite": {
@@ -78,8 +78,10 @@ export const QUERIES_YML_RELPATH = 'queries.yml'
78
78
  // Identity has no boundary to get wrong, and an author wanting a different schema
79
79
  // name writes `schema:`, which is one line and says what it means.
80
80
  //
81
- // Exported so the inverse (projection) can drop a `schema:` that merely restates
82
- // this default, keeping a projected queries.yml as terse as the author left it.
81
+ // ⛔ A pull does not drop a `schema:` equal to this default: it writes the schema the site holds,
82
+ // in its one spelling (`uwx/records-project.js::declToFileShape`). Until 2026-10-08 it did, keeping
83
+ // a projected queries.yml as terse as the author left it — and a clone, with no author's file to
84
+ // look at, wrote `series: { schema: '@/series' }` back as `series: {}`.
83
85
  export function defaultSchema(name) {
84
86
  return `@/${name}`
85
87
  }
@@ -4,7 +4,6 @@
4
4
  export {
5
5
  resolveQueriesConfig,
6
6
  queriesYmlPath,
7
- defaultSchema,
8
7
  foundationDataSchemas,
9
8
  foundationSchemaJson,
10
9
  QUERIES_YML_RELPATH,
@@ -41,7 +41,7 @@ import yaml from 'js-yaml'
41
41
  import { YAML_OPTIONS } from '../utils/yaml-schema.js'
42
42
  import { parseFrontmatter } from './entity-source.js'
43
43
  import { writeRecordFile, writeQueriesConfig, writeRecordsConfig } from './project-writer.js'
44
- import { defaultSchema, foundationDataSchemas, foundationSchemaJson, QUERIES_YML_RELPATH } from './queries-config.js'
44
+ import { foundationDataSchemas, foundationSchemaJson, QUERIES_YML_RELPATH } from './queries-config.js'
45
45
  import { dataKeyTypes } from './data-key-types.js'
46
46
  import { poolDirsForSchema, schemaForPoolDirs, resolveRecordsDir } from '../site/entity-pool.js'
47
47
  import { folderYmlPath } from '../site/records-config.js'
@@ -277,8 +277,8 @@ function setIf(obj, key, value) {
277
277
  // - `path:` is written VERBATIM, and omitted entirely when it equals the default
278
278
  // (the query's own name under the pool).
279
279
  // - `url:` (remote source) and a bare `source:` object are carried as-is.
280
- // - `schema:` is dropped when it only restates the query-name convention default,
281
- // so a terse author file stays terse.
280
+ // - `schema:` is written as the site holds it — the query-name default included —
281
+ // except a query typed by a data key, kept short where its type can be read again.
282
282
  //
283
283
  // ⛔ THE `collections/`-PREFIX STRIP AND THE site.yml ROUTING ARE BOTH GONE, and
284
284
  // they went together. They existed because `collections.yml` sat INSIDE
@@ -342,24 +342,31 @@ function declToFileShape(wire, scope = null, own = null, keyTypes = null, author
342
342
  decl.source = source
343
343
  }
344
344
 
345
- // The default is the query's name — or, when no data schema has that name, the type the
346
- // foundation declares for the data key of that name (`data-key-types.js`), which is what a
347
- // push sent in its place. Either one stays unwritten, so the terse file stays terse.
348
- // ⛔ UNLESS THE AUTHOR WROTE IT: a `schema:` the local file already states for this query is
349
- // written back as it is. Until 2026-09-25 a pull into a working copy dropped international's
350
- // `articles: { schema: '@std/article' }`, because a section types `articles` as `@std/article`.
351
- // ⭐ A CLONE GOES BY THE MARK. Where the key's type comes only from the registered foundation a
352
- // project keeps (`markedOnly`: its foundation is a catalog ref), a query is written terse only when
353
- // the push marked it `typed_by_data_key` — the author wrote no schema — or when the author's own
354
- // file declares it with none. Unmarked, the schema was the author's, and it is written.
345
+ // ⭐ THE SCHEMA THE SITE HOLDS IS WRITTEN — one spelling per meaning [Diego, 2026-10-08]. A
346
+ // query left to its name's default (`series` → `@/series`) comes back with that schema stated:
347
+ // `series: {}` and `series: { schema: '@/series' }` read the same records, and a clone, with no
348
+ // file of the author's to look at, could not tell which was written. ⛔ Until 2026-10-08 a schema
349
+ // equal to the default stayed unwritten unless the local file stated it, so a clone turned an
350
+ // author's `series: { schema: '@/series' }` into `series: {}`.
351
+ //
352
+ // ⛔ EXCEPT A QUERY TYPED BY A DATA KEY, which is not a second spelling of anything: `team:` reads
353
+ // its own folder, `records/team/`, as the type the foundation's data key declares
354
+ // (`data-key-types.js`), where `team: { schema: '@/member' }` would read `records/member/`. It
355
+ // stays as written — short — but only where this project can read it as that type again: the
356
+ // foundation's types (its own, or the registered version a clone keeps) give its name the schema
357
+ // the site holds. Then the push's mark says so (`typed_by_data_key`), or the author's file
358
+ // declares it short, or — the foundation in this project — the file does not declare it. An
359
+ // author's stated schema is never taken for one. ⛔ With no types to read, short would mean
360
+ // `@/team`: the schema is written, and the records go to its folder, so the copy still pushes it.
355
361
  const written = name ? authored?.get(name) : undefined
356
- const typedHere = !markedOnly || wire.typed_by_data_key === true || written === null
357
- const keyType = name && !own?.has(name) && typedHere ? keyTypes?.get(name) : null
362
+ const keyType = name && !own?.has(name) ? keyTypes?.get(name) : null
358
363
  // In the author's form: the registered version names the type qualified (`@acme/member`).
359
364
  const keyDefault = keyType ? unresolveSelfScope(keyType, scope, own) : null
360
- if (d.schema && (d.schema === written || (d.schema !== defaultSchema(name) && d.schema !== keyDefault))) {
361
- decl.schema = d.schema
362
- }
365
+ const typedByKey =
366
+ keyDefault !== null &&
367
+ d.schema === keyDefault &&
368
+ (wire.typed_by_data_key === true || written === null || (written === undefined && !markedOnly))
369
+ if (d.schema && !typedByKey) decl.schema = d.schema
363
370
  setIf(decl, 'sort', d.sort)
364
371
  setIf(decl, 'where', d.where)
365
372
  setIf(decl, 'limit', d.limit)
@@ -47,8 +47,7 @@ import { declarationsToQueriesYml } from './records-project.js'
47
47
  import { FILLED_SECTION_TYPE, buildRouteOf } from './site.js'
48
48
  import { translationContext, SITE_META_CONTEXT } from '../i18n/extract.js'
49
49
  import { authorableDeclaration, DECLARATION_KEYS } from '../site/fetch-shapes.js'
50
- import { createTranslationCollector, writeLocaleTranslations, writeFreeformTranslations, unwrapLocalizedContent, localesDir } from './locale-sync.js'
51
- import { resolveLocaleList } from '../i18n/locales.js'
50
+ import { createTranslationCollector, writeLocaleTranslations, writeFreeformTranslations, unwrapLocalizedContent } from './locale-sync.js'
52
51
  import { buildFreeformPath, freeformPathsFor } from '../i18n/freeform.js'
53
52
  import { unwrapLocalized, unwrapLocalizedList } from './backfill.js'
54
53
  import { LOCALIZED_FIELD_ASSUMPTION } from './localize.js'
@@ -214,7 +213,7 @@ function readAuthoredYaml(filePath) {
214
213
  * @returns {{ siteConfig: string, theme?: string, headHtml?: string }} per-file
215
214
  * write status ('updated' | 'unchanged')
216
215
  */
217
- export function siteInfoToConfig({ document, siteRoot, backend = null, sourceLocale = LOCALIZED_FIELD_ASSUMPTION.defaultSourceLocale, collector, keepAuthoredFoundation = false, deferLanguages = false }) {
216
+ export function siteInfoToConfig({ document, siteRoot, backend = null, sourceLocale = LOCALIZED_FIELD_ASSUMPTION.defaultSourceLocale, collector, keepAuthoredFoundation = false }) {
218
217
  const info = document?.info || {}
219
218
  const settingsSection = document?.settings || {}
220
219
 
@@ -325,18 +324,14 @@ export function siteInfoToConfig({ document, siteRoot, backend = null, sourceLoc
325
324
  }
326
325
  }
327
326
 
328
- // ⭐ `languages` for a site whose `site.yml` declares none is judged once the pull has written the
329
- // translation files (`deferLanguages`, `siteContentDocumentToProject`): a push sends the list those
330
- // files make (`uwx/site.js::settingsNested`), and writing it back would turn "every translation
331
- // file" into a fixed list.
332
- let pendingLanguages
333
- if (deferLanguages && siteChanges.languages !== undefined && !('languages' in (readAuthoredYaml(join(siteRoot, 'site.yml')) || {}))) {
334
- pendingLanguages = siteChanges.languages
335
- delete siteChanges.languages
336
- }
337
-
327
+ // ⭐ `languages` IS WRITTEN AS THE SITE HOLDS IT — one spelling per meaning [Diego, 2026-10-08]:
328
+ // "It's definitely good for the case of languages." A site whose `site.yml` declared none sent the
329
+ // list its translation files make (`uwx/site.js::settingsNested`), and that list comes back stated,
330
+ // so a clone and the copy that pushed it say the same thing. ⛔ Until 2026-10-08 the list was held
331
+ // back where the translation files the pull wrote made the same one, so a clone of a site that
332
+ // declared `languages: [en, fr]` had no `languages:` at all — and followed its translation files
333
+ // where the site it came from had a fixed list.
338
334
  const result = { siteConfig: writeSiteConfig(siteRoot, siteChanges) }
339
- if (pendingLanguages !== undefined) result.pendingLanguages = pendingLanguages
340
335
 
341
336
  // theme (whole object) → theme.yml.
342
337
  if (settingsSection.theme && typeof settingsSection.theme === 'object') {
@@ -1121,7 +1116,7 @@ export function siteContentDocumentToProject({ document, siteRoot, backend = nul
1121
1116
  backend ? readBackendState(siteRoot, backend).assets || {} : {}
1122
1117
  )
1123
1118
 
1124
- report.config = siteInfoToConfig({ document, siteRoot, backend, sourceLocale, collector, keepAuthoredFoundation, deferLanguages: true })
1119
+ report.config = siteInfoToConfig({ document, siteRoot, backend, sourceLocale, collector, keepAuthoredFoundation })
1125
1120
  report.queries = declarationsToQueriesYml({ document, siteRoot, scope })
1126
1121
 
1127
1122
  // The uuid identity index (gitignored `.uniweb/`): read the prior map to anchor
@@ -1162,18 +1157,6 @@ export function siteContentDocumentToProject({ document, siteRoot, backend = nul
1162
1157
  report.locales = writeLocaleTranslations(siteRoot, collector.byLocale)
1163
1158
  // Target-locale FREE-FORM bodies → locales/freeform/{locale}/<relpath> + manifest.
1164
1159
  report.freeform = writeFreeformTranslations(siteRoot, collector.freeformPending)
1165
- // The site's languages, for a site.yml that declares none (`siteInfoToConfig`) — written only where
1166
- // the translation files, now written, would not make the same list, or where `publishLanguages`
1167
- // needs a declared list to draw from: the build refuses one without it.
1168
- if (report.config?.pendingLanguages !== undefined) {
1169
- const pulled = report.config.pendingLanguages
1170
- delete report.config.pendingLanguages
1171
- const site = readAuthoredYaml(join(siteRoot, 'site.yml')) || {}
1172
- const source = site.defaultLanguage || sourceLocale
1173
- const made = new Set([source, ...resolveLocaleList(undefined, localesDir(siteRoot))])
1174
- const same = Array.isArray(pulled) && pulled.length === made.size && pulled.every((l) => made.has(l))
1175
- if (!same || site.publishLanguages !== undefined) writeSiteConfig(siteRoot, { languages: pulled })
1176
- }
1177
1160
  writePullIndex(siteRoot, ctx.newIndex)
1178
1161
  return report
1179
1162
  }
@@ -45,9 +45,18 @@ function columnOf(text, key) {
45
45
  return /^ *$/.test(lead) ? lead.length : null
46
46
  }
47
47
 
48
- /** Where a pair's text ends: after its value, its trailing comment and its newline. */
48
+ /**
49
+ * Where a pair's text ends: after its value, its trailing comment and its newline.
50
+ *
51
+ * ⛔ A SCALAR ENDS WHERE ITS OWN TEXT DOES — `range[1]`, then the rest of that line. Its
52
+ * `range[2]` also spans the comment lines that follow it, and for an empty value (`team:`)
53
+ * every comment and blank line up to the next key, at any indentation: those belong to what
54
+ * comes next. ⛔ Until 2026-10-08 this read `range[2]`, and a pull that gave a bare `team:` its
55
+ * schema deleted the blank line and the `# Build options` heading under it in `site.yml`.
56
+ */
49
57
  function pairEnd(text, pair) {
50
- let end = (pair.value ?? pair.key).range[2]
58
+ const node = pair.value ?? pair.key
59
+ let end = isScalar(node) ? node.range[1] : node.range[2]
51
60
  if (text[end - 1] !== '\n') {
52
61
  const nl = text.indexOf('\n', end)
53
62
  const rest = nl === -1 ? text.slice(end) : text.slice(end, nl)
@@ -169,6 +178,17 @@ function editMap(text, map, before, after, edits, dump) {
169
178
  continue
170
179
  }
171
180
  }
181
+ // ⭐ A BARE KEY GIVEN A VALUE KEEPS ITS LINE — and any comment the author wrote on it — and
182
+ // the value goes under it: `team: # the people` gains ` schema: '@/team'` beneath.
183
+ const bare = isScalar(value) && value.value === null && value.range[0] === value.range[1]
184
+ if (bare && (isPlainObject(next) || Array.isArray(next))) {
185
+ const whole = indented(dump({ [key]: next }), col, false)
186
+ const nl = whole.indexOf('\n')
187
+ if (nl !== -1 && nl < whole.length - 1) {
188
+ edits.push({ start: stop, end: stop, text: (text[stop - 1] === '\n' ? '' : '\n') + whole.slice(nl + 1) })
189
+ continue
190
+ }
191
+ }
172
192
  // Written whole — in flow style when the author wrote it that way (`tags: [a, b]`).
173
193
  const flow = (isMap(value) || isSeq(value)) && value.flow
174
194
  edits.push({