@uniweb/build 1.3.7 → 1.3.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.3.
|
|
3
|
+
"version": "1.3.8",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -56,12 +56,12 @@
|
|
|
56
56
|
"js-yaml": "^4.1.0",
|
|
57
57
|
"sharp": "^0.35.3",
|
|
58
58
|
"yaml": "^2.5.0",
|
|
59
|
-
"@uniweb/content-writer": "~1.1.2",
|
|
60
59
|
"@uniweb/content-reader": "~1.4.0",
|
|
60
|
+
"@uniweb/content-writer": "~1.1.2",
|
|
61
61
|
"@uniweb/projections": "~1.1.1",
|
|
62
|
+
"@uniweb/schemas": "~1.3.1",
|
|
62
63
|
"@uniweb/semantic-parser": "~1.8.1",
|
|
63
|
-
"@uniweb/theming": "~1.2.0"
|
|
64
|
-
"@uniweb/schemas": "~1.3.1"
|
|
64
|
+
"@uniweb/theming": "~1.2.0"
|
|
65
65
|
},
|
|
66
66
|
"optionalDependencies": {
|
|
67
67
|
"@uniweb/runtime": "~1.1.4"
|
|
@@ -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/
|
|
77
|
-
"@uniweb/
|
|
76
|
+
"@uniweb/core": "~1.2.2",
|
|
77
|
+
"@uniweb/runtime": "~1.1.4"
|
|
78
78
|
},
|
|
79
79
|
"peerDependenciesMeta": {
|
|
80
80
|
"vite": {
|
package/src/site/entity-pool.js
CHANGED
|
@@ -53,8 +53,7 @@
|
|
|
53
53
|
// there is no population to carry): a leftover `entities/` beside `records/` would
|
|
54
54
|
// otherwise hold records nothing reads, silently.
|
|
55
55
|
|
|
56
|
-
import {
|
|
57
|
-
import { existsSync, readFileSync, statSync } from 'node:fs'
|
|
56
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
|
58
57
|
import { join, extname, basename, resolve } from 'node:path'
|
|
59
58
|
import yaml from 'js-yaml'
|
|
60
59
|
import { YAML_OPTIONS } from '../utils/yaml-schema.js'
|
|
@@ -263,6 +262,17 @@ export function poolPathReadings(dirs) {
|
|
|
263
262
|
* leaves the backend's folder alone for a site with no records directory.
|
|
264
263
|
*/
|
|
265
264
|
export async function readEntityPool(siteRoot, opts = {}) {
|
|
265
|
+
return readEntityPoolSync(siteRoot, opts)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* `readEntityPool`, for a caller that cannot wait — the pull's projection, which asks what a
|
|
270
|
+
* `folder.yml` already says (`records-project.js`). One walk, which the async form returns.
|
|
271
|
+
*
|
|
272
|
+
* @param {string} siteRoot
|
|
273
|
+
* @param {object} [opts] - as `readEntityPool`'s
|
|
274
|
+
*/
|
|
275
|
+
export function readEntityPoolSync(siteRoot, opts = {}) {
|
|
266
276
|
let rel
|
|
267
277
|
if (opts.dir) {
|
|
268
278
|
rel = String(opts.dir).replace(/\/+$/, '')
|
|
@@ -280,10 +290,10 @@ export async function readEntityPool(siteRoot, opts = {}) {
|
|
|
280
290
|
// A file record is named by its stem, so two files of one stem cannot both be one.
|
|
281
291
|
const fileStems = new Map()
|
|
282
292
|
|
|
283
|
-
const walk =
|
|
293
|
+
const walk = (dir, dirs) => {
|
|
284
294
|
let listing
|
|
285
295
|
try {
|
|
286
|
-
listing =
|
|
296
|
+
listing = readdirSync(dir, { withFileTypes: true })
|
|
287
297
|
} catch {
|
|
288
298
|
return
|
|
289
299
|
}
|
|
@@ -304,7 +314,7 @@ export async function readEntityPool(siteRoot, opts = {}) {
|
|
|
304
314
|
)
|
|
305
315
|
continue
|
|
306
316
|
}
|
|
307
|
-
|
|
317
|
+
walk(full, [...dirs, e.name])
|
|
308
318
|
continue
|
|
309
319
|
}
|
|
310
320
|
if (!e.isFile()) continue
|
|
@@ -356,7 +366,7 @@ export async function readEntityPool(siteRoot, opts = {}) {
|
|
|
356
366
|
}
|
|
357
367
|
}
|
|
358
368
|
|
|
359
|
-
|
|
369
|
+
walk(base, [])
|
|
360
370
|
return { entities, errors, exists: true, dir: rel }
|
|
361
371
|
}
|
|
362
372
|
|
package/src/uwx/backfill.js
CHANGED
|
@@ -296,7 +296,13 @@ export function renderEntityDocument({ document, declaration, format, sourceLoca
|
|
|
296
296
|
if (document?.$disabled === true) record.draft = true
|
|
297
297
|
|
|
298
298
|
if (format === 'json') return JSON.stringify(record, null, 2) + '\n'
|
|
299
|
-
if (format === 'md')
|
|
299
|
+
if (format === 'md') {
|
|
300
|
+
// ⭐ Written as a section is (`project-writer.js::assembleSection`): a blank line after the front
|
|
301
|
+
// matter, and a final newline. ⛔ Until 2026-10-17 it had neither, so a pull that rewrote an
|
|
302
|
+
// edited record took both out of the author's file.
|
|
303
|
+
const body = String(dec.body ?? '').replace(/^(?:[ \t]*\n)+/, '').replace(/\s+$/, '')
|
|
304
|
+
return `---\n${yaml.dump(record)}---\n${body ? `\n${body}\n` : ''}`
|
|
305
|
+
}
|
|
300
306
|
return yaml.dump(record) // yaml / yaml
|
|
301
307
|
}
|
|
302
308
|
|
|
@@ -418,14 +418,16 @@ function restates(authored, decl) {
|
|
|
418
418
|
* (`records/`, or wherever `site.yml::paths.records` puts it), as a LIST.
|
|
419
419
|
*
|
|
420
420
|
* ⛔ A FULL WRITE, NOT A MERGE, and that is the one place this differs from every
|
|
421
|
-
* other projected config
|
|
422
|
-
*
|
|
423
|
-
* There is also nothing a shallow merge could mean here — the file is a sequence,
|
|
424
|
-
* and merging two lists either duplicates entries or silently drops them.
|
|
421
|
+
* other projected config: the file is a sequence, and merging two lists either
|
|
422
|
+
* duplicates entries or silently drops them.
|
|
425
423
|
*
|
|
426
|
-
* The caller decides whether there is anything to write: a
|
|
427
|
-
*
|
|
428
|
-
*
|
|
424
|
+
* The caller decides whether there is anything to write: a file that already says
|
|
425
|
+
* what the pulled folder says — its patterns included — is left alone
|
|
426
|
+
* (`records-project.js::organizesAlike`), and a folder with no sub-folders is no file
|
|
427
|
+
* at all (`folderToFolderYml` removes it), since missing and empty mean the same.
|
|
428
|
+
* ⛔ *Until 2026-10-17 this called the file a mirror — "concrete refs on both sides,
|
|
429
|
+
* nothing to invert" — and every pull wrote the folder's paths over a developer's
|
|
430
|
+
* patterns.* ⚠️ *Until 2026-09-21 this said an empty file was destructive
|
|
429
431
|
* on the next push; that stopped being true when the directory, not this file,
|
|
430
432
|
* became what decides which records the folder holds.*
|
|
431
433
|
*
|
|
@@ -43,8 +43,9 @@ import { parseFrontmatter } from './entity-source.js'
|
|
|
43
43
|
import { writeRecordFile, writeQueriesConfig, writeRecordsConfig } from './project-writer.js'
|
|
44
44
|
import { foundationDataSchemas, foundationSchemaJson, QUERIES_YML_RELPATH } from './queries-config.js'
|
|
45
45
|
import { dataKeyTypes } from './data-key-types.js'
|
|
46
|
-
import { poolDirsForSchema, schemaForPoolDirs, resolveRecordsDir } from '../site/entity-pool.js'
|
|
47
|
-
import { folderYmlPath } from '../site/records-config.js'
|
|
46
|
+
import { poolDirsForSchema, schemaForPoolDirs, resolveRecordsDir, readEntityPoolSync } from '../site/entity-pool.js'
|
|
47
|
+
import { folderYmlPath, resolveFolder } from '../site/records-config.js'
|
|
48
|
+
import { canonicalJson } from './same-content.js'
|
|
48
49
|
import { contentBodyTarget } from './record-layout.js'
|
|
49
50
|
import { qualifyRef } from '@uniweb/schemas/refs'
|
|
50
51
|
import { unresolveSelfScope, ownSchemaNames, refuseOrgOption } from './self-scope.js'
|
|
@@ -501,9 +502,14 @@ export function declarationsToQueriesYml({ document, siteRoot, scope, ...rest })
|
|
|
501
502
|
* Project a pulled `@uniweb/folder` document back to `folder.yml` in the records
|
|
502
503
|
* directory (`records/folder.yml`, or under `site.yml::paths.records`).
|
|
503
504
|
*
|
|
504
|
-
* ⭐ THE FOLDER
|
|
505
|
-
* `
|
|
506
|
-
*
|
|
505
|
+
* ⭐ A FILE THAT SAYS WHAT THE PULLED FOLDER SAYS IS LEFT AS WRITTEN — its patterns
|
|
506
|
+
* (`member/*.md`), its comments, its order (`organizesAlike`). Otherwise the folder
|
|
507
|
+
* is written as the paths the backend's folder places, which is what it says.
|
|
508
|
+
* ⛔ *This read "the folder round-trips trivially: `folder.yml` holds concrete paths
|
|
509
|
+
* on both sides, so there is nothing to invert" until 2026-10-17 — read as "the file
|
|
510
|
+
* holds no patterns", which it does — and every pull with a folder wrote its paths
|
|
511
|
+
* over the developer's patterns: a record added afterwards fell outside them.* The
|
|
512
|
+
* old shape put QUERY MACROS in the folder — a virtual `folders:` tree naming
|
|
507
513
|
* collections — and inverting a macro is not possible in general. Taking queries
|
|
508
514
|
* out of the folder is what dissolved that.
|
|
509
515
|
*
|
|
@@ -600,6 +606,16 @@ export function folderToFolderYml({ folderDoc, siteRoot, poolPathByUuid, sourceL
|
|
|
600
606
|
|
|
601
607
|
const entries = walk(folderDoc.contents, false)
|
|
602
608
|
if (unplaceable) return { status: 'skipped', entries: [], warnings, file }
|
|
609
|
+
if (existsSync(abs) && organizesAlike({ abs, entries, siteRoot, poolPathByUuid })) {
|
|
610
|
+
return { status: 'unchanged', entries, warnings, file }
|
|
611
|
+
}
|
|
612
|
+
// Said when it replaces a developer's pattern: a record they add from here is placed by nothing.
|
|
613
|
+
if (existsSync(abs) && holdsPattern(abs)) {
|
|
614
|
+
warnings.push(
|
|
615
|
+
`${file}: the folder no longer places its records as its patterns say, so it was written as their ` +
|
|
616
|
+
`paths. A record you add from here sits at the top of the folder until ${file} places it.`
|
|
617
|
+
)
|
|
618
|
+
}
|
|
603
619
|
if (entries.length === 0) {
|
|
604
620
|
if (!existsSync(abs)) return { status: 'unchanged', entries, warnings, file }
|
|
605
621
|
unlinkSync(abs)
|
|
@@ -608,6 +624,67 @@ export function folderToFolderYml({ folderDoc, siteRoot, poolPathByUuid, sourceL
|
|
|
608
624
|
return { status: writeRecordsConfig(siteRoot, entries), entries, warnings, file }
|
|
609
625
|
}
|
|
610
626
|
|
|
627
|
+
/**
|
|
628
|
+
* Does the `folder.yml` on disk say what the pulled folder says — read by the rule a push reads it
|
|
629
|
+
* by (`resolveFolder`), over the records directory as this pull left it?
|
|
630
|
+
*
|
|
631
|
+
* ⭐ About the records the backend's folder holds, every word: the folders, by name, label and tags,
|
|
632
|
+
* in order; and each record's folder, its tags and label, and its place among the others. Records
|
|
633
|
+
* the backend does not hold yet — a file the developer added and has not pushed — are theirs, so a
|
|
634
|
+
* pattern placing one is not a difference; nor is a folder holding only those. So `member/*.md` is
|
|
635
|
+
* kept while it places exactly the members the backend's folder has there, and goes on placing the
|
|
636
|
+
* next one the developer adds.
|
|
637
|
+
*
|
|
638
|
+
* @returns {boolean}
|
|
639
|
+
*/
|
|
640
|
+
function organizesAlike({ abs, entries, siteRoot, poolPathByUuid }) {
|
|
641
|
+
let authored
|
|
642
|
+
try {
|
|
643
|
+
authored = yaml.load(readFileSync(abs, 'utf8'), YAML_OPTIONS)
|
|
644
|
+
} catch {
|
|
645
|
+
return false
|
|
646
|
+
}
|
|
647
|
+
if (!Array.isArray(authored)) return false
|
|
648
|
+
const pool = readEntityPoolSync(siteRoot).entities
|
|
649
|
+
const pulled = new Set(poolPathByUuid.values())
|
|
650
|
+
const held = new Set(pool.filter((e) => pulled.has(e.poolPath)).map((e) => e.id))
|
|
651
|
+
const mine = resolveFolder(authored, pool)
|
|
652
|
+
const theirs = resolveFolder(entries, pool)
|
|
653
|
+
if (mine.errors.length || theirs.errors.length) return false
|
|
654
|
+
const theirFolders = new Set()
|
|
655
|
+
const said = (n) => ({ ...(n.label != null ? { label: n.label } : {}), ...(n.tags ? { tags: n.tags } : {}) })
|
|
656
|
+
// A tree as it reads for the held records. `keep(path, holds)` decides whether a folder stays.
|
|
657
|
+
const shape = (nodes, at, keep) =>
|
|
658
|
+
(nodes || []).flatMap((n) => {
|
|
659
|
+
if (n.kind === 'ref') return held.has(n.$entityId) ? [{ record: n.$entityId, ...said(n) }] : []
|
|
660
|
+
if (n.kind !== 'branch') return []
|
|
661
|
+
const path = [...at, n.name]
|
|
662
|
+
const records = shape(n.$children, path, keep)
|
|
663
|
+
return keep(path.join('/'), records.length > 0) ? [{ folder: n.name, ...said(n), records }] : []
|
|
664
|
+
})
|
|
665
|
+
// Every folder the backend has stays, empty or not; one of the file's stays when it holds a held
|
|
666
|
+
// record or the backend has it too — else it is the developer's, not pushed yet.
|
|
667
|
+
const theirShape = shape(theirs.nodes, [], (path) => (theirFolders.add(path), true))
|
|
668
|
+
const myShape = shape(mine.nodes, [], (path, holds) => holds || theirFolders.has(path))
|
|
669
|
+
return canonicalJson(myShape) === canonicalJson(theirShape)
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Does the `folder.yml` on disk place records by a pattern (`member/*.md`) anywhere?
|
|
673
|
+
function holdsPattern(abs) {
|
|
674
|
+
let authored
|
|
675
|
+
try {
|
|
676
|
+
authored = yaml.load(readFileSync(abs, 'utf8'), YAML_OPTIONS)
|
|
677
|
+
} catch {
|
|
678
|
+
return false
|
|
679
|
+
}
|
|
680
|
+
const has = (entries) =>
|
|
681
|
+
(Array.isArray(entries) ? entries : []).some((entry) => {
|
|
682
|
+
const path = typeof entry === 'string' ? entry : entry?.path
|
|
683
|
+
return (typeof path === 'string' && /[*?]/.test(path)) || has(entry?.records)
|
|
684
|
+
})
|
|
685
|
+
return has(authored)
|
|
686
|
+
}
|
|
687
|
+
|
|
611
688
|
/**
|
|
612
689
|
* Project a pulled folder + its record entities to `records/**` files.
|
|
613
690
|
*
|