uniweb 0.56.4 → 0.56.6
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 +9 -8
- package/partials/agents.md +5 -4
- package/src/commands/add.js +124 -8
- package/src/commands/starter.js +202 -0
- package/src/framework-index.json +6 -6
- package/src/index.js +11 -2
- package/src/utils/scaffold.js +10 -1
- package/templates/site/site.yml.hbs +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.56.
|
|
3
|
+
"version": "0.56.6",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,16 +41,17 @@
|
|
|
41
41
|
"js-yaml": "^4.1.0",
|
|
42
42
|
"prompts": "^2.4.2",
|
|
43
43
|
"tar": "^7.0.0",
|
|
44
|
+
"@uniweb/content-writer": "^0.3.4",
|
|
44
45
|
"@uniweb/core": "^0.29.4",
|
|
45
|
-
"@uniweb/kit": "^0.19.
|
|
46
|
-
"@uniweb/
|
|
47
|
-
"@uniweb/
|
|
48
|
-
"@uniweb/
|
|
46
|
+
"@uniweb/kit": "^0.19.3",
|
|
47
|
+
"@uniweb/semantic-parser": "^1.4.1",
|
|
48
|
+
"@uniweb/schemas": "^0.3.1",
|
|
49
|
+
"@uniweb/runtime": "^0.26.4"
|
|
49
50
|
},
|
|
50
51
|
"peerDependencies": {
|
|
51
|
-
"@uniweb/
|
|
52
|
-
"@uniweb/
|
|
53
|
-
"@uniweb/
|
|
52
|
+
"@uniweb/semantic-parser": "^1.4.1",
|
|
53
|
+
"@uniweb/build": "^0.52.5",
|
|
54
|
+
"@uniweb/content-reader": "^1.2.5"
|
|
54
55
|
},
|
|
55
56
|
"peerDependenciesMeta": {
|
|
56
57
|
"@uniweb/build": {
|
package/partials/agents.md
CHANGED
|
@@ -341,7 +341,7 @@ Description paragraph.
|
|
|
341
341
|

|
|
342
342
|
```
|
|
343
343
|
|
|
344
|
-
Heading levels set *structure* (pretitle, title, subtitle), not font size — the component controls visual sizing.
|
|
344
|
+
Heading levels set *structure* (pretitle, title, subtitle), not font size — the component controls visual sizing. Write a pretitle as a `#>` label line — any number of leading `#`s spells the same label, so match the heading you are labelling. A smaller ordinary heading directly above the title is *also* read as a pretitle, which is how files written before label lines keep working; prefer `#>`, because it says what it is wherever it lands rather than depending on what follows it.
|
|
345
345
|
|
|
346
346
|
**A section with no `type:` renders through the foundation's default section type — a component named `Section`, unless the foundation's `main.js` sets `defaultSection` to something else.** This is what lets a folder of plain markdown with no frontmatter at all become pages: mounted documentation, an imported wiki, anything written before it met this framework. If such content renders blank, the foundation has no `Section` — that, not the markdown, is what to fix.
|
|
347
347
|
|
|
@@ -356,8 +356,9 @@ The semantic parser produces a flat, guaranteed structure. No null checks needed
|
|
|
356
356
|
```js
|
|
357
357
|
content = {
|
|
358
358
|
title: '', // Main heading (string or string[] for multi-line)
|
|
359
|
-
pretitle: '', // `#>` label line(s)
|
|
360
|
-
// the title
|
|
359
|
+
pretitle: '', // `#>` label line(s) — also filled by smaller headings
|
|
360
|
+
// stacked above the title, for older content
|
|
361
|
+
// (string or string[])
|
|
361
362
|
subtitle: '', // Line(s) one step below the title — each further
|
|
362
363
|
// one-step descent is another line (string or string[])
|
|
363
364
|
paragraphs: [], // Text blocks
|
|
@@ -401,7 +402,7 @@ Lightning quick. │ content.items[0].paragraphs[0] = "Lightning
|
|
|
401
402
|
Enterprise-grade security. │ content.items[1].paragraphs[0] = "Enterprise-grade…"
|
|
402
403
|
```
|
|
403
404
|
|
|
404
|
-
The staircase rule produces this — each heading relates to the one before it: the same size adds another line to the same part; **one step smaller** joins the headline as the next part down (the subtitle, then further subtitle lines); **two steps smaller** starts an item; and once body content has begun, *any* heading starts an item. `#>` label lines
|
|
405
|
+
The staircase rule produces this — each heading relates to the one before it: the same size adds another line to the same part; **one step smaller** joins the headline as the next part down (the subtitle, then further subtitle lines); **two steps smaller** starts an item; and once body content has begun, *any* heading starts an item. `#>` label lines become `pretitle`; so do smaller headings stacked above the title, which is the older spelling.
|
|
405
406
|
|
|
406
407
|
### Items have the full content shape
|
|
407
408
|
|
package/src/commands/add.js
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
updateRootScripts
|
|
36
36
|
} from '../utils/config.js'
|
|
37
37
|
import { discoverFoundations, discoverSites } from '../utils/discover.js'
|
|
38
|
+
import { generateStarter, reportStarter, declarationFor } from './starter.js'
|
|
38
39
|
import {
|
|
39
40
|
validatePackageName,
|
|
40
41
|
getExistingPackageNames,
|
|
@@ -73,17 +74,30 @@ const colors = {
|
|
|
73
74
|
red: '\x1b[31m'
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
// Porcelain (`--json`) mode: stdout carries ONLY the JSON, so every human line
|
|
78
|
+
// diverts to stderr — the convention `register.js` already follows and
|
|
79
|
+
// `families.js` states ("stdout carries JSON and nothing else, so it pipes").
|
|
80
|
+
//
|
|
81
|
+
// ⛔ It is set per run rather than only when true, because a module-level flag
|
|
82
|
+
// that is only ever turned ON leaks into the next call in the same process —
|
|
83
|
+
// which is every call in the test suite.
|
|
84
|
+
let jsonMode = false
|
|
85
|
+
export function setAddJsonMode(on) {
|
|
86
|
+
jsonMode = Boolean(on)
|
|
87
|
+
}
|
|
88
|
+
|
|
76
89
|
function log(message) {
|
|
77
|
-
console.
|
|
90
|
+
if (jsonMode) console.error(message)
|
|
91
|
+
else console.log(message)
|
|
78
92
|
}
|
|
79
93
|
function success(message) {
|
|
80
|
-
|
|
94
|
+
log(`${colors.green}✓${colors.reset} ${message}`)
|
|
81
95
|
}
|
|
82
96
|
function error(message) {
|
|
83
97
|
console.error(`${colors.red}✗${colors.reset} ${message}`)
|
|
84
98
|
}
|
|
85
99
|
function info(message) {
|
|
86
|
-
|
|
100
|
+
log(`${colors.dim}${message}${colors.reset}`)
|
|
87
101
|
}
|
|
88
102
|
|
|
89
103
|
/**
|
|
@@ -106,12 +120,19 @@ function parseArgs(args) {
|
|
|
106
120
|
// scaffold PR-preview workflows.
|
|
107
121
|
target: null,
|
|
108
122
|
projectName: null,
|
|
109
|
-
previews: true
|
|
123
|
+
previews: true,
|
|
124
|
+
// `add section` only: generate starter content for the section type from
|
|
125
|
+
// its `content:` declaration, and which preset's params to frontmatter it
|
|
126
|
+
// with. `--write` sends the markdown to a file instead of stdout.
|
|
127
|
+
starter: false,
|
|
128
|
+
preset: null,
|
|
129
|
+
write: null,
|
|
130
|
+
json: false
|
|
110
131
|
}
|
|
111
132
|
|
|
112
133
|
// Booleans (no value) consumed up-front so the value-flag loop below
|
|
113
134
|
// doesn't accidentally swallow the next positional.
|
|
114
|
-
const BOOLEAN_FLAGS = new Set(['--force', '--no-previews'])
|
|
135
|
+
const BOOLEAN_FLAGS = new Set(['--force', '--no-previews', '--starter', '--json'])
|
|
115
136
|
|
|
116
137
|
// Value flags, mapped to their result key. Both spellings are accepted:
|
|
117
138
|
// `--host github-pages` and `--host=github-pages`.
|
|
@@ -131,7 +152,9 @@ function parseArgs(args) {
|
|
|
131
152
|
'--host': 'host',
|
|
132
153
|
'--domain': 'domain',
|
|
133
154
|
'--target': 'target',
|
|
134
|
-
'--project-name': 'projectName'
|
|
155
|
+
'--project-name': 'projectName',
|
|
156
|
+
'--preset': 'preset',
|
|
157
|
+
'--write': 'write'
|
|
135
158
|
}
|
|
136
159
|
|
|
137
160
|
/** Split `--flag=value` into [flag, value]; `--flag` into [flag, null]. */
|
|
@@ -169,6 +192,10 @@ function parseArgs(args) {
|
|
|
169
192
|
result.force = true
|
|
170
193
|
} else if (flag === '--no-previews') {
|
|
171
194
|
result.previews = false
|
|
195
|
+
} else if (flag === '--starter') {
|
|
196
|
+
result.starter = true
|
|
197
|
+
} else if (flag === '--json') {
|
|
198
|
+
result.json = true
|
|
172
199
|
}
|
|
173
200
|
}
|
|
174
201
|
|
|
@@ -1074,6 +1101,7 @@ async function wireExtensionToSite(
|
|
|
1074
1101
|
* Add a section type to a foundation
|
|
1075
1102
|
*/
|
|
1076
1103
|
async function addSection(rootDir, opts) {
|
|
1104
|
+
setAddJsonMode(opts.json)
|
|
1077
1105
|
let name = opts.name
|
|
1078
1106
|
|
|
1079
1107
|
// Interactive name prompt when not provided
|
|
@@ -1169,10 +1197,31 @@ async function addSection(rootDir, opts) {
|
|
|
1169
1197
|
const sectionDir = join(sectionsDir, name)
|
|
1170
1198
|
const relSectionPath = relative(foundationDir, sectionDir)
|
|
1171
1199
|
|
|
1200
|
+
// ⭐ `--starter` ON AN EXISTING SECTION IS NOT AN ERROR. The flag asks one
|
|
1201
|
+
// question — *what content would an author start this section with?* — and a
|
|
1202
|
+
// section type that already exists is the case where it has a real `content:`
|
|
1203
|
+
// declaration to answer from. Refusing here would make the flag testable only
|
|
1204
|
+
// against stubs, which is the one case where the answer is least interesting.
|
|
1205
|
+
// Nothing is scaffolded and nothing is overwritten on this path.
|
|
1172
1206
|
if (existsSync(sectionDir)) {
|
|
1207
|
+
if (opts.starter) {
|
|
1208
|
+
const { markdown, result } = await generateStarter({
|
|
1209
|
+
name,
|
|
1210
|
+
sectionDir,
|
|
1211
|
+
preset: opts.preset,
|
|
1212
|
+
json: opts.json,
|
|
1213
|
+
write: opts.write,
|
|
1214
|
+
})
|
|
1215
|
+
if (!opts.json) {
|
|
1216
|
+
if (!opts.write) log('\n' + markdown.trimEnd())
|
|
1217
|
+
reportStarter(result, { write: opts.write })
|
|
1218
|
+
}
|
|
1219
|
+
return
|
|
1220
|
+
}
|
|
1173
1221
|
error(
|
|
1174
1222
|
`Section '${name}' already exists at ${foundation.path}/${relSectionPath}/`
|
|
1175
1223
|
)
|
|
1224
|
+
log(` ${colors.dim}--starter generates starter content for it without touching the files.${colors.reset}`)
|
|
1176
1225
|
process.exit(1)
|
|
1177
1226
|
}
|
|
1178
1227
|
|
|
@@ -1207,9 +1256,21 @@ export default function ${name}({ content, params }) {
|
|
|
1207
1256
|
}
|
|
1208
1257
|
`
|
|
1209
1258
|
|
|
1259
|
+
// With `--starter`, the scaffold gets a `content:` declaration derived from
|
|
1260
|
+
// the family the name resolves to — so the three pieces agree: a declaration,
|
|
1261
|
+
// a component that reads it, and content that fills it. Without the flag the
|
|
1262
|
+
// stub declares nothing, exactly as before.
|
|
1263
|
+
let starterDeclaration = ''
|
|
1264
|
+
let starterResult = null
|
|
1265
|
+
if (opts.starter) {
|
|
1266
|
+
const preview = await generateStarter({ name, sectionDir: sectionDir })
|
|
1267
|
+
starterResult = preview.result
|
|
1268
|
+
starterDeclaration = await declarationFor(preview.result)
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1210
1271
|
const metaContent = `export default {
|
|
1211
1272
|
title: '${name}',
|
|
1212
|
-
description: ''
|
|
1273
|
+
description: '',${starterDeclaration}
|
|
1213
1274
|
params: {},
|
|
1214
1275
|
}
|
|
1215
1276
|
`
|
|
@@ -1230,6 +1291,37 @@ export default function ${name}({ content, params }) {
|
|
|
1230
1291
|
`${colors.dim}The dev server will pick it up automatically.${colors.reset}`
|
|
1231
1292
|
)
|
|
1232
1293
|
}
|
|
1294
|
+
|
|
1295
|
+
if (starterResult) {
|
|
1296
|
+
// Regenerate against the declaration just written, so what is printed is
|
|
1297
|
+
// what this section's own `meta.js` now asks for rather than the family's
|
|
1298
|
+
// guess — the two agree here, and saying it from the file keeps them so.
|
|
1299
|
+
const { markdown, result } = await generateStarter({
|
|
1300
|
+
name,
|
|
1301
|
+
sectionDir,
|
|
1302
|
+
preset: opts.preset,
|
|
1303
|
+
json: opts.json,
|
|
1304
|
+
write: opts.write,
|
|
1305
|
+
})
|
|
1306
|
+
if (!opts.json) {
|
|
1307
|
+
log('')
|
|
1308
|
+
log(`${colors.dim}Starter content for a page section:${colors.reset}`)
|
|
1309
|
+
if (!opts.write) log('\n' + markdown.trimEnd())
|
|
1310
|
+
reportStarter(result, { write: opts.write })
|
|
1311
|
+
// The stub component reads three elements. The declaration above may name
|
|
1312
|
+
// more, because it comes from the family rather than from the stub — say
|
|
1313
|
+
// so, rather than leave a developer wondering why half the content they
|
|
1314
|
+
// were just handed renders as nothing.
|
|
1315
|
+
const stubReads = new Set(['title', 'paragraphs', 'links'])
|
|
1316
|
+
const unread = Object.keys(result.content).filter((slot) => !stubReads.has(slot))
|
|
1317
|
+
if (unread.length) {
|
|
1318
|
+
log(
|
|
1319
|
+
` ${colors.dim}index.jsx reads title, paragraphs and links — extend it for: ${unread.join(', ')}${colors.reset}`
|
|
1320
|
+
)
|
|
1321
|
+
log('')
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1233
1325
|
}
|
|
1234
1326
|
|
|
1235
1327
|
/**
|
|
@@ -1730,7 +1822,20 @@ function parseNodeMajor(engines) {
|
|
|
1730
1822
|
}
|
|
1731
1823
|
|
|
1732
1824
|
/**
|
|
1733
|
-
* Show help for the add command
|
|
1825
|
+
* Show help for the add command.
|
|
1826
|
+
*
|
|
1827
|
+
* ⚠️ CURRENTLY UNREACHABLE, and the second copy of this text is the cost.
|
|
1828
|
+
* `src/index.js` short-circuits ANY `--help` in the args before dispatching
|
|
1829
|
+
* (its comment says why — `deploy --help` used to open a browser), and `add`
|
|
1830
|
+
* has a dedicated block in that file's HELP map, so `printCommandHelp('add')`
|
|
1831
|
+
* always wins and the `args[0] === '--help'` guard above never fires.
|
|
1832
|
+
*
|
|
1833
|
+
* ⛔ Which means a flag documented here and not there is documented nowhere.
|
|
1834
|
+
* Measured 2026-09-16, when `--starter` was added to both: this copy's
|
|
1835
|
+
* unescaped backticks around \`content:\` closed the template literal and broke
|
|
1836
|
+
* the WHOLE module — every `uniweb add` subcommand failed with "missing ) after
|
|
1837
|
+
* argument list" — and `uniweb add --help` printed correctly throughout,
|
|
1838
|
+
* because it never reaches this function.
|
|
1734
1839
|
*/
|
|
1735
1840
|
function showAddHelp() {
|
|
1736
1841
|
log(`
|
|
@@ -1762,6 +1867,13 @@ ${colors.bright}Extension Options:${colors.reset}
|
|
|
1762
1867
|
|
|
1763
1868
|
${colors.bright}Section Options:${colors.reset}
|
|
1764
1869
|
--foundation <n> Foundation to add section to (prompted if multiple exist)
|
|
1870
|
+
--starter Generate starter content from the section's \`content:\`
|
|
1871
|
+
declaration — what an author would begin editing. Works on
|
|
1872
|
+
a section that already exists (nothing is written), and on
|
|
1873
|
+
a new one (the scaffold gets a matching declaration)
|
|
1874
|
+
--preset <name> Frontmatter the starter content with this preset's params
|
|
1875
|
+
--write <file> Write the starter markdown to a file instead of printing it
|
|
1876
|
+
--json Emit the content structure and ProseMirror doc instead
|
|
1765
1877
|
|
|
1766
1878
|
${colors.bright}CI Options:${colors.reset}
|
|
1767
1879
|
--host <name> github-pages | cloudflare-pages | netlify | vercel
|
|
@@ -1790,6 +1902,10 @@ ${colors.bright}Examples:${colors.reset}
|
|
|
1790
1902
|
uniweb add extension effects --site site # Create ./extensions/effects/
|
|
1791
1903
|
uniweb add section Hero # Create Hero section type
|
|
1792
1904
|
uniweb add section Hero --foundation ui # Target specific foundation
|
|
1905
|
+
uniweb add section Hero --starter # Starter content for an existing Hero
|
|
1906
|
+
uniweb add section Pricing --starter # Scaffold Pricing + content that fills it
|
|
1907
|
+
uniweb add section Hero --starter --preset split # Frontmatter it with the 'split' preset
|
|
1908
|
+
uniweb add section Hero --starter --json # The structure + ProseMirror, for a script
|
|
1793
1909
|
uniweb add foundation --project docs # Create ./docs/foundation/ (co-located)
|
|
1794
1910
|
uniweb add site --project docs # Create ./docs/site/ (co-located)
|
|
1795
1911
|
uniweb add ci # Pick a host, add a deploy workflow
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Starter content for a section type — `uniweb add section <Name> --starter`.
|
|
3
|
+
*
|
|
4
|
+
* ⭐ IT IS DERIVED FROM `content:`, NOT AUTHORED. A developer declares what
|
|
5
|
+
* their component expects; this turns that declaration into something an author
|
|
6
|
+
* can edit instead of an empty box. There is no `starter:` key in `meta.js` and
|
|
7
|
+
* there should not be [Diego, 2026-09-16] — authored sample copy drifts against
|
|
8
|
+
* the declaration it is meant to match, and it can never be localized, because a
|
|
9
|
+
* developer's string is the foundation's own words and is shown verbatim in
|
|
10
|
+
* every UI language.
|
|
11
|
+
*
|
|
12
|
+
* ⚖️ THE GENERATOR IS NOT HERE. It is `@uniweb/schemas/starter`, because the
|
|
13
|
+
* visual editor is its other caller and must reach it in a browser — this file
|
|
14
|
+
* is the CLI's rendering of the same answer. Anything that decides WHAT the
|
|
15
|
+
* content is belongs there; what is left here is reading a `meta.js` off disk
|
|
16
|
+
* and serializing.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { existsSync } from 'node:fs'
|
|
20
|
+
import { readFile, writeFile } from 'node:fs/promises'
|
|
21
|
+
import { join, dirname } from 'node:path'
|
|
22
|
+
import { pathToFileURL } from 'node:url'
|
|
23
|
+
import { createHash } from 'node:crypto'
|
|
24
|
+
|
|
25
|
+
const colors = {
|
|
26
|
+
reset: '\x1b[0m',
|
|
27
|
+
bright: '\x1b[1m',
|
|
28
|
+
dim: '\x1b[2m',
|
|
29
|
+
cyan: '\x1b[36m',
|
|
30
|
+
yellow: '\x1b[33m',
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Load a section's `meta.js` as it is on disk NOW.
|
|
35
|
+
*
|
|
36
|
+
* ⛔ The URL carries the file's content hash. Node caches an ES module by URL
|
|
37
|
+
* for the life of the process, and `@uniweb/build`'s own loader hit exactly this
|
|
38
|
+
* — a dev server regenerating after a `meta.js` edit re-imported the file it had
|
|
39
|
+
* started with. Same rule here, same reason.
|
|
40
|
+
*/
|
|
41
|
+
async function loadMeta(metaPath) {
|
|
42
|
+
const version = createHash('sha1').update(await readFile(metaPath)).digest('hex').slice(0, 16)
|
|
43
|
+
const mod = await import(`${pathToFileURL(metaPath).href}?content=${version}`)
|
|
44
|
+
return mod.default
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The three framework functions this composes, imported at call time.
|
|
49
|
+
*
|
|
50
|
+
* ⚖️ Dynamic, matching `inspect.js`: these resolve through the project's own
|
|
51
|
+
* workspace, and a clear message beats a stack trace when one is missing.
|
|
52
|
+
*/
|
|
53
|
+
async function loadPipeline() {
|
|
54
|
+
try {
|
|
55
|
+
const [schemas, content, parser, writer] = await Promise.all([
|
|
56
|
+
import('@uniweb/schemas/starter'),
|
|
57
|
+
import('@uniweb/schemas/content'),
|
|
58
|
+
import('@uniweb/semantic-parser'),
|
|
59
|
+
import('@uniweb/content-writer'),
|
|
60
|
+
])
|
|
61
|
+
return {
|
|
62
|
+
starterContent: schemas.starterContent,
|
|
63
|
+
describeContent: content.describeContent,
|
|
64
|
+
buildDoc: parser.buildDoc,
|
|
65
|
+
serializeSection: writer.serializeSection,
|
|
66
|
+
}
|
|
67
|
+
} catch (err) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`Starter content needs @uniweb/schemas, @uniweb/semantic-parser and @uniweb/content-writer.\n` +
|
|
70
|
+
` ${err.message}\n` +
|
|
71
|
+
` Run your package manager's install in this workspace and try again.`,
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Generate starter content for one section type and render it.
|
|
78
|
+
*
|
|
79
|
+
* @param {object} args
|
|
80
|
+
* @param {string} args.name - the section type (PascalCase).
|
|
81
|
+
* @param {string} args.sectionDir - the section's directory in the foundation.
|
|
82
|
+
* @param {string} [args.preset] - a declared preset whose params become the frontmatter.
|
|
83
|
+
* @param {boolean} [args.json] - emit the structure and the ProseMirror doc instead of markdown.
|
|
84
|
+
* @param {string} [args.write] - write the markdown to this path instead of printing it.
|
|
85
|
+
* @returns {Promise<{markdown: string, result: object}>}
|
|
86
|
+
*/
|
|
87
|
+
export async function generateStarter({ name, sectionDir, preset, json, write }) {
|
|
88
|
+
const { starterContent, describeContent, buildDoc, serializeSection } = await loadPipeline()
|
|
89
|
+
|
|
90
|
+
const metaPath = join(sectionDir, 'meta.js')
|
|
91
|
+
const meta = existsSync(metaPath) ? await loadMeta(metaPath) : {}
|
|
92
|
+
const result = starterContent({ name, ...meta }, { preset })
|
|
93
|
+
|
|
94
|
+
const doc = buildDoc(result.content)
|
|
95
|
+
const markdown = doc ? serializeSection(result.params, doc) : ''
|
|
96
|
+
|
|
97
|
+
if (json) {
|
|
98
|
+
process.stdout.write(
|
|
99
|
+
JSON.stringify(
|
|
100
|
+
{
|
|
101
|
+
section: name,
|
|
102
|
+
family: result.family,
|
|
103
|
+
elementsInferred: result.elementsInferred,
|
|
104
|
+
unfilled: result.unfilled,
|
|
105
|
+
// What the component SAYS it expects, parsed — the same structure an
|
|
106
|
+
// editor renders a "what does this section want?" panel from. Carried
|
|
107
|
+
// here so the declaration and what was generated from it can be read
|
|
108
|
+
// side by side, without the app.
|
|
109
|
+
expects: describeContent({ name, ...meta }),
|
|
110
|
+
params: result.params,
|
|
111
|
+
content: result.content,
|
|
112
|
+
doc,
|
|
113
|
+
},
|
|
114
|
+
null,
|
|
115
|
+
2,
|
|
116
|
+
) + '\n',
|
|
117
|
+
)
|
|
118
|
+
return { markdown, result }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (write) {
|
|
122
|
+
await writeFile(write, markdown, 'utf-8')
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { markdown, result }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The human-facing report that follows generation.
|
|
130
|
+
*
|
|
131
|
+
* ⭐ It SAYS when the element list was ours. A component declaring no `content:`
|
|
132
|
+
* gets its family's canonical set, and a developer reading generated content
|
|
133
|
+
* they never specified should be told why — otherwise the natural conclusion is
|
|
134
|
+
* that the generator invented a declaration on their behalf.
|
|
135
|
+
*/
|
|
136
|
+
export function reportStarter(result, { write } = {}) {
|
|
137
|
+
const c = colors
|
|
138
|
+
const fam = result.family.id
|
|
139
|
+
? `${c.cyan}${result.family.id}${c.reset} ${c.dim}(${result.family.source})${c.reset}`
|
|
140
|
+
: `${c.dim}no family — generic copy${c.reset}`
|
|
141
|
+
|
|
142
|
+
console.log('')
|
|
143
|
+
console.log(` ${c.dim}family:${c.reset} ${fam}`)
|
|
144
|
+
console.log(` ${c.dim}slots:${c.reset} ${Object.keys(result.content).join(', ') || '—'}`)
|
|
145
|
+
|
|
146
|
+
if (result.elementsInferred) {
|
|
147
|
+
console.log('')
|
|
148
|
+
console.log(
|
|
149
|
+
` ${c.yellow}!${c.reset} This section declares no ${c.bright}content:${c.reset} — the elements above came from its family.`,
|
|
150
|
+
)
|
|
151
|
+
console.log(
|
|
152
|
+
` ${c.dim}Declare what the component expects and the starter content follows it instead.${c.reset}`,
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (result.unfilled.length) {
|
|
157
|
+
console.log('')
|
|
158
|
+
console.log(` ${c.yellow}!${c.reset} Not filled: ${result.unfilled.join(', ')}`)
|
|
159
|
+
console.log(
|
|
160
|
+
` ${c.dim}\`background\` is frontmatter, not content. A video needs an address we cannot invent.${c.reset}`,
|
|
161
|
+
)
|
|
162
|
+
console.log(
|
|
163
|
+
` ${c.dim}A \`data\` block needs a schema: a @/ ref resolves at build, and an empty {} declares no shape.${c.reset}`,
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (write) {
|
|
168
|
+
console.log('')
|
|
169
|
+
console.log(` ${c.cyan}→${c.reset} written to ${write}`)
|
|
170
|
+
}
|
|
171
|
+
console.log('')
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* A `content:` declaration for a section being scaffolded, derived from the
|
|
176
|
+
* family the name resolves to. Written into the new `meta.js` so the scaffold is
|
|
177
|
+
* coherent: a declaration, a component, and content that fills it.
|
|
178
|
+
*/
|
|
179
|
+
export async function declarationFor(result) {
|
|
180
|
+
const LABELS = {
|
|
181
|
+
title: 'Headline',
|
|
182
|
+
pretitle: 'Small label above the headline',
|
|
183
|
+
subtitle: 'Secondary headline',
|
|
184
|
+
paragraphs: 'Body copy [1-2]',
|
|
185
|
+
links: 'Calls to action [0-2]',
|
|
186
|
+
lists: 'Bullet points [0-1]',
|
|
187
|
+
items: 'One per entry [3-6]',
|
|
188
|
+
images: 'Image [1]',
|
|
189
|
+
icons: 'Icon [1]',
|
|
190
|
+
videos: 'Video [1]',
|
|
191
|
+
snippets: 'Code sample [1]',
|
|
192
|
+
data: 'The data block the author writes',
|
|
193
|
+
}
|
|
194
|
+
// ⛔ The slot → declaration spelling comes from @uniweb/schemas, not from a
|
|
195
|
+
// copy here. `image` declares and `images` delivers; that mapping is the
|
|
196
|
+
// generator's, and a second copy rots the moment a row is added there.
|
|
197
|
+
const { declarationKey } = await import('@uniweb/schemas/starter')
|
|
198
|
+
const lines = Object.keys(result.content).map(
|
|
199
|
+
(slot) => ` ${declarationKey(slot)}: '${LABELS[slot] || slot}',`,
|
|
200
|
+
)
|
|
201
|
+
return lines.length ? `\n content: {\n${lines.join('\n')}\n },\n` : ''
|
|
202
|
+
}
|
package/src/framework-index.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-09-
|
|
3
|
+
"generatedAt": "2026-09-16T20:35:26.755Z",
|
|
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.5",
|
|
14
14
|
"path": "framework/build",
|
|
15
15
|
"deps": [
|
|
16
16
|
"@uniweb/content-reader",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
]
|
|
55
55
|
},
|
|
56
56
|
"@uniweb/kit": {
|
|
57
|
-
"version": "0.19.
|
|
57
|
+
"version": "0.19.3",
|
|
58
58
|
"path": "framework/kit",
|
|
59
59
|
"deps": [
|
|
60
60
|
"@uniweb/core",
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
"deps": []
|
|
96
96
|
},
|
|
97
97
|
"@uniweb/schemas": {
|
|
98
|
-
"version": "0.
|
|
98
|
+
"version": "0.3.1",
|
|
99
99
|
"path": "framework/schemas",
|
|
100
100
|
"deps": []
|
|
101
101
|
},
|
|
@@ -105,12 +105,12 @@
|
|
|
105
105
|
"deps": []
|
|
106
106
|
},
|
|
107
107
|
"@uniweb/semantic-parser": {
|
|
108
|
-
"version": "1.4.
|
|
108
|
+
"version": "1.4.1",
|
|
109
109
|
"path": "framework/semantic-parser",
|
|
110
110
|
"deps": []
|
|
111
111
|
},
|
|
112
112
|
"@uniweb/templates": {
|
|
113
|
-
"version": "0.14.
|
|
113
|
+
"version": "0.14.5",
|
|
114
114
|
"path": "framework/templates",
|
|
115
115
|
"deps": []
|
|
116
116
|
},
|
package/src/index.js
CHANGED
|
@@ -1505,7 +1505,7 @@ ${colors.bright}Subcommands:${colors.reset}
|
|
|
1505
1505
|
add foundation [name] Add a foundation (--from, --path, --project)
|
|
1506
1506
|
add site [name] Add a site (--from, --foundation, --path, --project)
|
|
1507
1507
|
add extension <name> Add an extension (--from, --site, --path)
|
|
1508
|
-
add section <name> Add a section type to a foundation (--foundation)
|
|
1508
|
+
add section <name> Add a section type to a foundation (--foundation, --starter)
|
|
1509
1509
|
add ci Add a CI workflow so every push deploys (--host, --target)
|
|
1510
1510
|
|
|
1511
1511
|
${colors.bright}Common options:${colors.reset}
|
|
@@ -1514,6 +1514,15 @@ ${colors.bright}Common options:${colors.reset}
|
|
|
1514
1514
|
--foundation <name> Wire site/extension to this foundation (CI-friendly)
|
|
1515
1515
|
--site <name> Wire extension to this site (CI-friendly)
|
|
1516
1516
|
--non-interactive Fail with usage info instead of prompting
|
|
1517
|
+
|
|
1518
|
+
${colors.bright}Starter content (add section):${colors.reset}
|
|
1519
|
+
--starter Generate starter content from the section's
|
|
1520
|
+
\`content:\` declaration — what an author would begin
|
|
1521
|
+
editing. Works on an existing section (writes nothing)
|
|
1522
|
+
and on a new one (the scaffold gets a declaration)
|
|
1523
|
+
--preset <name> Frontmatter it with this preset's params
|
|
1524
|
+
--write <file> Write the markdown to a file instead of printing
|
|
1525
|
+
--json Emit the structure + ProseMirror doc instead
|
|
1517
1526
|
`,
|
|
1518
1527
|
export: `
|
|
1519
1528
|
${colors.cyan}${colors.bright}uniweb export${colors.reset} ${colors.dim}— Export a self-contained site for third-party hosting${colors.reset}
|
|
@@ -1888,7 +1897,7 @@ ${colors.bright}Add Subcommands:${colors.reset}
|
|
|
1888
1897
|
add foundation [name] Add a foundation (--from, --path, --project)
|
|
1889
1898
|
add site [name] Add a site (--from, --foundation, --path, --project)
|
|
1890
1899
|
add extension <name> Add an extension (--from, --site, --path)
|
|
1891
|
-
add section <name> Add a section type to a foundation (--foundation)
|
|
1900
|
+
add section <name> Add a section type to a foundation (--foundation, --starter)
|
|
1892
1901
|
add ci Add a CI workflow so every push deploys (--host, --target)
|
|
1893
1902
|
|
|
1894
1903
|
${colors.bright}Global Options:${colors.reset}
|
package/src/utils/scaffold.js
CHANGED
|
@@ -122,7 +122,16 @@ export async function scaffoldSite(targetDir, context, options = {}) {
|
|
|
122
122
|
registerVersions(getVersionsForTemplates())
|
|
123
123
|
|
|
124
124
|
const templatePath = join(TEMPLATES_DIR, 'site')
|
|
125
|
-
|
|
125
|
+
// A ref is written as a YAML scalar, not pasted as text: a scoped ref
|
|
126
|
+
// (`@acme/base@1.0.0`, `@acme/marketing`) starts with `@`, which plain YAML
|
|
127
|
+
// reserves, so the file would not parse — and every reader that swallows
|
|
128
|
+
// the parse error then reports the site as having no `$uuid`. js-yaml quotes
|
|
129
|
+
// only when needed, so `src` stays `src`. Rendered with `{{{ }}}`, because
|
|
130
|
+
// Handlebars' HTML escaping would also rewrite a URL's `&` and `=`.
|
|
131
|
+
const siteContext = context.foundationRef
|
|
132
|
+
? { ...context, foundationRefYaml: yaml.dump(context.foundationRef, { lineWidth: -1 }).trim() }
|
|
133
|
+
: context
|
|
134
|
+
await copyTemplateDirectory(templatePath, targetDir, siteContext, {
|
|
126
135
|
onProgress: options.onProgress,
|
|
127
136
|
onWarning: options.onWarning
|
|
128
137
|
})
|