uniweb 0.56.5 → 0.56.7
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 +7 -6
- package/partials/agents.md +5 -4
- package/src/commands/add.js +99 -83
- package/src/commands/starter.js +202 -0
- package/src/framework-index.json +7 -6
- package/src/index.js +75 -15
- 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.7",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -42,15 +42,16 @@
|
|
|
42
42
|
"prompts": "^2.4.2",
|
|
43
43
|
"tar": "^7.0.0",
|
|
44
44
|
"@uniweb/core": "^0.29.4",
|
|
45
|
-
"@uniweb/kit": "^0.19.2",
|
|
46
45
|
"@uniweb/runtime": "^0.26.4",
|
|
47
|
-
"@uniweb/schemas": "^0.3.
|
|
48
|
-
"@uniweb/
|
|
46
|
+
"@uniweb/schemas": "^0.3.1",
|
|
47
|
+
"@uniweb/content-writer": "^0.3.4",
|
|
48
|
+
"@uniweb/kit": "^0.19.3",
|
|
49
|
+
"@uniweb/semantic-parser": "^1.4.1"
|
|
49
50
|
},
|
|
50
51
|
"peerDependencies": {
|
|
51
|
-
"@uniweb/build": "^0.52.
|
|
52
|
+
"@uniweb/build": "^0.52.6",
|
|
52
53
|
"@uniweb/content-reader": "^1.2.5",
|
|
53
|
-
"@uniweb/semantic-parser": "^1.4.
|
|
54
|
+
"@uniweb/semantic-parser": "^1.4.1"
|
|
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
|
|
|
@@ -182,11 +209,6 @@ export async function add(rawArgs) {
|
|
|
182
209
|
const nonInteractive = isNonInteractive(rawArgs)
|
|
183
210
|
const args = stripNonInteractiveFlag(rawArgs)
|
|
184
211
|
|
|
185
|
-
if (args[0] === '--help' || args[0] === '-h') {
|
|
186
|
-
showAddHelp()
|
|
187
|
-
return
|
|
188
|
-
}
|
|
189
|
-
|
|
190
212
|
const pm = detectPackageManager()
|
|
191
213
|
const prefix = getCliPrefix()
|
|
192
214
|
|
|
@@ -1074,6 +1096,7 @@ async function wireExtensionToSite(
|
|
|
1074
1096
|
* Add a section type to a foundation
|
|
1075
1097
|
*/
|
|
1076
1098
|
async function addSection(rootDir, opts) {
|
|
1099
|
+
setAddJsonMode(opts.json)
|
|
1077
1100
|
let name = opts.name
|
|
1078
1101
|
|
|
1079
1102
|
// Interactive name prompt when not provided
|
|
@@ -1169,10 +1192,31 @@ async function addSection(rootDir, opts) {
|
|
|
1169
1192
|
const sectionDir = join(sectionsDir, name)
|
|
1170
1193
|
const relSectionPath = relative(foundationDir, sectionDir)
|
|
1171
1194
|
|
|
1195
|
+
// ⭐ `--starter` ON AN EXISTING SECTION IS NOT AN ERROR. The flag asks one
|
|
1196
|
+
// question — *what content would an author start this section with?* — and a
|
|
1197
|
+
// section type that already exists is the case where it has a real `content:`
|
|
1198
|
+
// declaration to answer from. Refusing here would make the flag testable only
|
|
1199
|
+
// against stubs, which is the one case where the answer is least interesting.
|
|
1200
|
+
// Nothing is scaffolded and nothing is overwritten on this path.
|
|
1172
1201
|
if (existsSync(sectionDir)) {
|
|
1202
|
+
if (opts.starter) {
|
|
1203
|
+
const { markdown, result } = await generateStarter({
|
|
1204
|
+
name,
|
|
1205
|
+
sectionDir,
|
|
1206
|
+
preset: opts.preset,
|
|
1207
|
+
json: opts.json,
|
|
1208
|
+
write: opts.write,
|
|
1209
|
+
})
|
|
1210
|
+
if (!opts.json) {
|
|
1211
|
+
if (!opts.write) log('\n' + markdown.trimEnd())
|
|
1212
|
+
reportStarter(result, { write: opts.write })
|
|
1213
|
+
}
|
|
1214
|
+
return
|
|
1215
|
+
}
|
|
1173
1216
|
error(
|
|
1174
1217
|
`Section '${name}' already exists at ${foundation.path}/${relSectionPath}/`
|
|
1175
1218
|
)
|
|
1219
|
+
log(` ${colors.dim}--starter generates starter content for it without touching the files.${colors.reset}`)
|
|
1176
1220
|
process.exit(1)
|
|
1177
1221
|
}
|
|
1178
1222
|
|
|
@@ -1207,9 +1251,21 @@ export default function ${name}({ content, params }) {
|
|
|
1207
1251
|
}
|
|
1208
1252
|
`
|
|
1209
1253
|
|
|
1254
|
+
// With `--starter`, the scaffold gets a `content:` declaration derived from
|
|
1255
|
+
// the family the name resolves to — so the three pieces agree: a declaration,
|
|
1256
|
+
// a component that reads it, and content that fills it. Without the flag the
|
|
1257
|
+
// stub declares nothing, exactly as before.
|
|
1258
|
+
let starterDeclaration = ''
|
|
1259
|
+
let starterResult = null
|
|
1260
|
+
if (opts.starter) {
|
|
1261
|
+
const preview = await generateStarter({ name, sectionDir: sectionDir })
|
|
1262
|
+
starterResult = preview.result
|
|
1263
|
+
starterDeclaration = await declarationFor(preview.result)
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1210
1266
|
const metaContent = `export default {
|
|
1211
1267
|
title: '${name}',
|
|
1212
|
-
description: ''
|
|
1268
|
+
description: '',${starterDeclaration}
|
|
1213
1269
|
params: {},
|
|
1214
1270
|
}
|
|
1215
1271
|
`
|
|
@@ -1230,6 +1286,37 @@ export default function ${name}({ content, params }) {
|
|
|
1230
1286
|
`${colors.dim}The dev server will pick it up automatically.${colors.reset}`
|
|
1231
1287
|
)
|
|
1232
1288
|
}
|
|
1289
|
+
|
|
1290
|
+
if (starterResult) {
|
|
1291
|
+
// Regenerate against the declaration just written, so what is printed is
|
|
1292
|
+
// what this section's own `meta.js` now asks for rather than the family's
|
|
1293
|
+
// guess — the two agree here, and saying it from the file keeps them so.
|
|
1294
|
+
const { markdown, result } = await generateStarter({
|
|
1295
|
+
name,
|
|
1296
|
+
sectionDir,
|
|
1297
|
+
preset: opts.preset,
|
|
1298
|
+
json: opts.json,
|
|
1299
|
+
write: opts.write,
|
|
1300
|
+
})
|
|
1301
|
+
if (!opts.json) {
|
|
1302
|
+
log('')
|
|
1303
|
+
log(`${colors.dim}Starter content for a page section:${colors.reset}`)
|
|
1304
|
+
if (!opts.write) log('\n' + markdown.trimEnd())
|
|
1305
|
+
reportStarter(result, { write: opts.write })
|
|
1306
|
+
// The stub component reads three elements. The declaration above may name
|
|
1307
|
+
// more, because it comes from the family rather than from the stub — say
|
|
1308
|
+
// so, rather than leave a developer wondering why half the content they
|
|
1309
|
+
// were just handed renders as nothing.
|
|
1310
|
+
const stubReads = new Set(['title', 'paragraphs', 'links'])
|
|
1311
|
+
const unread = Object.keys(result.content).filter((slot) => !stubReads.has(slot))
|
|
1312
|
+
if (unread.length) {
|
|
1313
|
+
log(
|
|
1314
|
+
` ${colors.dim}index.jsx reads title, paragraphs and links — extend it for: ${unread.join(', ')}${colors.reset}`
|
|
1315
|
+
)
|
|
1316
|
+
log('')
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1233
1320
|
}
|
|
1234
1321
|
|
|
1235
1322
|
/**
|
|
@@ -1729,74 +1816,3 @@ function parseNodeMajor(engines) {
|
|
|
1729
1816
|
return match ? match[1] : null
|
|
1730
1817
|
}
|
|
1731
1818
|
|
|
1732
|
-
/**
|
|
1733
|
-
* Show help for the add command
|
|
1734
|
-
*/
|
|
1735
|
-
function showAddHelp() {
|
|
1736
|
-
log(`
|
|
1737
|
-
${colors.cyan}${colors.bright}Uniweb Add${colors.reset}
|
|
1738
|
-
|
|
1739
|
-
Add projects, foundations, sites, extensions, section types, or CI workflows to your workspace.
|
|
1740
|
-
|
|
1741
|
-
${colors.bright}Usage:${colors.reset}
|
|
1742
|
-
uniweb add project [name] [options]
|
|
1743
|
-
uniweb add foundation [name] [options]
|
|
1744
|
-
uniweb add site [name] [options]
|
|
1745
|
-
uniweb add extension <name> [options]
|
|
1746
|
-
uniweb add section <name> [options]
|
|
1747
|
-
uniweb add ci [options]
|
|
1748
|
-
|
|
1749
|
-
${colors.bright}Common Options:${colors.reset}
|
|
1750
|
-
--from <template> Apply content from a template after scaffolding
|
|
1751
|
-
--path <dir> Custom directory for the package
|
|
1752
|
-
|
|
1753
|
-
${colors.bright}Foundation Options:${colors.reset}
|
|
1754
|
-
--project <name> Group under a project directory (co-located layout)
|
|
1755
|
-
|
|
1756
|
-
${colors.bright}Site Options:${colors.reset}
|
|
1757
|
-
--foundation <n> Foundation to wire to (prompted if multiple exist)
|
|
1758
|
-
--project <name> Group under a project directory (co-located layout)
|
|
1759
|
-
|
|
1760
|
-
${colors.bright}Extension Options:${colors.reset}
|
|
1761
|
-
--site <name> Site to wire extension URL into
|
|
1762
|
-
|
|
1763
|
-
${colors.bright}Section Options:${colors.reset}
|
|
1764
|
-
--foundation <n> Foundation to add section to (prompted if multiple exist)
|
|
1765
|
-
|
|
1766
|
-
${colors.bright}CI Options:${colors.reset}
|
|
1767
|
-
--host <name> github-pages | cloudflare-pages | netlify | vercel
|
|
1768
|
-
(prompted when omitted)
|
|
1769
|
-
--target <what> site (default) | foundation
|
|
1770
|
-
'foundation' publishes built foundations at permanent
|
|
1771
|
-
versioned URLs — the free alternative to the catalog
|
|
1772
|
-
--site <name> Site the workflow builds (prompted if multiple exist)
|
|
1773
|
-
--foundation <name> With --target foundation: publish just this one
|
|
1774
|
-
--domain <host> Custom domain (GitHub Pages: writes CNAME, serves at root)
|
|
1775
|
-
--project-name <name> Name to register under on the host (default: the
|
|
1776
|
-
workspace name; Cloudflare Pages project, etc.)
|
|
1777
|
-
--no-previews Skip the per-PR preview workflow
|
|
1778
|
-
--force Overwrite an existing workflow file
|
|
1779
|
-
|
|
1780
|
-
${colors.dim}Hosts that support PR previews: cloudflare-pages, netlify, vercel.
|
|
1781
|
-
GitHub Pages has no preview environment, so it scaffolds a deploy workflow only.${colors.reset}
|
|
1782
|
-
|
|
1783
|
-
${colors.bright}Examples:${colors.reset}
|
|
1784
|
-
uniweb add project docs # Create docs/foundation/ + docs/site/
|
|
1785
|
-
uniweb add project docs --from academic # Co-located pair + academic content
|
|
1786
|
-
uniweb add foundation # Create ./foundation/ at root
|
|
1787
|
-
uniweb add foundation ui # Create ./foundations/ui/
|
|
1788
|
-
uniweb add site # Create ./site/ at root
|
|
1789
|
-
uniweb add site blog --foundation marketing # Create ./sites/blog/ wired to marketing
|
|
1790
|
-
uniweb add extension effects --site site # Create ./extensions/effects/
|
|
1791
|
-
uniweb add section Hero # Create Hero section type
|
|
1792
|
-
uniweb add section Hero --foundation ui # Target specific foundation
|
|
1793
|
-
uniweb add foundation --project docs # Create ./docs/foundation/ (co-located)
|
|
1794
|
-
uniweb add site --project docs # Create ./docs/site/ (co-located)
|
|
1795
|
-
uniweb add ci # Pick a host, add a deploy workflow
|
|
1796
|
-
uniweb add ci --host github-pages --site marketing # Pick host + site explicitly
|
|
1797
|
-
uniweb add ci --host netlify # Deploy + PR-preview workflows
|
|
1798
|
-
uniweb add ci --host vercel --no-previews # Deploy workflow only
|
|
1799
|
-
uniweb add ci --domain mysite.com # Custom domain → writes CNAME + UNIWEB_BASE=/
|
|
1800
|
-
uniweb add ci --target foundation # Publish foundations at versioned URLs
|
|
1801
|
-
`)
|
|
1802
|
-
}
|
|
@@ -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:57:12.370Z",
|
|
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.6",
|
|
14
14
|
"path": "framework/build",
|
|
15
15
|
"deps": [
|
|
16
16
|
"@uniweb/content-reader",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"@uniweb/core",
|
|
19
19
|
"@uniweb/projections",
|
|
20
20
|
"@uniweb/runtime",
|
|
21
|
+
"@uniweb/runtime",
|
|
21
22
|
"@uniweb/schemas",
|
|
22
23
|
"@uniweb/semantic-parser",
|
|
23
24
|
"@uniweb/theming"
|
|
@@ -54,7 +55,7 @@
|
|
|
54
55
|
]
|
|
55
56
|
},
|
|
56
57
|
"@uniweb/kit": {
|
|
57
|
-
"version": "0.19.
|
|
58
|
+
"version": "0.19.3",
|
|
58
59
|
"path": "framework/kit",
|
|
59
60
|
"deps": [
|
|
60
61
|
"@uniweb/core",
|
|
@@ -95,7 +96,7 @@
|
|
|
95
96
|
"deps": []
|
|
96
97
|
},
|
|
97
98
|
"@uniweb/schemas": {
|
|
98
|
-
"version": "0.3.
|
|
99
|
+
"version": "0.3.1",
|
|
99
100
|
"path": "framework/schemas",
|
|
100
101
|
"deps": []
|
|
101
102
|
},
|
|
@@ -105,12 +106,12 @@
|
|
|
105
106
|
"deps": []
|
|
106
107
|
},
|
|
107
108
|
"@uniweb/semantic-parser": {
|
|
108
|
-
"version": "1.4.
|
|
109
|
+
"version": "1.4.1",
|
|
109
110
|
"path": "framework/semantic-parser",
|
|
110
111
|
"deps": []
|
|
111
112
|
},
|
|
112
113
|
"@uniweb/templates": {
|
|
113
|
-
"version": "0.14.
|
|
114
|
+
"version": "0.14.5",
|
|
114
115
|
"path": "framework/templates",
|
|
115
116
|
"deps": []
|
|
116
117
|
},
|
package/src/index.js
CHANGED
|
@@ -1498,22 +1498,82 @@ ${colors.bright}Options:${colors.reset}
|
|
|
1498
1498
|
--platform <name> (Deprecated alias for --host)
|
|
1499
1499
|
`,
|
|
1500
1500
|
add: `
|
|
1501
|
-
${colors.cyan}${colors.bright}
|
|
1501
|
+
${colors.cyan}${colors.bright}Uniweb Add${colors.reset}
|
|
1502
1502
|
|
|
1503
|
-
|
|
1504
|
-
add project [name] Add a co-located foundation + site pair
|
|
1505
|
-
add foundation [name] Add a foundation (--from, --path, --project)
|
|
1506
|
-
add site [name] Add a site (--from, --foundation, --path, --project)
|
|
1507
|
-
add extension <name> Add an extension (--from, --site, --path)
|
|
1508
|
-
add section <name> Add a section type to a foundation (--foundation)
|
|
1509
|
-
add ci Add a CI workflow so every push deploys (--host, --target)
|
|
1503
|
+
Add projects, foundations, sites, extensions, section types, or CI workflows to your workspace.
|
|
1510
1504
|
|
|
1511
|
-
${colors.bright}
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1505
|
+
${colors.bright}Usage:${colors.reset}
|
|
1506
|
+
uniweb add project [name] [options]
|
|
1507
|
+
uniweb add foundation [name] [options]
|
|
1508
|
+
uniweb add site [name] [options]
|
|
1509
|
+
uniweb add extension <name> [options]
|
|
1510
|
+
uniweb add section <name> [options]
|
|
1511
|
+
uniweb add ci [options]
|
|
1512
|
+
|
|
1513
|
+
${colors.bright}Common Options:${colors.reset}
|
|
1514
|
+
--from <template> Apply content from a template after scaffolding
|
|
1515
|
+
--path <dir> Custom directory for the package
|
|
1516
|
+
--non-interactive Fail with usage info instead of prompting
|
|
1517
|
+
|
|
1518
|
+
${colors.bright}Foundation Options:${colors.reset}
|
|
1519
|
+
--project <name> Group under a project directory (co-located layout)
|
|
1520
|
+
|
|
1521
|
+
${colors.bright}Site Options:${colors.reset}
|
|
1522
|
+
--foundation <n> Foundation to wire to (prompted if multiple exist)
|
|
1523
|
+
--project <name> Group under a project directory (co-located layout)
|
|
1524
|
+
|
|
1525
|
+
${colors.bright}Extension Options:${colors.reset}
|
|
1526
|
+
--site <name> Site to wire extension URL into
|
|
1527
|
+
|
|
1528
|
+
${colors.bright}Section Options:${colors.reset}
|
|
1529
|
+
--foundation <n> Foundation to add section to (prompted if multiple exist)
|
|
1530
|
+
--starter Generate starter content from the section's \`content:\`
|
|
1531
|
+
declaration — what an author would begin editing. Works on
|
|
1532
|
+
a section that already exists (nothing is written), and on
|
|
1533
|
+
a new one (the scaffold gets a matching declaration)
|
|
1534
|
+
--preset <name> Frontmatter the starter content with this preset's params
|
|
1535
|
+
--write <file> Write the starter markdown to a file instead of printing it
|
|
1536
|
+
--json Emit the content structure and ProseMirror doc instead
|
|
1537
|
+
|
|
1538
|
+
${colors.bright}CI Options:${colors.reset}
|
|
1539
|
+
--host <name> github-pages | cloudflare-pages | netlify | vercel
|
|
1540
|
+
(prompted when omitted)
|
|
1541
|
+
--target <what> site (default) | foundation
|
|
1542
|
+
'foundation' publishes built foundations at permanent
|
|
1543
|
+
versioned URLs — the free alternative to the catalog
|
|
1544
|
+
--site <name> Site the workflow builds (prompted if multiple exist)
|
|
1545
|
+
--foundation <name> With --target foundation: publish just this one
|
|
1546
|
+
--domain <host> Custom domain (GitHub Pages: writes CNAME, serves at root)
|
|
1547
|
+
--project-name <name> Name to register under on the host (default: the
|
|
1548
|
+
workspace name; Cloudflare Pages project, etc.)
|
|
1549
|
+
--no-previews Skip the per-PR preview workflow
|
|
1550
|
+
--force Overwrite an existing workflow file
|
|
1551
|
+
|
|
1552
|
+
${colors.dim}Hosts that support PR previews: cloudflare-pages, netlify, vercel.
|
|
1553
|
+
GitHub Pages has no preview environment, so it scaffolds a deploy workflow only.${colors.reset}
|
|
1554
|
+
|
|
1555
|
+
${colors.bright}Examples:${colors.reset}
|
|
1556
|
+
uniweb add project docs # Create docs/foundation/ + docs/site/
|
|
1557
|
+
uniweb add project docs --from academic # Co-located pair + academic content
|
|
1558
|
+
uniweb add foundation # Create ./foundation/ at root
|
|
1559
|
+
uniweb add foundation ui # Create ./foundations/ui/
|
|
1560
|
+
uniweb add site # Create ./site/ at root
|
|
1561
|
+
uniweb add site blog --foundation marketing # Create ./sites/blog/ wired to marketing
|
|
1562
|
+
uniweb add extension effects --site site # Create ./extensions/effects/
|
|
1563
|
+
uniweb add section Hero # Create Hero section type
|
|
1564
|
+
uniweb add section Hero --foundation ui # Target specific foundation
|
|
1565
|
+
uniweb add section Hero --starter # Starter content for an existing Hero
|
|
1566
|
+
uniweb add section Pricing --starter # Scaffold Pricing + content that fills it
|
|
1567
|
+
uniweb add section Hero --starter --preset split # Frontmatter it with the 'split' preset
|
|
1568
|
+
uniweb add section Hero --starter --json # The structure + ProseMirror, for a script
|
|
1569
|
+
uniweb add foundation --project docs # Create ./docs/foundation/ (co-located)
|
|
1570
|
+
uniweb add site --project docs # Create ./docs/site/ (co-located)
|
|
1571
|
+
uniweb add ci # Pick a host, add a deploy workflow
|
|
1572
|
+
uniweb add ci --host github-pages --site marketing # Pick host + site explicitly
|
|
1573
|
+
uniweb add ci --host netlify # Deploy + PR-preview workflows
|
|
1574
|
+
uniweb add ci --host vercel --no-previews # Deploy workflow only
|
|
1575
|
+
uniweb add ci --domain mysite.com # Custom domain → writes CNAME + UNIWEB_BASE=/
|
|
1576
|
+
uniweb add ci --target foundation # Publish foundations at versioned URLs
|
|
1517
1577
|
`,
|
|
1518
1578
|
export: `
|
|
1519
1579
|
${colors.cyan}${colors.bright}uniweb export${colors.reset} ${colors.dim}— Export a self-contained site for third-party hosting${colors.reset}
|
|
@@ -1888,7 +1948,7 @@ ${colors.bright}Add Subcommands:${colors.reset}
|
|
|
1888
1948
|
add foundation [name] Add a foundation (--from, --path, --project)
|
|
1889
1949
|
add site [name] Add a site (--from, --foundation, --path, --project)
|
|
1890
1950
|
add extension <name> Add an extension (--from, --site, --path)
|
|
1891
|
-
add section <name> Add a section type to a foundation (--foundation)
|
|
1951
|
+
add section <name> Add a section type to a foundation (--foundation, --starter)
|
|
1892
1952
|
add ci Add a CI workflow so every push deploys (--host, --target)
|
|
1893
1953
|
|
|
1894
1954
|
${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
|
})
|