uniweb 0.9.3 → 0.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +4 -4
- package/partials/agents.md +125 -1
- package/src/framework-index.json +203 -0
- package/src/templates/resolver.js +27 -22
- package/src/versions.js +77 -12
- package/src/templates/official-templates.snapshot.json +0 -50
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.5",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -42,11 +42,11 @@
|
|
|
42
42
|
"prompts": "^2.4.2",
|
|
43
43
|
"tar": "^7.0.0",
|
|
44
44
|
"@uniweb/core": "0.6.1",
|
|
45
|
-
"@uniweb/runtime": "0.7.
|
|
46
|
-
"@uniweb/kit": "0.8.
|
|
45
|
+
"@uniweb/runtime": "0.7.4",
|
|
46
|
+
"@uniweb/kit": "0.8.2"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"@uniweb/build": "0.9.
|
|
49
|
+
"@uniweb/build": "0.9.4",
|
|
50
50
|
"@uniweb/content-reader": "1.1.4",
|
|
51
51
|
"@uniweb/semantic-parser": "1.1.9"
|
|
52
52
|
},
|
package/partials/agents.md
CHANGED
|
@@ -529,6 +529,44 @@ For the rare case where children should be independent sections with their own t
|
|
|
529
529
|
|
|
530
530
|
**Data and child blocks:** Page-level `data:` is available to all blocks on the page, including children. Each child block resolves data independently through the same page → site hierarchy. If a child component needs data, declare it in the child's `meta.js` or in the child section's frontmatter (`data: articles`).
|
|
531
531
|
|
|
532
|
+
### Dividers — Content Boundaries
|
|
533
|
+
|
|
534
|
+
The `---` (horizontal rule) in markdown creates a boundary between content regions. The developer decides what each region means. Two patterns:
|
|
535
|
+
|
|
536
|
+
**Data-driven iteration (Loom).** Dividers separate header/body/footer in a repeated template. The content handler splits *before* Loom runs because each segment gets a different variable context — the body template contains item-level fields that don't exist on the top-level data. The header and footer are instantiated once against the full data; the body is repeated per data item.
|
|
537
|
+
|
|
538
|
+
```markdown
|
|
539
|
+
---
|
|
540
|
+
type: CvEntry
|
|
541
|
+
source: education
|
|
542
|
+
---
|
|
543
|
+
# Education
|
|
544
|
+
{COUNT OF education} degrees.
|
|
545
|
+
---
|
|
546
|
+
## {degree}
|
|
547
|
+
{institution} — {field} ({start}–{end})
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
The `source` frontmatter param names the data array to iterate. The content handler (see "Content Handlers" below) reads it. A second `---` starts a footer, rendered once after all items.
|
|
551
|
+
|
|
552
|
+
**UI regions (component).** Dividers separate structural areas that the component renders differently — e.g., lesson prose vs challenge content. `splitContent()` from `@uniweb/kit` splits the parsed content at divider elements in the sequence:
|
|
553
|
+
|
|
554
|
+
```jsx
|
|
555
|
+
import { splitContent } from '@uniweb/kit'
|
|
556
|
+
|
|
557
|
+
function Lesson({ content, block }) {
|
|
558
|
+
const [lesson, challenge] = splitContent(content)
|
|
559
|
+
return (
|
|
560
|
+
<div>
|
|
561
|
+
<Prose content={lesson} block={block} />
|
|
562
|
+
<aside><Prose content={challenge} block={block} /></aside>
|
|
563
|
+
</div>
|
|
564
|
+
)
|
|
565
|
+
}
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
**When to use which:** Different data contexts per region → Loom pre-parse split (handled by the content handler). Same data, different UI treatment → kit post-parse split (`splitContent`). A foundation can use both — Loom splits and iterates to produce final content, then the component splits the result to route regions to different UI.
|
|
569
|
+
|
|
532
570
|
### Section Backgrounds
|
|
533
571
|
|
|
534
572
|
Set `background` in frontmatter — the runtime renders it automatically:
|
|
@@ -825,7 +863,7 @@ function MyComponent({ content, params, block }) {
|
|
|
825
863
|
}
|
|
826
864
|
```
|
|
827
865
|
|
|
828
|
-
All non-reserved frontmatter fields become `params`. Reserved: `type`, `preset`, `input`, `data`, `id`, `background`, `theme`. Everything else flows to the component.
|
|
866
|
+
All non-reserved frontmatter fields become `params`. Reserved: `type`, `preset`, `input`, `data`, `id`, `background`, `theme`, `source`, `where`. Everything else flows to the component.
|
|
829
867
|
|
|
830
868
|
### block properties
|
|
831
869
|
|
|
@@ -1386,6 +1424,92 @@ Named subdirectories are self-contained — no inheritance. Layout cascade: `pag
|
|
|
1386
1424
|
|
|
1387
1425
|
---
|
|
1388
1426
|
|
|
1427
|
+
## Content Handlers
|
|
1428
|
+
|
|
1429
|
+
Content handlers are a transform layer that runs between data assembly and the component. They're declared in `foundation.js` and apply to every section in the foundation. The standard content shape (title, paragraphs, items, sequence) is the default — handlers can reshape it.
|
|
1430
|
+
|
|
1431
|
+
### The three hooks
|
|
1432
|
+
|
|
1433
|
+
The foundation declares handlers as an object in its default export:
|
|
1434
|
+
|
|
1435
|
+
```js
|
|
1436
|
+
// foundation.js
|
|
1437
|
+
export default {
|
|
1438
|
+
handlers: {
|
|
1439
|
+
data: (data, block) => { /* ... */ },
|
|
1440
|
+
content: (data, block) => { /* ... */ },
|
|
1441
|
+
props: (content, params, block) => { /* ... */ },
|
|
1442
|
+
},
|
|
1443
|
+
}
|
|
1444
|
+
```
|
|
1445
|
+
|
|
1446
|
+
All three are optional. Each runs per block and is error-isolated (a failing handler logs a warning and falls back to the default behavior).
|
|
1447
|
+
|
|
1448
|
+
| Handler | When it runs | Receives | Returns | Purpose |
|
|
1449
|
+
|---|---|---|---|---|
|
|
1450
|
+
| `data` | After data assembly, before content transform | `(data, block)` | New data object, or null | Filter, reshape, or augment the assembled data |
|
|
1451
|
+
| `content` | After data handler | `(data, block)` | ProseMirror document, or null | Transform raw content (Loom instantiation, template expansion) |
|
|
1452
|
+
| `props` | After parsing, defaults, and guarantees | `(content, params, block)` | `{ content, params }`, or null | Post-process the final shape before the component sees it |
|
|
1453
|
+
|
|
1454
|
+
### Loom integration
|
|
1455
|
+
|
|
1456
|
+
The most common use of content handlers is Loom-based content instantiation — resolving `{placeholder}` expressions in markdown against live data. `@uniweb/loom` provides a factory that creates the handler for you:
|
|
1457
|
+
|
|
1458
|
+
```js
|
|
1459
|
+
import { createLoomHandlers } from '@uniweb/loom'
|
|
1460
|
+
|
|
1461
|
+
export default {
|
|
1462
|
+
handlers: createLoomHandlers({
|
|
1463
|
+
vars: (data) => data?.profile?.[0],
|
|
1464
|
+
}),
|
|
1465
|
+
}
|
|
1466
|
+
```
|
|
1467
|
+
|
|
1468
|
+
The `vars` function extracts the Loom variable namespace from the assembled data. The factory returns a `content` handler that reads the `source` and `where` frontmatter params — without `source`, the handler does simple substitution; with `source`, the handler splits the markdown at `---` dividers and repeats the body per data item (see "Dividers — Content Boundaries" above). When `where` is also set, the source array is filtered first — only items where the expression evaluates to truthy are iterated:
|
|
1469
|
+
|
|
1470
|
+
```yaml
|
|
1471
|
+
---
|
|
1472
|
+
type: PublicationList
|
|
1473
|
+
source: publications
|
|
1474
|
+
where: "type = 'book'"
|
|
1475
|
+
---
|
|
1476
|
+
```
|
|
1477
|
+
|
|
1478
|
+
`where` expressions use Loom Plain form: `type = 'book'` (equality), `year > 1870` (comparison), `refereed` (truthy check), `type = 'book' AND refereed` (boolean combination). Aggregate expressions in the header (like `{COUNT OF publications}`) reflect the filtered set.
|
|
1479
|
+
|
|
1480
|
+
### Writing a custom handler
|
|
1481
|
+
|
|
1482
|
+
When the factory doesn't cover your case, write handlers directly:
|
|
1483
|
+
|
|
1484
|
+
```js
|
|
1485
|
+
import { Loom, instantiateContent, instantiateRepeated } from '@uniweb/loom'
|
|
1486
|
+
|
|
1487
|
+
const loom = new Loom()
|
|
1488
|
+
|
|
1489
|
+
export default {
|
|
1490
|
+
handlers: {
|
|
1491
|
+
content: (data, block) => {
|
|
1492
|
+
const profile = data?.profile?.[0]
|
|
1493
|
+
if (!profile) return null
|
|
1494
|
+
|
|
1495
|
+
const doc = block.rawContent?.doc ?? block.rawContent
|
|
1496
|
+
const source = block.properties?.source
|
|
1497
|
+
|
|
1498
|
+
if (!source) return instantiateContent(doc, loom, profile)
|
|
1499
|
+
return instantiateRepeated(doc, loom, profile, source)
|
|
1500
|
+
},
|
|
1501
|
+
},
|
|
1502
|
+
}
|
|
1503
|
+
```
|
|
1504
|
+
|
|
1505
|
+
The content handler receives `block.parsedContent.data` and reads raw ProseMirror from `block.rawContent`. It returns a new ProseMirror document — the framework re-parses it through the semantic parser. Returning `null` or the same reference as `block.rawContent` signals no change.
|
|
1506
|
+
|
|
1507
|
+
### Reserved frontmatter fields
|
|
1508
|
+
|
|
1509
|
+
`source` and `where` are convention-level reserved fields — they flow through to both `block.properties` (for handler access) and `params` (visible to components). Components can ignore them. This is consistent with how `background` and `theme` work. List them in `meta.js` params with descriptions so the editor and schema recognize them.
|
|
1510
|
+
|
|
1511
|
+
---
|
|
1512
|
+
|
|
1389
1513
|
## Migrating From Other Frameworks
|
|
1390
1514
|
|
|
1391
1515
|
Don't port line-by-line. Study the source, then rebuild from first principles. Other frameworks produce far more components than Uniweb needs — expect consolidation, not 1:1 correspondence.
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"generatedAt": "2026-04-16T16:24:12.713Z",
|
|
4
|
+
"packages": {
|
|
5
|
+
"@uniweb/build": {
|
|
6
|
+
"version": "0.9.4",
|
|
7
|
+
"path": "framework/build",
|
|
8
|
+
"deps": [
|
|
9
|
+
"@uniweb/content-reader",
|
|
10
|
+
"@uniweb/core",
|
|
11
|
+
"@uniweb/runtime",
|
|
12
|
+
"@uniweb/schemas",
|
|
13
|
+
"@uniweb/theming"
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
"@uniweb/content-reader": {
|
|
17
|
+
"version": "1.1.4",
|
|
18
|
+
"path": "framework/content-reader",
|
|
19
|
+
"deps": []
|
|
20
|
+
},
|
|
21
|
+
"@uniweb/content-writer": {
|
|
22
|
+
"version": "0.2.3",
|
|
23
|
+
"path": "framework/content-writer",
|
|
24
|
+
"deps": []
|
|
25
|
+
},
|
|
26
|
+
"@uniweb/core": {
|
|
27
|
+
"version": "0.6.1",
|
|
28
|
+
"path": "framework/core",
|
|
29
|
+
"deps": [
|
|
30
|
+
"@uniweb/semantic-parser",
|
|
31
|
+
"@uniweb/theming"
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
"@uniweb/frame-bridge": {
|
|
35
|
+
"version": "0.2.3",
|
|
36
|
+
"path": "framework/frame-bridge",
|
|
37
|
+
"deps": []
|
|
38
|
+
},
|
|
39
|
+
"@uniweb/icons": {
|
|
40
|
+
"version": "0.1.0",
|
|
41
|
+
"path": "framework/icons",
|
|
42
|
+
"deps": []
|
|
43
|
+
},
|
|
44
|
+
"@uniweb/kit": {
|
|
45
|
+
"version": "0.8.2",
|
|
46
|
+
"path": "framework/kit",
|
|
47
|
+
"deps": [
|
|
48
|
+
"@uniweb/core"
|
|
49
|
+
]
|
|
50
|
+
},
|
|
51
|
+
"@uniweb/loom": {
|
|
52
|
+
"version": "0.2.2",
|
|
53
|
+
"path": "framework/loom",
|
|
54
|
+
"deps": []
|
|
55
|
+
},
|
|
56
|
+
"@uniweb/press": {
|
|
57
|
+
"version": "0.2.3",
|
|
58
|
+
"path": "framework/press",
|
|
59
|
+
"deps": []
|
|
60
|
+
},
|
|
61
|
+
"@uniweb/runtime": {
|
|
62
|
+
"version": "0.7.4",
|
|
63
|
+
"path": "framework/runtime",
|
|
64
|
+
"deps": [
|
|
65
|
+
"@uniweb/core",
|
|
66
|
+
"@uniweb/theming"
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
"@uniweb/schemas": {
|
|
70
|
+
"version": "0.2.1",
|
|
71
|
+
"path": "framework/schemas",
|
|
72
|
+
"deps": []
|
|
73
|
+
},
|
|
74
|
+
"@uniweb/scholar": {
|
|
75
|
+
"version": "0.2.1",
|
|
76
|
+
"path": "framework/scholar",
|
|
77
|
+
"deps": []
|
|
78
|
+
},
|
|
79
|
+
"@uniweb/semantic-parser": {
|
|
80
|
+
"version": "1.1.9",
|
|
81
|
+
"path": "framework/semantic-parser",
|
|
82
|
+
"deps": []
|
|
83
|
+
},
|
|
84
|
+
"@uniweb/templates": {
|
|
85
|
+
"version": "0.7.26",
|
|
86
|
+
"path": "framework/templates",
|
|
87
|
+
"deps": []
|
|
88
|
+
},
|
|
89
|
+
"@uniweb/theming": {
|
|
90
|
+
"version": "0.1.3",
|
|
91
|
+
"path": "framework/theming",
|
|
92
|
+
"deps": []
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"templates": {
|
|
96
|
+
"marketing": {
|
|
97
|
+
"name": "Marketing Starter",
|
|
98
|
+
"description": "Landing pages and marketing sites with Tailwind CSS v4",
|
|
99
|
+
"tags": [
|
|
100
|
+
"marketing",
|
|
101
|
+
"landing-page",
|
|
102
|
+
"tailwind"
|
|
103
|
+
]
|
|
104
|
+
},
|
|
105
|
+
"academic": {
|
|
106
|
+
"name": "Academic",
|
|
107
|
+
"description": "Academic sites for researchers, labs, and departments",
|
|
108
|
+
"tags": [
|
|
109
|
+
"academic",
|
|
110
|
+
"research",
|
|
111
|
+
"portfolio"
|
|
112
|
+
]
|
|
113
|
+
},
|
|
114
|
+
"docs": {
|
|
115
|
+
"name": "Documentation",
|
|
116
|
+
"description": "Documentation sites with navigation levels",
|
|
117
|
+
"tags": [
|
|
118
|
+
"docs",
|
|
119
|
+
"documentation",
|
|
120
|
+
"technical"
|
|
121
|
+
]
|
|
122
|
+
},
|
|
123
|
+
"international": {
|
|
124
|
+
"name": "International Business",
|
|
125
|
+
"description": "Multilingual corporate website with English, Spanish, and French",
|
|
126
|
+
"tags": [
|
|
127
|
+
"i18n",
|
|
128
|
+
"multilingual",
|
|
129
|
+
"corporate",
|
|
130
|
+
"business",
|
|
131
|
+
"localization"
|
|
132
|
+
]
|
|
133
|
+
},
|
|
134
|
+
"dynamic": {
|
|
135
|
+
"name": "Dynamic Data",
|
|
136
|
+
"description": "Live API data fetching with loading states, transforms, and the portable data pattern",
|
|
137
|
+
"tags": [
|
|
138
|
+
"data",
|
|
139
|
+
"api",
|
|
140
|
+
"fetch",
|
|
141
|
+
"loading-states",
|
|
142
|
+
"dynamic"
|
|
143
|
+
]
|
|
144
|
+
},
|
|
145
|
+
"store": {
|
|
146
|
+
"name": "Solis Artisans - Store",
|
|
147
|
+
"description": "Artisan e-commerce with product collections, Shopify Buy Button, journal blog, and stone-amber design",
|
|
148
|
+
"tags": [
|
|
149
|
+
"store",
|
|
150
|
+
"e-commerce",
|
|
151
|
+
"shopify",
|
|
152
|
+
"artisan",
|
|
153
|
+
"products"
|
|
154
|
+
]
|
|
155
|
+
},
|
|
156
|
+
"learning": {
|
|
157
|
+
"name": "Learning Platform",
|
|
158
|
+
"description": "Course-based learning site with quizzes, code challenges, and AI grading integration",
|
|
159
|
+
"tags": [
|
|
160
|
+
"learning",
|
|
161
|
+
"course",
|
|
162
|
+
"education",
|
|
163
|
+
"quiz",
|
|
164
|
+
"lms",
|
|
165
|
+
"tutorial"
|
|
166
|
+
]
|
|
167
|
+
},
|
|
168
|
+
"extensions": {
|
|
169
|
+
"name": "Extensions Demo",
|
|
170
|
+
"description": "Primary foundation with a visual effects extension, demonstrating multi-foundation sites",
|
|
171
|
+
"tags": [
|
|
172
|
+
"extensions",
|
|
173
|
+
"multi-foundation",
|
|
174
|
+
"visual-effects",
|
|
175
|
+
"runtime"
|
|
176
|
+
]
|
|
177
|
+
},
|
|
178
|
+
"cv": {
|
|
179
|
+
"name": "CV (Docusite)",
|
|
180
|
+
"description": "An academic CV as a docusite — a URL whose content is also a downloadable Microsoft Word document. Ships with a full Charles Darwin sample CV and demonstrates the one-foundation-many-tenants pattern for download-time theming.",
|
|
181
|
+
"tags": [
|
|
182
|
+
"cv",
|
|
183
|
+
"resume",
|
|
184
|
+
"docusite",
|
|
185
|
+
"academic",
|
|
186
|
+
"document",
|
|
187
|
+
"press"
|
|
188
|
+
]
|
|
189
|
+
},
|
|
190
|
+
"cv-loom": {
|
|
191
|
+
"name": "CV (Loom-instantiated)",
|
|
192
|
+
"description": "A single-page narrative CV whose prose is written with {placeholder} expressions and instantiated at render time via the foundation content handler and @uniweb/loom. Reference implementation of the handler hook.",
|
|
193
|
+
"tags": [
|
|
194
|
+
"cv",
|
|
195
|
+
"loom",
|
|
196
|
+
"template-engine",
|
|
197
|
+
"content-handler",
|
|
198
|
+
"dynamic-prose",
|
|
199
|
+
"academic"
|
|
200
|
+
]
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
@@ -23,10 +23,12 @@ export const BUILTIN_TEMPLATES = ['blank', 'starter', 'none']
|
|
|
23
23
|
* `node scripts/framework/sandbox.js create`.
|
|
24
24
|
*
|
|
25
25
|
* 2. **Published CLI (npm-installed)** — the monorepo isn't on disk, so
|
|
26
|
-
* we read
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
26
|
+
* we read the vendored framework index at `../framework-index.json`,
|
|
27
|
+
* which the publish pipeline's pre-publish hook rewrites just before
|
|
28
|
+
* `pnpm publish` runs. The framework index is a single snapshot file
|
|
29
|
+
* that also carries `@uniweb/*` package versions (consumed by
|
|
30
|
+
* versions.js), so both the template list and the version resolver
|
|
31
|
+
* share one source of truth.
|
|
30
32
|
*
|
|
31
33
|
* When both sources are available (local dev with a committed snapshot),
|
|
32
34
|
* the live workspace manifest wins so newly-added templates are visible
|
|
@@ -34,43 +36,46 @@ export const BUILTIN_TEMPLATES = ['blank', 'starter', 'none']
|
|
|
34
36
|
*
|
|
35
37
|
* The previous implementation hardcoded a string array here, which
|
|
36
38
|
* silently duplicated `framework/templates/manifest.json` and required
|
|
37
|
-
* a two-repo edit whenever a template was added.
|
|
39
|
+
* a two-repo edit whenever a template was added. The one before THAT
|
|
40
|
+
* (v0.9.3) used a separate `./official-templates.snapshot.json` file;
|
|
41
|
+
* it has been retired in favor of the shared framework index.
|
|
38
42
|
*/
|
|
39
43
|
function loadOfficialTemplateList() {
|
|
40
44
|
// Local dev: framework/templates/manifest.json relative to this file
|
|
41
45
|
// at framework/cli/src/templates/resolver.js
|
|
42
46
|
const workspaceManifest = join(__dirname, '..', '..', '..', 'templates', 'manifest.json')
|
|
43
|
-
const picked =
|
|
47
|
+
const picked = tryReadTemplateKeys(workspaceManifest)
|
|
44
48
|
if (picked) return picked
|
|
45
49
|
|
|
46
|
-
// Published CLI fallback:
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
+
// Published CLI fallback: the framework index snapshot, one directory
|
|
51
|
+
// up at framework/cli/src/framework-index.json.
|
|
52
|
+
const indexPath = join(__dirname, '..', 'framework-index.json')
|
|
53
|
+
const fromIndex = tryReadTemplateKeys(indexPath)
|
|
54
|
+
if (fromIndex) return fromIndex
|
|
50
55
|
|
|
51
|
-
// If both fail, return an empty list rather than a stale
|
|
52
|
-
// array. An unknown template name then falls through to
|
|
53
|
-
// `@uniweb/template-<name>` lookup path, which is the
|
|
54
|
-
// behavior for third-party templates.
|
|
56
|
+
// If both sources fail, return an empty list rather than a stale
|
|
57
|
+
// hardcoded array. An unknown template name then falls through to
|
|
58
|
+
// the npm `@uniweb/template-<name>` lookup path, which is the
|
|
59
|
+
// intended behavior for third-party templates.
|
|
55
60
|
return []
|
|
56
61
|
}
|
|
57
62
|
|
|
58
|
-
function
|
|
63
|
+
function tryReadTemplateKeys(path) {
|
|
59
64
|
try {
|
|
60
65
|
if (!statSync(path).isFile()) return null
|
|
61
|
-
const
|
|
62
|
-
if (
|
|
63
|
-
return Object.keys(
|
|
66
|
+
const data = JSON.parse(readFileSync(path, 'utf8'))
|
|
67
|
+
if (data && data.templates && typeof data.templates === 'object') {
|
|
68
|
+
return Object.keys(data.templates)
|
|
64
69
|
}
|
|
65
70
|
} catch {}
|
|
66
71
|
return null
|
|
67
72
|
}
|
|
68
73
|
|
|
69
74
|
// Official templates from the templates repo. Derived from manifest.json
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
// reflect a just-added template, restart the CLI process or rerun
|
|
73
|
-
// scaffolder; this constant is read once per process.
|
|
75
|
+
// (local dev) or framework-index.json (published CLI) at module load
|
|
76
|
+
// time — see loadOfficialTemplateList() for details. If the list needs
|
|
77
|
+
// to reflect a just-added template, restart the CLI process or rerun
|
|
78
|
+
// the scaffolder; this constant is read once per process.
|
|
74
79
|
export const OFFICIAL_TEMPLATES = loadOfficialTemplateList()
|
|
75
80
|
|
|
76
81
|
/**
|
package/src/versions.js
CHANGED
|
@@ -96,12 +96,61 @@ function readWorkspaceVersion(packageName) {
|
|
|
96
96
|
return null
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Load the vendored framework index file (see
|
|
101
|
+
* `./framework-index.json`). The index is a snapshot of the framework
|
|
102
|
+
* state taken at CLI publish time — every `@uniweb/*` package name,
|
|
103
|
+
* version, path, and inter-package dep edges, plus the template list.
|
|
104
|
+
*
|
|
105
|
+
* The snapshot is the only way a published CLI (running from
|
|
106
|
+
* `node_modules/uniweb/…`, with no workspace on disk) can resolve
|
|
107
|
+
* versions for packages it doesn't directly import (press, loom,
|
|
108
|
+
* scholar, etc.). Without it, Handlebars helpers like
|
|
109
|
+
* `{{version "@uniweb/press"}}` fall through to a bogus default.
|
|
110
|
+
*
|
|
111
|
+
* Returns null if the file doesn't exist, is unparseable, or has a
|
|
112
|
+
* schema version we don't understand. Callers treat null as "no
|
|
113
|
+
* snapshot available" and fall back to their next source.
|
|
114
|
+
*/
|
|
115
|
+
function loadFrameworkIndex() {
|
|
116
|
+
const indexPath = join(__dirname, 'framework-index.json')
|
|
117
|
+
try {
|
|
118
|
+
const raw = readFileSync(indexPath, 'utf8')
|
|
119
|
+
const parsed = JSON.parse(raw)
|
|
120
|
+
if (parsed && parsed.schemaVersion === 1 && parsed.packages) {
|
|
121
|
+
return parsed
|
|
122
|
+
}
|
|
123
|
+
} catch {}
|
|
124
|
+
return null
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Pull `@uniweb/*` package versions out of the framework index's
|
|
129
|
+
* `packages` field and format them as caret ranges. Used as the
|
|
130
|
+
* published-CLI fallback source after the CLI's own deps and the live
|
|
131
|
+
* workspace walk both come up empty.
|
|
132
|
+
*/
|
|
133
|
+
function readIndexPackages() {
|
|
134
|
+
const index = loadFrameworkIndex()
|
|
135
|
+
if (!index) return {}
|
|
136
|
+
const result = {}
|
|
137
|
+
for (const [name, entry] of Object.entries(index.packages)) {
|
|
138
|
+
if (entry && entry.version) {
|
|
139
|
+
result[name] = `^${entry.version}`
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return result
|
|
143
|
+
}
|
|
144
|
+
|
|
99
145
|
/**
|
|
100
146
|
* Enumerate every `@uniweb/*` package under `framework/*` and return a
|
|
101
147
|
* map of `{ name: '^version' }`. Used to seed the resolved-versions
|
|
102
148
|
* cache so that packages not explicitly listed in the CLI's own deps
|
|
103
149
|
* (press, loom, scholar, schemas, etc.) still have a version available
|
|
104
150
|
* to templates that reference them.
|
|
151
|
+
*
|
|
152
|
+
* Works only in the monorepo. Published CLIs fall through to the
|
|
153
|
+
* framework-index snapshot via readIndexPackages() instead.
|
|
105
154
|
*/
|
|
106
155
|
function discoverWorkspacePackages() {
|
|
107
156
|
const root = getFrameworkRoot()
|
|
@@ -145,17 +194,23 @@ function resolveVersionSpec(spec, packageName) {
|
|
|
145
194
|
* Get resolved versions for @uniweb/* packages.
|
|
146
195
|
*
|
|
147
196
|
* Priority (highest first):
|
|
197
|
+
*
|
|
148
198
|
* 1. A concrete version spec already in the CLI's own `package.json`
|
|
149
|
-
* (the state after an npm publish: `workspace:*` is resolved
|
|
150
|
-
* real version).
|
|
151
|
-
* 2.
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
199
|
+
* (the state after an npm publish: `workspace:*` is resolved by
|
|
200
|
+
* pnpm to a real version).
|
|
201
|
+
* 2. Live workspace walk — every `@uniweb/*` package found under
|
|
202
|
+
* `framework/*` at CLI invocation time. This is the path that
|
|
203
|
+
* matters for local dev: a freshly-added package becomes
|
|
204
|
+
* reachable from every locally-run CLI without republishing.
|
|
205
|
+
* 3. Framework index snapshot — `./framework-index.json`, written by
|
|
206
|
+
* the publish pipeline's pre-publish hook. This is the path that
|
|
207
|
+
* matters for published CLIs: the monorepo isn't on disk, so the
|
|
208
|
+
* snapshot is how the CLI knows about packages it doesn't import
|
|
209
|
+
* directly (press, loom, scholar, schemas, …).
|
|
155
210
|
*
|
|
156
|
-
* The return shape is stable across
|
|
157
|
-
* npm-compatible version specs, plus the CLI's own version under
|
|
158
|
-
* `uniweb`.
|
|
211
|
+
* The return shape is stable across all paths: a map of package names
|
|
212
|
+
* to npm-compatible version specs, plus the CLI's own version under
|
|
213
|
+
* the key `uniweb`.
|
|
159
214
|
*
|
|
160
215
|
* @returns {Object} Map of package names to version specs
|
|
161
216
|
*/
|
|
@@ -173,14 +228,24 @@ export function getResolvedVersions() {
|
|
|
173
228
|
if (resolved) result[name] = resolved
|
|
174
229
|
}
|
|
175
230
|
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
231
|
+
// Layer in the live workspace walk. Overrides nothing (dev versions
|
|
232
|
+
// are fresher than anything in the CLI's own deps), but fills in
|
|
233
|
+
// packages the CLI doesn't reference directly.
|
|
179
234
|
const discovered = discoverWorkspacePackages()
|
|
180
235
|
for (const [name, version] of Object.entries(discovered)) {
|
|
181
236
|
if (!result[name]) result[name] = version
|
|
182
237
|
}
|
|
183
238
|
|
|
239
|
+
// Final fallback: the vendored framework index snapshot. Only hits
|
|
240
|
+
// when the workspace walk came up empty for a package (published
|
|
241
|
+
// CLI running outside the monorepo). The snapshot is refreshed at
|
|
242
|
+
// publish time, so its view of the world is current as of the CLI's
|
|
243
|
+
// own publish.
|
|
244
|
+
const indexed = readIndexPackages()
|
|
245
|
+
for (const [name, version] of Object.entries(indexed)) {
|
|
246
|
+
if (!result[name]) result[name] = version
|
|
247
|
+
}
|
|
248
|
+
|
|
184
249
|
// CLI itself. Caret on the current version — templates referencing
|
|
185
250
|
// `{{version "uniweb"}}` pick up whatever patch/minor ships in the
|
|
186
251
|
// same publish cycle as the template.
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": "1.0.0",
|
|
3
|
-
"templates": {
|
|
4
|
-
"marketing": {
|
|
5
|
-
"name": "Marketing Starter",
|
|
6
|
-
"description": "Landing pages and marketing sites with Tailwind CSS v4",
|
|
7
|
-
"tags": ["marketing", "landing-page", "tailwind"]
|
|
8
|
-
},
|
|
9
|
-
"academic": {
|
|
10
|
-
"name": "Academic",
|
|
11
|
-
"description": "Academic sites for researchers, labs, and departments",
|
|
12
|
-
"tags": ["academic", "research", "portfolio"]
|
|
13
|
-
},
|
|
14
|
-
"docs": {
|
|
15
|
-
"name": "Documentation",
|
|
16
|
-
"description": "Documentation sites with navigation levels",
|
|
17
|
-
"tags": ["docs", "documentation", "technical"]
|
|
18
|
-
},
|
|
19
|
-
"international": {
|
|
20
|
-
"name": "International Business",
|
|
21
|
-
"description": "Multilingual corporate website with English, Spanish, and French",
|
|
22
|
-
"tags": ["i18n", "multilingual", "corporate", "business", "localization"]
|
|
23
|
-
},
|
|
24
|
-
"dynamic": {
|
|
25
|
-
"name": "Dynamic Data",
|
|
26
|
-
"description": "Live API data fetching with loading states, transforms, and the portable data pattern",
|
|
27
|
-
"tags": ["data", "api", "fetch", "loading-states", "dynamic"]
|
|
28
|
-
},
|
|
29
|
-
"store": {
|
|
30
|
-
"name": "Solis Artisans - Store",
|
|
31
|
-
"description": "Artisan e-commerce with product collections, Shopify Buy Button, journal blog, and stone-amber design",
|
|
32
|
-
"tags": ["store", "e-commerce", "shopify", "artisan", "products"]
|
|
33
|
-
},
|
|
34
|
-
"learning": {
|
|
35
|
-
"name": "Learning Platform",
|
|
36
|
-
"description": "Course-based learning site with quizzes, code challenges, and AI grading integration",
|
|
37
|
-
"tags": ["learning", "course", "education", "quiz", "lms", "tutorial"]
|
|
38
|
-
},
|
|
39
|
-
"extensions": {
|
|
40
|
-
"name": "Extensions Demo",
|
|
41
|
-
"description": "Primary foundation with a visual effects extension, demonstrating multi-foundation sites",
|
|
42
|
-
"tags": ["extensions", "multi-foundation", "visual-effects", "runtime"]
|
|
43
|
-
},
|
|
44
|
-
"cv": {
|
|
45
|
-
"name": "CV (Docusite)",
|
|
46
|
-
"description": "An academic CV as a docusite — a URL whose content is also a downloadable Microsoft Word document. Ships with a full Charles Darwin sample CV and demonstrates the one-foundation-many-tenants pattern for download-time theming.",
|
|
47
|
-
"tags": ["cv", "resume", "docusite", "academic", "document", "press"]
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|