@uniweb/build 0.53.0 → 0.54.0
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 +4 -4
- package/src/site/content-collector.js +81 -20
- package/src/uwx/site-diff.js +3 -2
- package/src/uwx/site-dirs.js +32 -0
- package/src/uwx/site-project.js +20 -8
- package/src/uwx/site.js +84 -20
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.54.0",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -58,11 +58,11 @@
|
|
|
58
58
|
"sharp": "^0.35.3",
|
|
59
59
|
"yaml": "^2.5.0",
|
|
60
60
|
"@uniweb/content-reader": "^1.2.5",
|
|
61
|
+
"@uniweb/semantic-parser": "^1.4.1",
|
|
61
62
|
"@uniweb/schemas": "^0.3.3",
|
|
62
|
-
"@uniweb/theming": "^0.1.15",
|
|
63
63
|
"@uniweb/content-writer": "^0.3.4",
|
|
64
|
-
"@uniweb/
|
|
65
|
-
"@uniweb/
|
|
64
|
+
"@uniweb/theming": "^0.1.15",
|
|
65
|
+
"@uniweb/projections": "^0.7.3"
|
|
66
66
|
},
|
|
67
67
|
"optionalDependencies": {
|
|
68
68
|
"@uniweb/runtime": "^0.26.5"
|
|
@@ -1714,14 +1714,18 @@ async function processPage(pagePath, pageName, siteRoot, { isIndex = false, pare
|
|
|
1714
1714
|
}
|
|
1715
1715
|
|
|
1716
1716
|
/**
|
|
1717
|
-
*
|
|
1717
|
+
* The homepage ONE config declares: the first named `pages:` entry, else `index:`.
|
|
1718
|
+
* `undefined` when it declares none; `null` when its first `pages:` entry names
|
|
1719
|
+
* nothing usable, which has always meant "no homepage" rather than "fall through".
|
|
1718
1720
|
*
|
|
1719
|
-
*
|
|
1720
|
-
*
|
|
1721
|
-
*
|
|
1721
|
+
* The within-file rule, in one place — a level's config and each of the root's two
|
|
1722
|
+
* configs (`rootOrderConfig`) are read by it alike.
|
|
1723
|
+
*
|
|
1724
|
+
* @param {Object} [config] - `{ pages, index }` as authored
|
|
1725
|
+
* @returns {string|null|undefined}
|
|
1722
1726
|
*/
|
|
1723
|
-
function
|
|
1724
|
-
const { pages: pagesArray, index: indexName } =
|
|
1727
|
+
function declaredHomepage(config) {
|
|
1728
|
+
const { pages: pagesArray, index: indexName } = config || {}
|
|
1725
1729
|
|
|
1726
1730
|
// 1. Explicit pages array - first non-'...' item is index
|
|
1727
1731
|
if (Array.isArray(pagesArray) && pagesArray.length > 0) {
|
|
@@ -1733,9 +1737,69 @@ function determineIndexPage(orderConfig, availableFolders) {
|
|
|
1733
1737
|
}
|
|
1734
1738
|
|
|
1735
1739
|
// 2. Explicit index property
|
|
1736
|
-
|
|
1737
|
-
|
|
1740
|
+
return indexName || undefined
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
/**
|
|
1744
|
+
* The homepage a level's order config names. The root's was already decided across
|
|
1745
|
+
* its two configs, and arrives as `homepage` (`rootOrderConfig`); any other level
|
|
1746
|
+
* that promotes one — a folder promoted to `/`, the latest version of a scope at the
|
|
1747
|
+
* root — names it in its own config.
|
|
1748
|
+
*/
|
|
1749
|
+
function homepageOf(orderConfig) {
|
|
1750
|
+
return orderConfig && 'homepage' in orderConfig ? orderConfig.homepage : declaredHomepage(orderConfig)
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
/**
|
|
1754
|
+
* The site's TOP LEVEL — its order and its homepage — from the two configs that can
|
|
1755
|
+
* declare it: the site config (`site.yml`, or a document profile's own file) and the
|
|
1756
|
+
* pages directory's own config (`pages/folder.yml`, or `pages/page.yml`).
|
|
1757
|
+
*
|
|
1758
|
+
* ⭐ The pages directory's config is read as a MOUNTED directory's is: the content's
|
|
1759
|
+
* own declaration, under the site's. Every other directory's config orders its
|
|
1760
|
+
* children, and a mounted one's does too — but the root's was read for its mode
|
|
1761
|
+
* alone until 2026-09-19, so a content repository that orders itself kept its order
|
|
1762
|
+
* mounted below the root and lost it as the whole pages directory: alphabetical,
|
|
1763
|
+
* and in folder mode with no homepage at all.
|
|
1764
|
+
*
|
|
1765
|
+
* ⛔ THE HOMEPAGE IS DECIDED PER CONFIG, NOT PER KEY. Within one file the first
|
|
1766
|
+
* `pages:` entry beats `index:`, as it always has; across the two, whichever form the
|
|
1767
|
+
* site config uses beats the pages directory's. Merged key by key, a `pages:` list
|
|
1768
|
+
* from the pages directory would override a `site.yml` `index:`. The order is the
|
|
1769
|
+
* site config's `pages:`, else the pages directory's.
|
|
1770
|
+
*
|
|
1771
|
+
* ⚖️ `pages:` and `index:` ONLY. The root has no container page, so the keys that
|
|
1772
|
+
* describe one (`title`, `seo`, …) mean nothing here — a repository carries them for
|
|
1773
|
+
* when it is mounted below the root. And `layout:` stays the site config's alone: a
|
|
1774
|
+
* site's layout travels on the sync lane as that file's own value, verbatim, so a
|
|
1775
|
+
* second source would render on a static build and nowhere else.
|
|
1776
|
+
*
|
|
1777
|
+
* @param {{ pages?: Array, index?: string }} [site] - the site config's own order keys
|
|
1778
|
+
* @param {Object} [root] - the pages directory's config, as `readFolderConfig` read it
|
|
1779
|
+
* @returns {{ pages: Array|undefined, index: string|undefined, homepage: string|null|undefined }}
|
|
1780
|
+
*/
|
|
1781
|
+
export function rootOrderConfig(site = {}, root = {}) {
|
|
1782
|
+
const listed = (value) => Array.isArray(value) && value.length > 0
|
|
1783
|
+
const fromSite = declaredHomepage(site)
|
|
1784
|
+
const homepage = fromSite !== undefined ? fromSite : declaredHomepage(root)
|
|
1785
|
+
return {
|
|
1786
|
+
pages: listed(site?.pages) ? site.pages : listed(root?.pages) ? root.pages : undefined,
|
|
1787
|
+
index: homepage || undefined,
|
|
1788
|
+
homepage,
|
|
1738
1789
|
}
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
/**
|
|
1793
|
+
* Determine the index page name from ordering config
|
|
1794
|
+
*
|
|
1795
|
+
* @param {Object} orderConfig - { pages: [...], index: 'name' } from parent
|
|
1796
|
+
* @param {Array} availableFolders - Array of { name, order } for folders at this level
|
|
1797
|
+
* @returns {string|null} The folder name that should be the index, or null
|
|
1798
|
+
*/
|
|
1799
|
+
function determineIndexPage(orderConfig, availableFolders) {
|
|
1800
|
+
// 1–2. The first named `pages:` entry, else `index:` (`declaredHomepage`).
|
|
1801
|
+
const declared = homepageOf(orderConfig)
|
|
1802
|
+
if (declared !== undefined) return declared
|
|
1739
1803
|
|
|
1740
1804
|
// 3. Fallback: lowest order value, or first alphabetically
|
|
1741
1805
|
// IMPORTANT: Dynamic route folders (e.g., [slug]) can never be index pages
|
|
@@ -1953,11 +2017,7 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
1953
2017
|
// pages: controls order only and every folder keeps its natural route.
|
|
1954
2018
|
let indexName = null
|
|
1955
2019
|
if (parentRoute === '/') {
|
|
1956
|
-
|
|
1957
|
-
indexName = extractItemName(pagesParsedFM.before[0])
|
|
1958
|
-
} else {
|
|
1959
|
-
indexName = orderConfig?.index || null
|
|
1960
|
-
}
|
|
2020
|
+
indexName = homepageOf(orderConfig) ?? null
|
|
1961
2021
|
}
|
|
1962
2022
|
|
|
1963
2023
|
// Add md-file-pages
|
|
@@ -2505,21 +2565,22 @@ export async function collectSiteContent(sitePath, options = {}) {
|
|
|
2505
2565
|
}
|
|
2506
2566
|
}
|
|
2507
2567
|
|
|
2568
|
+
// Root content mode default comes from the profile (folder mode for
|
|
2569
|
+
// documents, page mode for sites). A folder.yml/page.yml in the pages root
|
|
2570
|
+
// overrides the profile default per readFolderConfig.
|
|
2571
|
+
const { mode: rootContentMode, config: rootFolderConfig } = await readFolderConfig(pagesPath, profile.defaultMode)
|
|
2572
|
+
|
|
2508
2573
|
// Extract page ordering config from the top-level config.
|
|
2509
2574
|
// Document profile reads the `content:` field (with `pages:` as a back-compat
|
|
2510
2575
|
// alias); site profile reads `pages:`. Either way the internal field name is
|
|
2511
2576
|
// `pages` so collectPagesRecursive doesn't need to care about profile.
|
|
2512
|
-
|
|
2577
|
+
// ⭐ Layered over the pages directory's own `pages:` / `index:` — see `rootOrderConfig`.
|
|
2578
|
+
const siteOrderConfig = rootOrderConfig({
|
|
2513
2579
|
pages: profile.orderField === 'pages'
|
|
2514
2580
|
? siteConfig.pages
|
|
2515
2581
|
: (siteConfig[profile.orderField] ?? siteConfig.pages),
|
|
2516
2582
|
index: siteConfig.index
|
|
2517
|
-
}
|
|
2518
|
-
|
|
2519
|
-
// Root content mode default comes from the profile (folder mode for
|
|
2520
|
-
// documents, page mode for sites). A folder.yml/page.yml in the pages root
|
|
2521
|
-
// overrides the profile default per readFolderConfig.
|
|
2522
|
-
const { mode: rootContentMode } = await readFolderConfig(pagesPath, profile.defaultMode)
|
|
2583
|
+
}, rootFolderConfig)
|
|
2523
2584
|
|
|
2524
2585
|
// Collect layout areas from the layout/ directory, read from the site alone
|
|
2525
2586
|
const { layouts, assetCollection: layoutAssets, iconCollection: layoutIcons } =
|
package/src/uwx/site-diff.js
CHANGED
|
@@ -64,8 +64,9 @@ const ownContent = (record, ...childKeys) => {
|
|
|
64
64
|
* the projector would write it to.
|
|
65
65
|
*
|
|
66
66
|
* Paths use the conventional `pages/` and `layout/` roots. A site that relocates
|
|
67
|
-
* them via `
|
|
68
|
-
* would differ from disk, which is a labelling
|
|
67
|
+
* them via `site.yml::paths` (`settings.paths` on the wire) still gets stable, unique
|
|
68
|
+
* keys — only the displayed prefix would differ from disk, which is a labelling
|
|
69
|
+
* nicety, not a correctness issue.
|
|
69
70
|
*
|
|
70
71
|
* @returns {Map<string, object>} path → the unit's own record
|
|
71
72
|
*/
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Where a site's pages and layout live — ONE definition for the push, which reads
|
|
2
|
+
// the tree from here (`site.js`), and the pull, which writes it back here
|
|
3
|
+
// (`site-project.js`). A leaf module so the pull does not import the push.
|
|
4
|
+
|
|
5
|
+
import { join, resolve } from 'node:path'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* `site.yml::paths.pages` / `paths.layout`, else `pages/` and `layout/` — resolved
|
|
9
|
+
* as the build resolves them (`site/content-collector.js`).
|
|
10
|
+
*
|
|
11
|
+
* ⛔ The pull read this from the pulled document's `info.paths` until 2026-09-19.
|
|
12
|
+
* `paths` moved to the `settings` Section on 2026-09-09 and that reader did not
|
|
13
|
+
* move with it, so it read as absent: every pull wrote pages to `pages/` and layout
|
|
14
|
+
* to `layout/` whatever the site declared, while the build and the next push kept
|
|
15
|
+
* reading the declared directory — untouched by the pull, and recorded by it as
|
|
16
|
+
* current.
|
|
17
|
+
*
|
|
18
|
+
* ⭐ Reading `site.yml` rather than the document is the fix, not a detour: it is the
|
|
19
|
+
* file both readers use, and the pull writes the document's `settings.paths` into it
|
|
20
|
+
* before it places a single page.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} siteRoot
|
|
23
|
+
* @param {object|null} siteYml - the parsed `site.yml`
|
|
24
|
+
* @returns {{ pagesDir: string, layoutDir: string }}
|
|
25
|
+
*/
|
|
26
|
+
export function siteContentDirs(siteRoot, siteYml) {
|
|
27
|
+
const paths = siteYml?.paths
|
|
28
|
+
return {
|
|
29
|
+
pagesDir: paths?.pages ? resolve(siteRoot, paths.pages) : join(siteRoot, 'pages'),
|
|
30
|
+
layoutDir: paths?.layout ? resolve(siteRoot, paths.layout) : join(siteRoot, 'layout'),
|
|
31
|
+
}
|
|
32
|
+
}
|
package/src/uwx/site-project.js
CHANGED
|
@@ -46,6 +46,7 @@ import { createTranslationCollector, writeLocaleTranslations, writeFreeformTrans
|
|
|
46
46
|
import { buildFreeformPath } from '../i18n/freeform.js'
|
|
47
47
|
import { unwrapLocalized, unwrapLocalizedList } from './backfill.js'
|
|
48
48
|
import { LOCALIZED_FIELD_ASSUMPTION } from './localize.js'
|
|
49
|
+
import { siteContentDirs } from './site-dirs.js'
|
|
49
50
|
|
|
50
51
|
// The pull-side identity index: a per-clone, GITIGNORED `uuid → relative path`
|
|
51
52
|
// map under `.uniweb/`, the home for the backend's per-item identity so that
|
|
@@ -607,24 +608,35 @@ function pageRecordToYml(record, sectionsArray, sourceLocale, existing = null) {
|
|
|
607
608
|
return y
|
|
608
609
|
}
|
|
609
610
|
|
|
610
|
-
//
|
|
611
|
+
// ⛔ A PRUNE MAY ONLY DELETE WHAT A PUSH COULD HAVE SENT. An orphan is a file the
|
|
612
|
+
// incoming document no longer has — but a push reads the tree with the build's own
|
|
613
|
+
// rules (`isMarkdownFile`, `isIgnoredFolder`), so a `_draft.md`, a `README.md`, a
|
|
614
|
+
// `_partials/` folder or a `.git/` directory never reaches the backend and can never
|
|
615
|
+
// come back in a pulled document. Judged by "not in the incoming set" alone, every
|
|
616
|
+
// one of them was an orphan and a default pull DELETED it — the `.git/` of a pages
|
|
617
|
+
// directory that is its own clone included. The same two rules decide both sides
|
|
618
|
+
// now, which is also what the layout prune below already did.
|
|
619
|
+
|
|
620
|
+
// Delete the section files in `pageDir` whose stableId isn't in `keep`.
|
|
611
621
|
function pruneOrphanSectionFiles(pageDir, keep, report) {
|
|
612
622
|
if (!existsSync(pageDir)) return
|
|
613
623
|
for (const entry of readdirSync(pageDir)) {
|
|
614
|
-
if (
|
|
624
|
+
if (!isMarkdownFile(entry)) continue
|
|
615
625
|
if (keep.has(basename(entry, extname(entry)))) continue
|
|
616
626
|
const p = join(pageDir, entry)
|
|
627
|
+
if (!statSync(p).isFile()) continue
|
|
617
628
|
unlinkSync(p)
|
|
618
629
|
report.deleted.push(p)
|
|
619
630
|
}
|
|
620
631
|
}
|
|
621
632
|
|
|
622
|
-
// Delete
|
|
633
|
+
// Delete the page directories in `pagesDir` whose name isn't an incoming page dir.
|
|
623
634
|
function pruneOrphanPageDirs(pagesDir, keepDirs, report) {
|
|
624
635
|
if (!existsSync(pagesDir)) return
|
|
625
636
|
for (const entry of readdirSync(pagesDir)) {
|
|
637
|
+
if (isIgnoredFolder(entry) || keepDirs.has(entry)) continue
|
|
626
638
|
const p = join(pagesDir, entry)
|
|
627
|
-
if (!statSync(p).isDirectory()
|
|
639
|
+
if (!statSync(p).isDirectory()) continue
|
|
628
640
|
rmSync(p, { recursive: true, force: true })
|
|
629
641
|
report.deleted.push(p)
|
|
630
642
|
}
|
|
@@ -870,11 +882,11 @@ export function siteContentDocumentToProject({ document, siteRoot, sourceLocale
|
|
|
870
882
|
// localized scalars during the page walk.
|
|
871
883
|
const ctx = { siteRoot, oldIndex: readPullIndex(siteRoot), newIndex: {}, report, collector, sourceLocale }
|
|
872
884
|
|
|
873
|
-
|
|
874
|
-
|
|
885
|
+
// Where the tree goes is what `site.yml` says NOW — `siteInfoToConfig` above has
|
|
886
|
+
// written the document's `settings.paths` into it — because that is the file the
|
|
887
|
+
// next push and the build read. See `siteContentDirs`.
|
|
888
|
+
const { pagesDir, layoutDir: layoutBaseDir } = siteContentDirs(siteRoot, readAuthoredYaml(join(siteRoot, 'site.yml')))
|
|
875
889
|
projectPages(document?.pages, pagesDir, sourceLocale, report, prune, ctx)
|
|
876
|
-
|
|
877
|
-
const layoutBaseDir = paths.layout ? join(siteRoot, paths.layout) : join(siteRoot, 'layout')
|
|
878
890
|
projectLayout(document?.layout_sections, layoutBaseDir, report, prune, ctx)
|
|
879
891
|
|
|
880
892
|
report.locales = writeLocaleTranslations(siteRoot, collector.byLocale)
|
package/src/uwx/site.js
CHANGED
|
@@ -57,6 +57,7 @@ import {
|
|
|
57
57
|
fetchFromQueryShorthand,
|
|
58
58
|
assertRouteFolder,
|
|
59
59
|
mountEntriesOf,
|
|
60
|
+
rootOrderConfig,
|
|
60
61
|
} from '../site/content-collector.js'
|
|
61
62
|
import { refuseBinding, refuseUnder, refuseOutsideLanguage, warnDuplicateBindings } from '../site/data-fetcher.js'
|
|
62
63
|
import { readLayoutFolder } from '../site/layout-folder.js'
|
|
@@ -69,6 +70,7 @@ import { loadFreeformTranslation } from '../i18n/freeform.js'
|
|
|
69
70
|
import { upsertYamlScalar } from './yaml-upsert.js'
|
|
70
71
|
import { resolveQueriesConfig } from './queries-config.js'
|
|
71
72
|
import { resolveSelfScope } from './self-scope.js'
|
|
73
|
+
import { siteContentDirs } from './site-dirs.js'
|
|
72
74
|
|
|
73
75
|
const SITE_ENTITY_KEY = 'site-content' // one content entity per site project
|
|
74
76
|
|
|
@@ -545,8 +547,7 @@ async function collectPageSectionsNested(pageDir, siteRoot, pageConfig) {
|
|
|
545
547
|
}
|
|
546
548
|
|
|
547
549
|
// Top-level sections: every NON-`@` markdown file, in order. `@`-prefixed files
|
|
548
|
-
// are children, pulled in via their parent's `nest:` above
|
|
549
|
-
// file with no parent is simply omitted — it stays out of the document).
|
|
550
|
+
// are children, pulled in via their parent's `nest:` above.
|
|
550
551
|
const seen = new Set()
|
|
551
552
|
const sections = []
|
|
552
553
|
for (const file of mdFiles) {
|
|
@@ -555,13 +556,33 @@ async function collectPageSectionsNested(pageDir, siteRoot, pageConfig) {
|
|
|
555
556
|
sections.push(await buildSection(file, seen))
|
|
556
557
|
}
|
|
557
558
|
|
|
559
|
+
// An `@` file no `nest:` claimed is an ORPHAN, and the build keeps it — appended
|
|
560
|
+
// as a top-level section, with a warning, "to avoid silent data loss" (`processPage`,
|
|
561
|
+
// `site/content-collector.js`). Mirrored exactly: at the end, in file order, with no
|
|
562
|
+
// children of its own. ⛔ This lane omitted it until 2026-09-19, and a pull of the
|
|
563
|
+
// result then DELETED the file — on disk and absent from the document, it read as
|
|
564
|
+
// something removed upstream.
|
|
565
|
+
for (const file of mdFiles) {
|
|
566
|
+
if (!isChildSection(file) || seen.has(file)) continue
|
|
567
|
+
seen.add(file)
|
|
568
|
+
const stableDefault = parseNumericPrefix(stripAtPrefix(parse(file).name)).name
|
|
569
|
+
const { section } = await processMarkdownFile(join(pageDir, file), String(seen.size), siteRoot, stableDefault)
|
|
570
|
+
section.subsections = []
|
|
571
|
+
sections.push(section)
|
|
572
|
+
}
|
|
573
|
+
|
|
558
574
|
return sections.map((s, i) => sectionToRecord(s, i))
|
|
559
575
|
}
|
|
560
576
|
|
|
561
577
|
// A folder-mode folder's `.md` files are pages to the build and nothing to this
|
|
562
578
|
// lane. Name every one of them, so the shortfall is visible at push time rather
|
|
563
579
|
// than by reading the backend's store or opening the published site.
|
|
564
|
-
|
|
580
|
+
//
|
|
581
|
+
// `source` is `readFolderConfig`'s: a folder with no config of its own is in folder
|
|
582
|
+
// mode because a `folder.yml` above it put it there, and the message says so —
|
|
583
|
+
// "it is a folder-mode folder (`folder.yml`)" over a folder that has none would send
|
|
584
|
+
// the author looking for a file that does not exist.
|
|
585
|
+
async function warnFolderModePagesSkipped(dirPath, siteRoot, source = 'folder.yml') {
|
|
565
586
|
let files
|
|
566
587
|
try {
|
|
567
588
|
files = (await readdir(dirPath)).filter(isMarkdownFile)
|
|
@@ -570,18 +591,33 @@ async function warnFolderModePagesSkipped(dirPath, siteRoot) {
|
|
|
570
591
|
}
|
|
571
592
|
if (files.length === 0) return
|
|
572
593
|
const where = relative(siteRoot, dirPath)
|
|
594
|
+
const why = source === 'inherited'
|
|
595
|
+
? 'it is a folder-mode folder (it has no `folder.yml` or `page.yml` of its own, so it inherits folder mode from the `folder.yml` above it)'
|
|
596
|
+
: 'it is a folder-mode folder (`folder.yml`)'
|
|
573
597
|
console.warn(
|
|
574
598
|
`uwx/site: ${files.length} page(s) in \`${where}\` will NOT be pushed — ` +
|
|
575
|
-
|
|
599
|
+
`${why}, and pushing \`.md\`-as-pages ` +
|
|
576
600
|
`is not implemented on the sync lane. A build renders them; the backend ` +
|
|
577
601
|
`will not have them.\n` +
|
|
578
602
|
files.map((f) => ` - ${where}/${f}`).join('\n') +
|
|
579
603
|
`\n To publish them, make each one a page-mode directory ` +
|
|
580
604
|
`(\`<name>/page.yml\` + its section files). To ship them as they are, ` +
|
|
581
|
-
`use \`uniweb export\` or \`uniweb deploy --host <adapter>\`.`
|
|
605
|
+
`use \`uniweb export\` or \`uniweb deploy --host <adapter>\`.` +
|
|
606
|
+
REPUBLISH_CLAUSE
|
|
582
607
|
)
|
|
583
608
|
}
|
|
584
609
|
|
|
610
|
+
// A push sends the page tree as ONE entity (see site-diff.js), so a republish does
|
|
611
|
+
// not merge: the shorter document becomes the whole tree. Measured by the backend
|
|
612
|
+
// lane 2026-09-18 against a current `uniwebd` — 49 stored pages replaced by 2, the
|
|
613
|
+
// publish reporting success. Both skip warnings carry this, because "will not be
|
|
614
|
+
// pushed" reads as "you publish short" when the real cost on an already-published
|
|
615
|
+
// site is that live pages are REMOVED.
|
|
616
|
+
const REPUBLISH_CLAUSE =
|
|
617
|
+
`\n ⚠ If this site has been published before, a push REPLACES its whole page ` +
|
|
618
|
+
`tree — pages already live that are missing here are REMOVED, and the publish ` +
|
|
619
|
+
`still reports success.`
|
|
620
|
+
|
|
585
621
|
// Recursively build the `pages` tree: each record carries its fields, its inline
|
|
586
622
|
// `page_sections` (page mode only), and its child pages under `$children`.
|
|
587
623
|
async function walkPagesNested(ctx, dirPath, parentSlugPath, inheritedMode, parentConfig, isRoot) {
|
|
@@ -597,7 +633,16 @@ async function walkPagesNested(ctx, dirPath, parentSlugPath, inheritedMode, pare
|
|
|
597
633
|
assertRouteFolder(f.dirName, insideCatchAll ? '/:path*' : '/')
|
|
598
634
|
const dyn = f.dirName.match(DYNAMIC_RE)
|
|
599
635
|
const slug = dyn ? dyn[1] : f.name
|
|
600
|
-
|
|
636
|
+
// ⭐ THE BUILD'S ANSWER, NOT A SECOND ONE. `readFolderConfig` has already resolved
|
|
637
|
+
// this folder's mode — a `folder.yml` here, a `page.yml` here, or neither and
|
|
638
|
+
// INHERITED from its parent — and that resolved mode is exactly what the build
|
|
639
|
+
// reads (`site/content-collector.js`). ⛔ This line tested the config file's NAME
|
|
640
|
+
// until 2026-09-19, so a folder with no config under a folder-mode parent — the
|
|
641
|
+
// documented cascade, one `folder.yml` at the top of a docs tree — was pushed as
|
|
642
|
+
// ONE page whose sections were its `.md` files while the build rendered each as a
|
|
643
|
+
// page of its own. Nothing warned, and a pull then wrote a `page.yml` into the
|
|
644
|
+
// folder, converting the author's own site to the wrong shape.
|
|
645
|
+
const mode = f.internalMode === 'pages' ? 'folder' : 'page'
|
|
601
646
|
const slugPath = parentSlugPath ? `${parentSlugPath}/${slug}` : slug
|
|
602
647
|
|
|
603
648
|
const data = buildPageData(f.config, {
|
|
@@ -632,7 +677,7 @@ async function walkPagesNested(ctx, dirPath, parentSlugPath, inheritedMode, pare
|
|
|
632
677
|
// measured 2026-09-18, a 49-page documentation site published 2 pages with
|
|
633
678
|
// every CLI instrument reporting success [measured:backend, channel with
|
|
634
679
|
// backend, and reproduced here against the same site].
|
|
635
|
-
await warnFolderModePagesSkipped(f.path, siteRoot)
|
|
680
|
+
await warnFolderModePagesSkipped(f.path, siteRoot, f.source)
|
|
636
681
|
}
|
|
637
682
|
|
|
638
683
|
const children = await walkPagesNested(ctx, f.path, slugPath, f.internalMode, f.config, false)
|
|
@@ -1384,7 +1429,8 @@ export async function siteProjectToDocument(siteRoot, opts = {}) {
|
|
|
1384
1429
|
// left in an old site.yml is inert — nothing reads it, so nothing sends it.
|
|
1385
1430
|
// (uwx-format.md → info.url.)
|
|
1386
1431
|
|
|
1387
|
-
|
|
1432
|
+
// `siteIndex` — the homepage — joins below, once the pages directory's own config is read.
|
|
1433
|
+
const ctx = { siteRoot, sourceLocale, translations }
|
|
1388
1434
|
// ⛔ ONE `paths:` CONVENTION, TWO READERS, AND THIS ONE IS A STRICT SUBSET.
|
|
1389
1435
|
// The build honours sub-mounts — `paths: { pages/<segment>: <dir> }`, through
|
|
1390
1436
|
// `resolveMounts`/`mountEntriesOf` in `site/content-collector.js` — and this
|
|
@@ -1394,25 +1440,43 @@ export async function siteProjectToDocument(siteRoot, opts = {}) {
|
|
|
1394
1440
|
// a site whose entire corpus was one sub-mount: 49 pages built, 2 pushed.
|
|
1395
1441
|
// Deferred by design for now (bidirectional-sync.md); warned about here so it
|
|
1396
1442
|
// is not also silent.
|
|
1443
|
+
//
|
|
1444
|
+
// The same two directories the pull writes back to — see `siteContentDirs`.
|
|
1445
|
+
const { pagesDir: pagesPath, layoutDir } = siteContentDirs(siteRoot, siteYml)
|
|
1397
1446
|
for (const [segment] of mountEntriesOf(siteYml.paths)) {
|
|
1447
|
+
// Only a LOCAL stub (`pages/<segment>/`, the folder that gives a mounted route its
|
|
1448
|
+
// layout) is walked, so the route arrives as an empty container; with no stub,
|
|
1449
|
+
// nothing here stands for the route and it is not sent at all. ⚠️ The message
|
|
1450
|
+
// said "empty container" in both cases until 2026-09-19.
|
|
1451
|
+
const fate = existsSync(join(pagesPath, segment))
|
|
1452
|
+
? 'will be pushed as an empty container'
|
|
1453
|
+
: 'will not be pushed at all'
|
|
1398
1454
|
console.warn(
|
|
1399
1455
|
`uwx/site: \`paths: { pages/${segment}: … }\` is NOT read on the sync lane — ` +
|
|
1400
|
-
`\`${segment}\`
|
|
1456
|
+
`\`${segment}\` ${fate} and none of the mounted ` +
|
|
1401
1457
|
`pages will reach the backend. A build mounts it normally. Inline the ` +
|
|
1402
1458
|
`directory under \`pages/${segment}/\` to push it, or ship the site with ` +
|
|
1403
|
-
`\`uniweb export\` / \`uniweb deploy --host <adapter>\`.`
|
|
1459
|
+
`\`uniweb export\` / \`uniweb deploy --host <adapter>\`.` +
|
|
1460
|
+
REPUBLISH_CLAUSE
|
|
1404
1461
|
)
|
|
1405
1462
|
}
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1463
|
+
let pages = []
|
|
1464
|
+
if (existsSync(pagesPath)) {
|
|
1465
|
+
// The root has a mode too, read as the build reads it (`rootContentMode`):
|
|
1466
|
+
// `pages/folder.yml` puts the whole site in folder mode, and every folder with no
|
|
1467
|
+
// config of its own inherits it. ⛔ This walk started in page mode unconditionally
|
|
1468
|
+
// until 2026-09-19, so such a site's top-level `.md` pages were dropped without a
|
|
1469
|
+
// word and its top-level folders were pushed as pages of sections.
|
|
1470
|
+
const { mode: rootMode, source: rootSource, config: rootConfig } = await readFolderConfig(pagesPath, 'sections')
|
|
1471
|
+
if (rootMode === 'pages') await warnFolderModePagesSkipped(pagesPath, siteRoot, rootSource)
|
|
1472
|
+
// The top level's order and homepage, decided by the build's own function: the
|
|
1473
|
+
// site's `pages:` / `index:` over the pages directory's own. ⛔ The homepage read
|
|
1474
|
+
// `index:` alone until 2026-09-19, so a site naming it by its first `pages:` entry
|
|
1475
|
+
// — as the build does — pushed no homepage at all.
|
|
1476
|
+
const top = rootOrderConfig({ pages: siteYml.pages, index: siteYml.index }, rootConfig)
|
|
1477
|
+
pages = await walkPagesNested({ ...ctx, siteIndex: top.homepage }, pagesPath, '', rootMode, { pages: top.pages }, true)
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1416
1480
|
const layoutSections = await collectLayoutNested(layoutDir, siteRoot)
|
|
1417
1481
|
|
|
1418
1482
|
// Wrap each section's content into its per-locale form (source doc + target
|