uniweb 0.56.2 → 0.56.4
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 +8 -7
- package/partials/agents.md +23 -1
- package/src/commands/docs.js +7 -3
- package/src/commands/doctor.js +239 -2
- package/src/commands/families.js +82 -0
- package/src/framework-index.json +7 -7
- package/src/index.js +35 -0
- package/starter/foundation/sections/Section/meta.js +0 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.56.
|
|
3
|
+
"version": "0.56.4",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,15 +41,16 @@
|
|
|
41
41
|
"js-yaml": "^4.1.0",
|
|
42
42
|
"prompts": "^2.4.2",
|
|
43
43
|
"tar": "^7.0.0",
|
|
44
|
-
"@uniweb/core": "^0.29.
|
|
45
|
-
"@uniweb/kit": "^0.19.
|
|
46
|
-
"@uniweb/
|
|
47
|
-
"@uniweb/runtime": "^0.26.
|
|
44
|
+
"@uniweb/core": "^0.29.4",
|
|
45
|
+
"@uniweb/kit": "^0.19.2",
|
|
46
|
+
"@uniweb/schemas": "^0.2.15",
|
|
47
|
+
"@uniweb/runtime": "^0.26.4",
|
|
48
|
+
"@uniweb/semantic-parser": "^1.4.0"
|
|
48
49
|
},
|
|
49
50
|
"peerDependencies": {
|
|
51
|
+
"@uniweb/build": "^0.52.3",
|
|
50
52
|
"@uniweb/content-reader": "^1.2.5",
|
|
51
|
-
"@uniweb/semantic-parser": "^1.4.0"
|
|
52
|
-
"@uniweb/build": "^0.52.1"
|
|
53
|
+
"@uniweb/semantic-parser": "^1.4.0"
|
|
53
54
|
},
|
|
54
55
|
"peerDependenciesMeta": {
|
|
55
56
|
"@uniweb/build": {
|
package/partials/agents.md
CHANGED
|
@@ -236,10 +236,32 @@ cat <foundation>/sections/Hero/meta.js # what one expects and accepts
|
|
|
236
236
|
|
|
237
237
|
> **A site-relative URL (`/effects/entry.js`) only works where the site serves its own files** — `uniweb export` and `uniweb deploy --host=<adapter>`. A site published to Uniweb hosting ships no JS, so nothing serves that path, and `uniweb publish` rejects it with a pointer to the catalog-ref form. Register the extension (`uniweb register` in its directory) and reference it like any other foundation. When you reference a workspace-local extension, `uniweb publish` brings it along exactly as it does the primary — releasing it if its code changed, and pinning the released version on the published site.
|
|
238
238
|
|
|
239
|
-
Each `meta.js` is a catalog entry: `description` (what the type is for), `content:` (what markdown it expects), `params:` (what frontmatter it accepts, with defaults), `presets:` (named param bundles). Read them as a menu — that is what they are. There is no CLI command that lists them; reading the folder *is* the discovery step.
|
|
239
|
+
Each `meta.js` is a catalog entry: `description` (what the type is for), `content:` (what markdown it expects), `params:` (what frontmatter it accepts, with defaults), `presets:` (named param bundles), and optionally `family:` (the standard section family it belongs to). Read them as a menu — that is what they are. There is no CLI command that lists them; reading the folder *is* the discovery step.
|
|
240
240
|
|
|
241
241
|
> **Never write a `type:` or a param you haven't confirmed exists.** Both failures are silent (Part 0) — invisible from the terminal, visible only on the page.
|
|
242
242
|
|
|
243
|
+
#### `family:` — so an editor can draw your section
|
|
244
|
+
|
|
245
|
+
An editor showing an author a section picker has to know what each section type
|
|
246
|
+
**is**, to pick its illustration and a label it can translate. Two ways to tell
|
|
247
|
+
it, in this order:
|
|
248
|
+
|
|
249
|
+
1. **Name the component for its family.** Run `uniweb families`, and use one of
|
|
250
|
+
those names when it fits — `Hero`, `Footer`, `Pricing`, `FAQ`, `CardGrid`.
|
|
251
|
+
Then there is nothing to declare: the name resolves on its own.
|
|
252
|
+
2. **Declare `family:`** when the name is yours — a variant, or your own
|
|
253
|
+
vocabulary:
|
|
254
|
+
```js
|
|
255
|
+
// sections/ProfileHero/meta.js
|
|
256
|
+
export default { title: 'Researcher Profile', family: 'profile' }
|
|
257
|
+
```
|
|
258
|
+
One value, the dominant shape: a `HeroWithEstimate` is `family: 'hero'`.
|
|
259
|
+
|
|
260
|
+
**Leave it off when nothing fits.** A section that is genuinely specific to this
|
|
261
|
+
site gets a generic illustration, and that is the right outcome — do not force it
|
|
262
|
+
into the nearest family. `uniweb doctor` shows what each of your sections
|
|
263
|
+
resolved to, and suggests a family for any it recognises by another name.
|
|
264
|
+
|
|
243
265
|
### 3. Find your lane
|
|
244
266
|
|
|
245
267
|
The architecture exists to keep content and code separate. Your task sits in one of them. Decide before you edit, then stay there.
|
package/src/commands/docs.js
CHANGED
|
@@ -175,8 +175,12 @@ Component metadata in <foundation>/components/[Name]/meta.js (or sections/[Name]
|
|
|
175
175
|
${colors.bright}Identity:${colors.reset}
|
|
176
176
|
${colors.cyan}title${colors.reset} Display name in editor
|
|
177
177
|
${colors.cyan}description${colors.reset} What the component does
|
|
178
|
-
${colors.cyan}
|
|
179
|
-
|
|
178
|
+
${colors.cyan}family${colors.reset} The standard section family this is — "hero", "pricing",
|
|
179
|
+
"faq". Picks the editor's illustration and a translated
|
|
180
|
+
label. Only needed when the component NAME is not already
|
|
181
|
+
one: \`Hero\` resolves on its own, \`ProfileHero\` does not.
|
|
182
|
+
An unrecognized value is legal and falls back.
|
|
183
|
+
${colors.dim}uniweb families${colors.reset}
|
|
180
184
|
${colors.cyan}hidden${colors.reset} If true, not selectable in frontmatter
|
|
181
185
|
|
|
182
186
|
${colors.bright}Content Expectations:${colors.reset}
|
|
@@ -202,7 +206,7 @@ ${colors.bright}Example:${colors.reset}
|
|
|
202
206
|
${colors.dim}export default {
|
|
203
207
|
title: 'Hero Banner',
|
|
204
208
|
description: 'Bold hero section with headline and CTA',
|
|
205
|
-
|
|
209
|
+
family: 'hero',
|
|
206
210
|
background: true,
|
|
207
211
|
|
|
208
212
|
content: {
|
package/src/commands/doctor.js
CHANGED
|
@@ -27,6 +27,8 @@ import { discoverFoundations, discoverSites } from '../utils/discover.js'
|
|
|
27
27
|
import { checkSiteInstall } from '../utils/install-integrity.js'
|
|
28
28
|
import { findWorkspaceRoot } from '../utils/workspace.js'
|
|
29
29
|
import { DATA_DIR } from '@uniweb/core/data-paths'
|
|
30
|
+
import { FAMILIES, resolveFamily } from '@uniweb/schemas/families'
|
|
31
|
+
import { suggestFamily } from '@uniweb/schemas/family-aliases'
|
|
30
32
|
|
|
31
33
|
/**
|
|
32
34
|
* Parse the `--fix [<issue-id>]` flag.
|
|
@@ -99,10 +101,21 @@ function loadFoundationJs(dir) {
|
|
|
99
101
|
}
|
|
100
102
|
|
|
101
103
|
/**
|
|
102
|
-
* Load built schema.json
|
|
104
|
+
* Load a foundation's built schema.json.
|
|
105
|
+
*
|
|
106
|
+
* ⛔ THE PATH IS `dist/meta/schema.json`, and it read `dist/schema.json` from
|
|
107
|
+
* 2026-02-06 until 2026-09-15 — a file the build has never written. So this
|
|
108
|
+
* returned `null` every time, and its one caller (the extension `vars`/layouts
|
|
109
|
+
* warning, which `framework/CLAUDE.md` gotcha #18 says doctor performs) could
|
|
110
|
+
* not fire at all.
|
|
111
|
+
*
|
|
112
|
+
* ⭐ THE CLASS IS WORTH MORE THAN THE BUG: a wrong PATH returns empty, it does
|
|
113
|
+
* not throw. `existsSync` said no, the function returned `null`, every caller
|
|
114
|
+
* used `?.` and skipped — and a check that never fires looks exactly like a
|
|
115
|
+
* codebase with nothing to report.
|
|
103
116
|
*/
|
|
104
117
|
function loadSchemaJson(dir) {
|
|
105
|
-
const schemaPath = join(dir, 'dist', 'schema.json')
|
|
118
|
+
const schemaPath = join(dir, 'dist', 'meta', 'schema.json')
|
|
106
119
|
if (!existsSync(schemaPath)) return null
|
|
107
120
|
try {
|
|
108
121
|
return JSON.parse(readFileSync(schemaPath, 'utf8'))
|
|
@@ -498,6 +511,175 @@ export async function checkFoundationSupports({
|
|
|
498
511
|
log(` ${colors.dim}uniweb doctor --fix ${id}${colors.reset}`)
|
|
499
512
|
}
|
|
500
513
|
|
|
514
|
+
/**
|
|
515
|
+
* Every section type's standard family — what it resolved to, and HOW.
|
|
516
|
+
*
|
|
517
|
+
* ⭐ THIS IS WHERE THE TAXONOMY IS ACTUALLY LEARNED. Nobody reads a word list in
|
|
518
|
+
* advance; they read the warning that names their own component and proposes the
|
|
519
|
+
* edit. A foundation developer sees their own picker here before an author does.
|
|
520
|
+
*
|
|
521
|
+
* ⛔ AN UNRECOGNIZED NAME IS NOT AN ERROR, and this check must never imply it is.
|
|
522
|
+
* A foundation with its own vocabulary is a supported choice: the section falls
|
|
523
|
+
* back to a generic illustration and nothing breaks. What is reported is the
|
|
524
|
+
* OPPORTUNITY — and only where there is a concrete suggestion to act on.
|
|
525
|
+
*
|
|
526
|
+
* ⚠️ READS THE BUILT `schema.json`, so it says nothing before a build. That is
|
|
527
|
+
* deliberate: schema.json is what a picker actually receives, and inferring from
|
|
528
|
+
* unbuilt source would report on a state no consumer sees.
|
|
529
|
+
*/
|
|
530
|
+
export function checkSectionFamilies({
|
|
531
|
+
foundationName,
|
|
532
|
+
folderName,
|
|
533
|
+
foundationDir,
|
|
534
|
+
issues,
|
|
535
|
+
shouldFixExplicitly,
|
|
536
|
+
fixed,
|
|
537
|
+
}) {
|
|
538
|
+
const schema = loadSchemaJson(foundationDir)
|
|
539
|
+
if (!schema) return
|
|
540
|
+
|
|
541
|
+
const familyIds = FAMILIES.map((f) => f.id)
|
|
542
|
+
const sections = Object.entries(schema)
|
|
543
|
+
.filter(([key, value]) => !key.startsWith('_') && key !== 'dataSchemas' && value && typeof value === 'object')
|
|
544
|
+
.map(([name, entry]) => ({ name, entry, ...resolveFamily({ name, family: entry.family }) }))
|
|
545
|
+
|
|
546
|
+
if (sections.length === 0) return
|
|
547
|
+
|
|
548
|
+
const declared = sections.filter((s) => s.source === 'declared')
|
|
549
|
+
const byName = sections.filter((s) => s.source === 'name')
|
|
550
|
+
const misses = sections.filter((s) => !s.id)
|
|
551
|
+
|
|
552
|
+
info(
|
|
553
|
+
`${folderName}: ${sections.length - misses.length}/${sections.length} sections have a standard family ` +
|
|
554
|
+
`${colors.dim}(${declared.length} declared, ${byName.length} from the name)${colors.reset}`
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
// ── a `family:` that names nothing we know ────────────────────────────────
|
|
558
|
+
const bogus = misses.filter((s) => s.unknown)
|
|
559
|
+
for (const s of bogus) {
|
|
560
|
+
const id = 'section-family-unknown'
|
|
561
|
+
const near = suggestFamily(s.unknown, familyIds)
|
|
562
|
+
issues.push({
|
|
563
|
+
id,
|
|
564
|
+
type: 'warning',
|
|
565
|
+
foundation: foundationName,
|
|
566
|
+
message: `${s.name} declares family '${s.unknown}', which is not a standard family`,
|
|
567
|
+
})
|
|
568
|
+
warn(`[${id}] ${folderName}/${s.name}: family '${s.unknown}' is not a standard family`)
|
|
569
|
+
if (near.id) log(` Did you mean ${colors.blue}${near.id}${colors.reset}?`)
|
|
570
|
+
log(` ${colors.dim}An unrecognized value is legal — it falls back to a generic illustration.${colors.reset}`)
|
|
571
|
+
log(` ${colors.dim}uniweb families${colors.reset}`)
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// ── a name we do not know, WITH something to suggest ──────────────────────
|
|
575
|
+
const undeclared = misses.filter((s) => !s.unknown)
|
|
576
|
+
const proposals = undeclared
|
|
577
|
+
.map((s) => ({ ...s, suggestion: suggestFamily(s.name, familyIds) }))
|
|
578
|
+
.filter((s) => s.suggestion.id || s.suggestion.ambiguous)
|
|
579
|
+
if (proposals.length === 0) return
|
|
580
|
+
|
|
581
|
+
const id = 'section-family-unrecognized'
|
|
582
|
+
const writable = proposals.filter((p) => p.suggestion.fixable && p.suggestion.id)
|
|
583
|
+
|
|
584
|
+
// ⛔ EXPLICIT ID ONLY — never a bare `--fix`. Every id below is a GUESS from a
|
|
585
|
+
// table that exists here, rather than in the resolver, precisely so a person
|
|
586
|
+
// approves it. A bare `--fix` writing them would put the silent-wrong-guess
|
|
587
|
+
// failure back, one directory deeper.
|
|
588
|
+
if (shouldFixExplicitly?.(id) && writable.length > 0) {
|
|
589
|
+
const srcDir = resolveFoundationSrcPath(foundationDir)
|
|
590
|
+
for (const p of writable) {
|
|
591
|
+
const metaPath = join(srcDir, p.entry.path || '', 'meta.js')
|
|
592
|
+
const written = writeFamilyIntoMeta(metaPath, p.suggestion.id)
|
|
593
|
+
if (written) fixed?.(`${folderName}/${p.entry.path}/meta.js now declares family: '${p.suggestion.id}'`)
|
|
594
|
+
else warn(`[${id}] could not write ${p.name}'s meta.js — add \`family: '${p.suggestion.id}'\` by hand`)
|
|
595
|
+
}
|
|
596
|
+
return
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
issues.push({
|
|
600
|
+
id,
|
|
601
|
+
type: 'info',
|
|
602
|
+
foundation: foundationName,
|
|
603
|
+
message: `${proposals.length} section(s) in ${foundationName} could declare a standard family`,
|
|
604
|
+
details: proposals.map((p) => ({ name: p.name, suggested: p.suggestion.id, via: p.suggestion.via })),
|
|
605
|
+
fixable: writable.length > 0,
|
|
606
|
+
})
|
|
607
|
+
|
|
608
|
+
warn(`[${id}] ${folderName}: ${proposals.length} section(s) have no standard family`)
|
|
609
|
+
const width = Math.max(...proposals.map((p) => p.name.length))
|
|
610
|
+
for (const p of proposals) {
|
|
611
|
+
const { id: hit, via, ambiguous } = p.suggestion
|
|
612
|
+
if (ambiguous) {
|
|
613
|
+
log(` ${p.name.padEnd(width)} ${colors.dim}ambiguous — ${ambiguous}${colors.reset}`)
|
|
614
|
+
} else {
|
|
615
|
+
const how = via === 'near' ? 'possible typo' : via === 'suffix' ? 'name + shape suffix' : 'known alias'
|
|
616
|
+
log(` ${p.name.padEnd(width)} → ${colors.blue}${hit}${colors.reset} ${colors.dim}(${how})${colors.reset}`)
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
log(` ${colors.dim}Nothing is broken — an unrecognized name falls back to a generic illustration.${colors.reset}`)
|
|
620
|
+
if (writable.length > 0) {
|
|
621
|
+
log(` ${colors.dim}uniweb doctor --fix ${id}${colors.reset} ${colors.dim}— writes ${writable.length} \`family:\` line(s). Read the diff: these are guesses.${colors.reset}`)
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/**
|
|
626
|
+
* Insert `family: '<id>'` as the first key of a meta.js default export.
|
|
627
|
+
*
|
|
628
|
+
* ⛔ REFUSES ANYTHING IT DOES NOT RECOGNIZE. A meta.js is hand-written source,
|
|
629
|
+
* and a half-understood rewrite of someone's file is far worse than telling them
|
|
630
|
+
* to add one line. A `false` return is reported as "could not write", never as
|
|
631
|
+
* fixed.
|
|
632
|
+
*/
|
|
633
|
+
function writeFamilyIntoMeta(metaPath, familyId) {
|
|
634
|
+
if (!existsSync(metaPath)) return false
|
|
635
|
+
try {
|
|
636
|
+
const src = readFileSync(metaPath, 'utf8')
|
|
637
|
+
if (/^\s*family\s*:/m.test(src)) return false // already has one — never overwrite
|
|
638
|
+
const m = src.match(/export\s+default\s*\{[ \t]*\r?\n/)
|
|
639
|
+
if (!m) return false
|
|
640
|
+
const at = m.index + m[0].length
|
|
641
|
+
const indent = src.slice(at).match(/^([ \t]+)/)?.[1] ?? ' '
|
|
642
|
+
writeFileSync(metaPath, `${src.slice(0, at)}${indent}family: '${familyId}',\n${src.slice(at)}`)
|
|
643
|
+
return true
|
|
644
|
+
} catch {
|
|
645
|
+
return false
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* `category:` and `purpose:` were retired when `family:` landed.
|
|
651
|
+
*
|
|
652
|
+
* ⚖️ INFO, NOT A WARNING. Both are inert — nothing in the build, the runtime or
|
|
653
|
+
* the wire has ever read either — so a foundation carrying one is not broken and
|
|
654
|
+
* does not need to hurry. It is worth saying once, because a developer who sees
|
|
655
|
+
* `category:` in a neighbour's meta.js will reasonably copy it.
|
|
656
|
+
*/
|
|
657
|
+
export function checkRetiredMetaKeys({ foundationName, folderName, foundationDir, issues }) {
|
|
658
|
+
const schema = loadSchemaJson(foundationDir)
|
|
659
|
+
if (!schema) return
|
|
660
|
+
const stale = Object.entries(schema)
|
|
661
|
+
.filter(([key, v]) => !key.startsWith('_') && key !== 'dataSchemas' && v && typeof v === 'object')
|
|
662
|
+
.filter(([, v]) => v.category !== undefined || v.purpose !== undefined)
|
|
663
|
+
if (stale.length === 0) return
|
|
664
|
+
|
|
665
|
+
const id = 'section-retired-meta-key'
|
|
666
|
+
const keys = [...new Set(stale.flatMap(([, v]) => [
|
|
667
|
+
v.category !== undefined ? 'category' : null,
|
|
668
|
+
v.purpose !== undefined ? 'purpose' : null,
|
|
669
|
+
].filter(Boolean)))]
|
|
670
|
+
issues.push({
|
|
671
|
+
id,
|
|
672
|
+
type: 'info',
|
|
673
|
+
foundation: foundationName,
|
|
674
|
+
message: `${stale.length} section(s) still declare ${keys.join(' / ')}`,
|
|
675
|
+
})
|
|
676
|
+
info(
|
|
677
|
+
`[${id}] ${folderName}: ${stale.length} section(s) still declare ` +
|
|
678
|
+
`${colors.dim}${keys.join(' / ')}${colors.reset} — retired, and read by nothing`
|
|
679
|
+
)
|
|
680
|
+
log(` ${colors.dim}\`family:\` replaced them. Removing them changes no behaviour.${colors.reset}`)
|
|
681
|
+
}
|
|
682
|
+
|
|
501
683
|
/**
|
|
502
684
|
* The gates kit exposes for a host service, and the field that IS the gate.
|
|
503
685
|
*
|
|
@@ -888,6 +1070,11 @@ export async function doctor(args = []) {
|
|
|
888
1070
|
// each diagnostic below so the rewrite happens with full context.
|
|
889
1071
|
const fixFlag = parseFixFlag(args)
|
|
890
1072
|
const shouldFix = (id) => fixFlag === 'all' || fixFlag === id
|
|
1073
|
+
// ⛔ SOME FIXES MUST NOT RIDE A BARE `--fix`. A fix derived from a GUESS — the
|
|
1074
|
+
// family aliases, which exist in the CLI precisely so a human approves each
|
|
1075
|
+
// one — becomes a silent write the moment `--fix` alone applies it, which is
|
|
1076
|
+
// the failure the guess was moved here to avoid. Those checks ask for the id.
|
|
1077
|
+
const shouldFixExplicitly = (id) => fixFlag === id
|
|
891
1078
|
const fixed = (msg) =>
|
|
892
1079
|
console.log(` ${colors.green}↳ Fixed:${colors.reset} ${msg}`)
|
|
893
1080
|
|
|
@@ -1054,6 +1241,26 @@ export async function doctor(args = []) {
|
|
|
1054
1241
|
log(` ${colors.dim}@import "@uniweb/kit/prose-tokens.css";${colors.reset}`)
|
|
1055
1242
|
}
|
|
1056
1243
|
|
|
1244
|
+
// ⛔ SAY WHEN A CHECK DID NOT RUN. Every foundation check below reads the
|
|
1245
|
+
// BUILT `dist/meta/schema.json` — what a consumer actually receives — and
|
|
1246
|
+
// returns silently when there is none. In a workspace with a site, "Foundation
|
|
1247
|
+
// not built yet" turns up later under that site; in a FOUNDATION-ONLY
|
|
1248
|
+
// workspace, which is the normal shape when a foundation is the product, it
|
|
1249
|
+
// never does. The report then looks clean because nothing ran.
|
|
1250
|
+
//
|
|
1251
|
+
// That is the same class as the `dist/schema.json` path bug (a check that
|
|
1252
|
+
// never fires is indistinguishable from a codebase with nothing to report),
|
|
1253
|
+
// so it is named here rather than left to be rediscovered.
|
|
1254
|
+
for (const f of foundations) {
|
|
1255
|
+
if (!loadSchemaJson(f.path)) {
|
|
1256
|
+
info(
|
|
1257
|
+
`${f.folderName}: not built, so nothing was checked here ` +
|
|
1258
|
+
`${colors.dim}(services, section families, retired keys)${colors.reset}`
|
|
1259
|
+
)
|
|
1260
|
+
log(` ${colors.dim}Run \`uniweb build\` in ${f.folderName}, then doctor again.${colors.reset}`)
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1057
1264
|
// ── uniweb.supports — the services this foundation says it is built against ──
|
|
1058
1265
|
for (const f of foundations) {
|
|
1059
1266
|
await checkFoundationSupports({
|
|
@@ -1071,6 +1278,20 @@ export async function doctor(args = []) {
|
|
|
1071
1278
|
srcDir: resolveFoundationSrcPath(f.path),
|
|
1072
1279
|
issues
|
|
1073
1280
|
})
|
|
1281
|
+
checkSectionFamilies({
|
|
1282
|
+
foundationName: f.name,
|
|
1283
|
+
folderName: f.folderName,
|
|
1284
|
+
foundationDir: f.path,
|
|
1285
|
+
issues,
|
|
1286
|
+
shouldFixExplicitly,
|
|
1287
|
+
fixed
|
|
1288
|
+
})
|
|
1289
|
+
checkRetiredMetaKeys({
|
|
1290
|
+
foundationName: f.name,
|
|
1291
|
+
folderName: f.folderName,
|
|
1292
|
+
foundationDir: f.path,
|
|
1293
|
+
issues
|
|
1294
|
+
})
|
|
1074
1295
|
}
|
|
1075
1296
|
|
|
1076
1297
|
// Extensions carry `uniweb.supports` too — same key, same field, and a site's
|
|
@@ -1093,6 +1314,22 @@ export async function doctor(args = []) {
|
|
|
1093
1314
|
srcDir: resolveFoundationSrcPath(e.path),
|
|
1094
1315
|
issues
|
|
1095
1316
|
})
|
|
1317
|
+
// An extension contributes section types by URL and lands in the same
|
|
1318
|
+
// picker, so its sections need a family for exactly the same reason.
|
|
1319
|
+
checkSectionFamilies({
|
|
1320
|
+
foundationName: e.name,
|
|
1321
|
+
folderName: e.folderName,
|
|
1322
|
+
foundationDir: e.path,
|
|
1323
|
+
issues,
|
|
1324
|
+
shouldFixExplicitly,
|
|
1325
|
+
fixed
|
|
1326
|
+
})
|
|
1327
|
+
checkRetiredMetaKeys({
|
|
1328
|
+
foundationName: e.name,
|
|
1329
|
+
folderName: e.folderName,
|
|
1330
|
+
foundationDir: e.path,
|
|
1331
|
+
issues
|
|
1332
|
+
})
|
|
1096
1333
|
}
|
|
1097
1334
|
|
|
1098
1335
|
if (extensions.length > 0) {
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `uniweb families` — the standard section families this CLI knows.
|
|
3
|
+
*
|
|
4
|
+
* ⭐ NAMED AFTER THE FIELD, not after the things it lists. A developer's question
|
|
5
|
+
* is "what can I put in `family:`?", and they will type the word they just read
|
|
6
|
+
* in a `meta.js` or a doctor warning.
|
|
7
|
+
*
|
|
8
|
+
* ⚠️ IT REPORTS THE ROSTER THIS CLI SHIPS WITH, not a live fetch — the list comes
|
|
9
|
+
* from the `@uniweb/schemas` version this CLI resolved. That is the right answer,
|
|
10
|
+
* because it is also the list `uniweb doctor` matched against. A newer family
|
|
11
|
+
* than your CLI will not appear, and declaring one is not fatal either way: an
|
|
12
|
+
* unrecognized value falls back to a generic illustration.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { FAMILIES, GROUPS } from '@uniweb/schemas/families'
|
|
16
|
+
import { getCliVersion } from '../versions.js'
|
|
17
|
+
|
|
18
|
+
const colors = {
|
|
19
|
+
reset: '\x1b[0m',
|
|
20
|
+
bright: '\x1b[1m',
|
|
21
|
+
dim: '\x1b[2m',
|
|
22
|
+
cyan: '\x1b[36m'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function families(args = []) {
|
|
26
|
+
if (args.includes('--json')) {
|
|
27
|
+
// ⛔ stdout carries JSON and nothing else, so it pipes. Every human-facing
|
|
28
|
+
// line in this command goes to stdout only in the non-JSON branch.
|
|
29
|
+
process.stdout.write(
|
|
30
|
+
JSON.stringify(
|
|
31
|
+
{ cliVersion: getCliVersion(), count: FAMILIES.length, groups: GROUPS, families: FAMILIES },
|
|
32
|
+
null,
|
|
33
|
+
2
|
|
34
|
+
) + '\n'
|
|
35
|
+
)
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const filter = args.find((a) => !a.startsWith('-'))
|
|
40
|
+
const shown = filter
|
|
41
|
+
? FAMILIES.filter(
|
|
42
|
+
(f) =>
|
|
43
|
+
f.id.includes(filter.toLowerCase()) ||
|
|
44
|
+
f.label.toLowerCase().includes(filter.toLowerCase()) ||
|
|
45
|
+
f.group === filter.toLowerCase()
|
|
46
|
+
)
|
|
47
|
+
: FAMILIES
|
|
48
|
+
|
|
49
|
+
console.log('')
|
|
50
|
+
console.log(
|
|
51
|
+
`${colors.bright}Section families${colors.reset} ${colors.dim}(uniweb ${getCliVersion()})${colors.reset}`
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if (shown.length === 0) {
|
|
55
|
+
console.log('')
|
|
56
|
+
console.log(` ${colors.dim}Nothing matches "${filter}".${colors.reset}`)
|
|
57
|
+
console.log(
|
|
58
|
+
` ${colors.dim}A name of your own is fine — it falls back to a generic illustration.${colors.reset}`
|
|
59
|
+
)
|
|
60
|
+
console.log('')
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const width = Math.max(...shown.map((f) => f.id.length))
|
|
65
|
+
for (const group of GROUPS) {
|
|
66
|
+
const rows = shown.filter((f) => f.group === group.id)
|
|
67
|
+
if (rows.length === 0) continue
|
|
68
|
+
console.log('')
|
|
69
|
+
console.log(` ${colors.bright}${group.label}${colors.reset} ${colors.dim}— ${group.description}${colors.reset}`)
|
|
70
|
+
for (const f of rows) {
|
|
71
|
+
console.log(` ${colors.cyan}${f.id.padEnd(width)}${colors.reset} ${colors.dim}${f.label}${colors.reset}`)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.log('')
|
|
76
|
+
console.log(` ${colors.dim}Declare one in a section's meta.js: family: 'hero'${colors.reset}`)
|
|
77
|
+
console.log(
|
|
78
|
+
` ${colors.dim}A component already named for its family needs no declaration.${colors.reset}`
|
|
79
|
+
)
|
|
80
|
+
console.log(` ${colors.dim}--json for scripts. uniweb doctor reports what your sections resolved to.${colors.reset}`)
|
|
81
|
+
console.log('')
|
|
82
|
+
}
|
package/src/framework-index.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-09-
|
|
3
|
+
"generatedAt": "2026-09-16T04:55:41.770Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/api": {
|
|
6
6
|
"version": "0.3.6",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
]
|
|
11
11
|
},
|
|
12
12
|
"@uniweb/build": {
|
|
13
|
-
"version": "0.52.
|
|
13
|
+
"version": "0.52.3",
|
|
14
14
|
"path": "framework/build",
|
|
15
15
|
"deps": [
|
|
16
16
|
"@uniweb/content-reader",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"deps": []
|
|
35
35
|
},
|
|
36
36
|
"@uniweb/core": {
|
|
37
|
-
"version": "0.29.
|
|
37
|
+
"version": "0.29.4",
|
|
38
38
|
"path": "framework/core",
|
|
39
39
|
"deps": [
|
|
40
40
|
"@uniweb/semantic-parser",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
]
|
|
55
55
|
},
|
|
56
56
|
"@uniweb/kit": {
|
|
57
|
-
"version": "0.19.
|
|
57
|
+
"version": "0.19.2",
|
|
58
58
|
"path": "framework/kit",
|
|
59
59
|
"deps": [
|
|
60
60
|
"@uniweb/core",
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
]
|
|
83
83
|
},
|
|
84
84
|
"@uniweb/runtime": {
|
|
85
|
-
"version": "0.26.
|
|
85
|
+
"version": "0.26.4",
|
|
86
86
|
"path": "framework/runtime",
|
|
87
87
|
"deps": [
|
|
88
88
|
"@uniweb/core",
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
"deps": []
|
|
96
96
|
},
|
|
97
97
|
"@uniweb/schemas": {
|
|
98
|
-
"version": "0.2.
|
|
98
|
+
"version": "0.2.15",
|
|
99
99
|
"path": "framework/schemas",
|
|
100
100
|
"deps": []
|
|
101
101
|
},
|
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
"deps": []
|
|
111
111
|
},
|
|
112
112
|
"@uniweb/templates": {
|
|
113
|
-
"version": "0.14.
|
|
113
|
+
"version": "0.14.4",
|
|
114
114
|
"path": "framework/templates",
|
|
115
115
|
"deps": []
|
|
116
116
|
},
|
package/src/index.js
CHANGED
|
@@ -956,6 +956,13 @@ async function main() {
|
|
|
956
956
|
return
|
|
957
957
|
}
|
|
958
958
|
|
|
959
|
+
// Handle families command — the standard section families this CLI ships with
|
|
960
|
+
if (command === 'families') {
|
|
961
|
+
const { families } = await import('./commands/families.js')
|
|
962
|
+
await families(args.slice(1))
|
|
963
|
+
return
|
|
964
|
+
}
|
|
965
|
+
|
|
959
966
|
// Handle template command
|
|
960
967
|
if (command === 'template') {
|
|
961
968
|
await template(args.slice(1))
|
|
@@ -1705,6 +1712,33 @@ ${colors.cyan}${colors.bright}uniweb handoff${colors.reset} ${colors.dim}— (re
|
|
|
1705
1712
|
|
|
1706
1713
|
The site-handoff flow was retired with the legacy backend.
|
|
1707
1714
|
Manage client sites from the Uniweb app for now.
|
|
1715
|
+
`,
|
|
1716
|
+
families: `
|
|
1717
|
+
${colors.cyan}${colors.bright}uniweb families${colors.reset} ${colors.dim}— The standard section families${colors.reset}
|
|
1718
|
+
|
|
1719
|
+
${colors.bright}Usage:${colors.reset}
|
|
1720
|
+
uniweb families List them, grouped
|
|
1721
|
+
uniweb families <filter> Match an id, a label or a group name
|
|
1722
|
+
uniweb families --json Machine-readable (for scripts)
|
|
1723
|
+
|
|
1724
|
+
${colors.bright}What a family is for:${colors.reset}
|
|
1725
|
+
It tells an editor which illustration to show for a section type, and gives
|
|
1726
|
+
it a label it can translate. Declare it in a section's meta.js:
|
|
1727
|
+
|
|
1728
|
+
${colors.dim}export default { title: 'Researcher Profile', family: 'profile' }${colors.reset}
|
|
1729
|
+
|
|
1730
|
+
${colors.bright}Most components need no declaration.${colors.reset} The section type IS the component
|
|
1731
|
+
name, so \`Hero\`, \`Footer\` and \`Pricing\` already resolve. \`family:\` is for the
|
|
1732
|
+
ones whose name does not give it away — \`ProfileHero\`, \`CvEntry\`.
|
|
1733
|
+
|
|
1734
|
+
An unrecognized value is legal: it falls back to a generic illustration and
|
|
1735
|
+
nothing breaks, so a foundation with its own vocabulary is never blocked.
|
|
1736
|
+
|
|
1737
|
+
\`uniweb doctor\` reports what each of your sections resolved to, and suggests
|
|
1738
|
+
a family for the ones it recognizes by another name.
|
|
1739
|
+
|
|
1740
|
+
${colors.bright}Note:${colors.reset} this is the roster ${colors.bright}this CLI ships with${colors.reset}, not a live fetch — and it is
|
|
1741
|
+
the same list \`doctor\` matches against.
|
|
1708
1742
|
`,
|
|
1709
1743
|
template: `
|
|
1710
1744
|
${colors.cyan}${colors.bright}uniweb template${colors.reset} ${colors.dim}— Official templates${colors.reset}
|
|
@@ -1835,6 +1869,7 @@ ${colors.bright}Commands:${colors.reset}
|
|
|
1835
1869
|
status Show a site's sync state (unpushed content, foundation)
|
|
1836
1870
|
inspect <path> Inspect parsed content shape of a markdown file or folder
|
|
1837
1871
|
docs Generate component documentation
|
|
1872
|
+
families List the standard section families (for meta.js family:)
|
|
1838
1873
|
doctor Diagnose project configuration issues
|
|
1839
1874
|
validate Check your content against your foundation's data schemas
|
|
1840
1875
|
update Align workspace deps + AGENTS.md to the running CLI
|