uniweb 0.12.41 → 0.12.43
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 +6 -6
- package/partials/agents.md +61 -0
- package/src/framework-index.json +30 -19
- package/src/index.js +6 -13
- package/src/templates/index.js +2 -2
- package/src/templates/resolver.js +60 -25
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.43",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,14 +41,14 @@
|
|
|
41
41
|
"js-yaml": "^4.1.0",
|
|
42
42
|
"prompts": "^2.4.2",
|
|
43
43
|
"tar": "^7.0.0",
|
|
44
|
-
"@uniweb/core": "0.7.
|
|
45
|
-
"@uniweb/
|
|
46
|
-
"@uniweb/
|
|
44
|
+
"@uniweb/core": "0.7.17",
|
|
45
|
+
"@uniweb/runtime": "0.8.23",
|
|
46
|
+
"@uniweb/kit": "0.9.21"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
-
"@uniweb/build": "0.14.24",
|
|
50
49
|
"@uniweb/content-reader": "1.1.12",
|
|
51
|
-
"@uniweb/semantic-parser": "1.1.17"
|
|
50
|
+
"@uniweb/semantic-parser": "1.1.17",
|
|
51
|
+
"@uniweb/build": "0.14.25"
|
|
52
52
|
},
|
|
53
53
|
"peerDependenciesMeta": {
|
|
54
54
|
"@uniweb/build": {
|
package/partials/agents.md
CHANGED
|
@@ -1005,6 +1005,67 @@ contexts:
|
|
|
1005
1005
|
|
|
1006
1006
|
> **Contrast warning:** Bright brand colors (orange, yellow, light green) at shade 500 may not meet WCAG contrast (4.5:1) with white foreground text. Test buttons for readability — if contrast is insufficient, keep the default shade 600 mapping.
|
|
1007
1007
|
|
|
1008
|
+
### Fonts
|
|
1009
|
+
|
|
1010
|
+
Font families are a **site** setting, configured in `theme.yml` under `fonts:`. A foundation never installs a font package (no `@fontsource/*`) and never hardcodes a family in its code — if you reach for either, you're off the paved path. The framework provides three roles and wires each onto the standard elements for you:
|
|
1011
|
+
|
|
1012
|
+
| Role | Wired onto | Typical use |
|
|
1013
|
+
|---|---|---|
|
|
1014
|
+
| `body` | `body` (all text by default) | paragraphs, UI |
|
|
1015
|
+
| `heading` | `h1, h2, h3` | titles |
|
|
1016
|
+
| `mono` | `code, pre, kbd, samp` | code, technical text |
|
|
1017
|
+
|
|
1018
|
+
```yaml
|
|
1019
|
+
# site/theme.yml
|
|
1020
|
+
fonts:
|
|
1021
|
+
body: "Inter, system-ui, sans-serif"
|
|
1022
|
+
heading: "Poppins, system-ui, sans-serif"
|
|
1023
|
+
mono: "'Fira Code', monospace"
|
|
1024
|
+
```
|
|
1025
|
+
|
|
1026
|
+
Because the site wires the families onto real elements, **components need no font-family classes** — render semantic markup (`<H1>`, `<P>`, `<code>` from the kit) and the roles apply. This is what lets one foundation re-font per site with zero code changes.
|
|
1027
|
+
|
|
1028
|
+
**Loading the files** — two site-side options, both config-only, neither adds a dependency:
|
|
1029
|
+
|
|
1030
|
+
```yaml
|
|
1031
|
+
# Hosted (Google Fonts or any provider). The build auto-adds preconnect
|
|
1032
|
+
# hints and drops families no role references.
|
|
1033
|
+
fonts:
|
|
1034
|
+
heading: "Poppins, sans-serif"
|
|
1035
|
+
import:
|
|
1036
|
+
- url: "https://fonts.googleapis.com/css2?family=Poppins:wght@600;700"
|
|
1037
|
+
|
|
1038
|
+
# Self-hosted (no CDN — privacy / offline). Put the files under
|
|
1039
|
+
# site/public/fonts/ and declare @font-face faces; the build emits the
|
|
1040
|
+
# @font-face rules + preload hints, and skips any face whose family no
|
|
1041
|
+
# role uses.
|
|
1042
|
+
fonts:
|
|
1043
|
+
heading: "Söhne, sans-serif"
|
|
1044
|
+
faces:
|
|
1045
|
+
- { family: "Söhne", src: /fonts/soehne-bold.woff2, weight: 700 }
|
|
1046
|
+
- { family: "Söhne", src: /fonts/soehne-regular.woff2, weight: 400 }
|
|
1047
|
+
```
|
|
1048
|
+
|
|
1049
|
+
**In a component, weight is yours; family is the site's.** Weight / size / style utilities (`font-bold`, `font-black`, `italic`, `text-xl`) are ordinary design vocabulary — use them freely. A font-**family** utility is only safe when the family behind it is site-controlled — and by default `font-sans` / `font-serif` are *not* (they resolve to Tailwind's built-in stacks; only `font-mono` tracks the site's `mono` role, by name-collision). So a bare `font-serif` in a component silently hardcodes a typeface.
|
|
1050
|
+
|
|
1051
|
+
**A family beyond the three roles** — an editorial serif, a display face — is a **foundation var**, so the site keeps control. Name it after a Tailwind font slot (`font-serif`) and that utility resolves to it; or give it any name and reference the variable directly:
|
|
1052
|
+
|
|
1053
|
+
```js
|
|
1054
|
+
// foundation src/main.js
|
|
1055
|
+
export const vars = {
|
|
1056
|
+
'font-serif': {
|
|
1057
|
+
default: "Georgia, 'Times New Roman', serif",
|
|
1058
|
+
description: 'Editorial serif for pull-quotes, taglines, quotes',
|
|
1059
|
+
},
|
|
1060
|
+
}
|
|
1061
|
+
```
|
|
1062
|
+
```jsx
|
|
1063
|
+
<p className="font-serif">{content.tagline}</p> {/* now site-controlled */}
|
|
1064
|
+
// …or a custom-named var: <p style={{ fontFamily: 'var(--font-display)' }}>
|
|
1065
|
+
```
|
|
1066
|
+
|
|
1067
|
+
The site sets the family in `theme.yml` under `vars:` (`font-serif: "Fraunces, Georgia, serif"`) and loads it with `fonts.import` / `fonts.faces` — the file loads even though the family isn't one of the three role slots.
|
|
1068
|
+
|
|
1008
1069
|
### Foundation variables
|
|
1009
1070
|
|
|
1010
1071
|
Most customization is handled by component params. Both section components and layout components declare their own params in `meta.js` — layouts are full components with params, not just structural wrappers. A header height, for example, is typically a layout param, not a foundation var.
|
package/src/framework-index.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-07-
|
|
3
|
+
"generatedAt": "2026-07-17T21:03:16.675Z",
|
|
4
4
|
"packages": {
|
|
5
5
|
"@uniweb/build": {
|
|
6
|
-
"version": "0.14.
|
|
6
|
+
"version": "0.14.25",
|
|
7
7
|
"path": "framework/build",
|
|
8
8
|
"deps": [
|
|
9
9
|
"@uniweb/content-reader",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"deps": []
|
|
26
26
|
},
|
|
27
27
|
"@uniweb/core": {
|
|
28
|
-
"version": "0.7.
|
|
28
|
+
"version": "0.7.17",
|
|
29
29
|
"path": "framework/core",
|
|
30
30
|
"deps": [
|
|
31
31
|
"@uniweb/semantic-parser",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"deps": []
|
|
44
44
|
},
|
|
45
45
|
"@uniweb/kit": {
|
|
46
|
-
"version": "0.9.
|
|
46
|
+
"version": "0.9.21",
|
|
47
47
|
"path": "framework/kit",
|
|
48
48
|
"deps": [
|
|
49
49
|
"@uniweb/core",
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
"deps": []
|
|
62
62
|
},
|
|
63
63
|
"@uniweb/runtime": {
|
|
64
|
-
"version": "0.8.
|
|
64
|
+
"version": "0.8.23",
|
|
65
65
|
"path": "framework/runtime",
|
|
66
66
|
"deps": [
|
|
67
67
|
"@uniweb/core",
|
|
@@ -89,17 +89,17 @@
|
|
|
89
89
|
"deps": []
|
|
90
90
|
},
|
|
91
91
|
"@uniweb/templates": {
|
|
92
|
-
"version": "0.7.
|
|
92
|
+
"version": "0.7.44",
|
|
93
93
|
"path": "framework/templates",
|
|
94
94
|
"deps": []
|
|
95
95
|
},
|
|
96
96
|
"@uniweb/theming": {
|
|
97
|
-
"version": "0.1.
|
|
97
|
+
"version": "0.1.6",
|
|
98
98
|
"path": "framework/theming",
|
|
99
99
|
"deps": []
|
|
100
100
|
},
|
|
101
101
|
"@uniweb/unipress": {
|
|
102
|
-
"version": "0.4.
|
|
102
|
+
"version": "0.4.32",
|
|
103
103
|
"path": "framework/unipress",
|
|
104
104
|
"deps": [
|
|
105
105
|
"@uniweb/build",
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
},
|
|
113
113
|
"templates": {
|
|
114
114
|
"marketing": {
|
|
115
|
-
"name": "Marketing
|
|
115
|
+
"name": "Marketing",
|
|
116
116
|
"description": "Landing pages and marketing sites with Tailwind CSS v4",
|
|
117
117
|
"tags": [
|
|
118
118
|
"marketing",
|
|
@@ -130,7 +130,7 @@
|
|
|
130
130
|
]
|
|
131
131
|
},
|
|
132
132
|
"docs": {
|
|
133
|
-
"name": "
|
|
133
|
+
"name": "Docs",
|
|
134
134
|
"description": "Documentation sites with navigation levels",
|
|
135
135
|
"tags": [
|
|
136
136
|
"docs",
|
|
@@ -139,7 +139,7 @@
|
|
|
139
139
|
]
|
|
140
140
|
},
|
|
141
141
|
"international": {
|
|
142
|
-
"name": "International
|
|
142
|
+
"name": "International",
|
|
143
143
|
"description": "Multilingual corporate website with English, Spanish, and French",
|
|
144
144
|
"tags": [
|
|
145
145
|
"i18n",
|
|
@@ -161,7 +161,7 @@
|
|
|
161
161
|
]
|
|
162
162
|
},
|
|
163
163
|
"dynamic": {
|
|
164
|
-
"name": "Dynamic
|
|
164
|
+
"name": "Dynamic",
|
|
165
165
|
"description": "Live API data fetching with loading states, transforms, and the portable data pattern",
|
|
166
166
|
"tags": [
|
|
167
167
|
"data",
|
|
@@ -172,7 +172,7 @@
|
|
|
172
172
|
]
|
|
173
173
|
},
|
|
174
174
|
"store": {
|
|
175
|
-
"name": "
|
|
175
|
+
"name": "Store",
|
|
176
176
|
"description": "Artisan e-commerce with product collections, Shopify Buy Button, journal blog, and stone-amber design",
|
|
177
177
|
"tags": [
|
|
178
178
|
"store",
|
|
@@ -183,7 +183,7 @@
|
|
|
183
183
|
]
|
|
184
184
|
},
|
|
185
185
|
"learning": {
|
|
186
|
-
"name": "Learning
|
|
186
|
+
"name": "Learning",
|
|
187
187
|
"description": "Course-based learning site with quizzes, code challenges, and AI grading integration",
|
|
188
188
|
"tags": [
|
|
189
189
|
"learning",
|
|
@@ -195,7 +195,7 @@
|
|
|
195
195
|
]
|
|
196
196
|
},
|
|
197
197
|
"extensions": {
|
|
198
|
-
"name": "Extensions
|
|
198
|
+
"name": "Extensions",
|
|
199
199
|
"description": "Primary foundation with a visual effects extension, demonstrating multi-foundation sites",
|
|
200
200
|
"tags": [
|
|
201
201
|
"extensions",
|
|
@@ -205,7 +205,7 @@
|
|
|
205
205
|
]
|
|
206
206
|
},
|
|
207
207
|
"cv-loom": {
|
|
208
|
-
"name": "CV (Loom
|
|
208
|
+
"name": "CV (Loom)",
|
|
209
209
|
"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.",
|
|
210
210
|
"tags": [
|
|
211
211
|
"cv",
|
|
@@ -217,7 +217,7 @@
|
|
|
217
217
|
]
|
|
218
218
|
},
|
|
219
219
|
"monograph": {
|
|
220
|
-
"name": "Monograph
|
|
220
|
+
"name": "Monograph",
|
|
221
221
|
"description": "A richly illustrated long-form document — numbered chapters with nested subsections, figures with captions, data-driven specimen and measurement tables, and a citestyle bibliography. The same JSX drives the themed web preview and a branded .docx with headers, footers, and page numbers. Ships with Darwin's Galapagos observations as sample content.",
|
|
222
222
|
"tags": [
|
|
223
223
|
"monograph",
|
|
@@ -230,7 +230,7 @@
|
|
|
230
230
|
]
|
|
231
231
|
},
|
|
232
232
|
"academic-metrics": {
|
|
233
|
-
"name": "Academic Metrics
|
|
233
|
+
"name": "Academic Metrics",
|
|
234
234
|
"description": "A docusite whose content is also a downloadable Excel workbook. Aggregates academic metrics (publications, funding, supervisions) across multiple members of a university unit. Rich web previews + flat tabular xlsx sheets from the same data. Ships with three 19th-century naturalists as sample members.",
|
|
235
235
|
"tags": [
|
|
236
236
|
"academic-metrics",
|
|
@@ -244,7 +244,7 @@
|
|
|
244
244
|
]
|
|
245
245
|
},
|
|
246
246
|
"business-docs": {
|
|
247
|
-
"name": "Business
|
|
247
|
+
"name": "Business Documents",
|
|
248
248
|
"description": "A complete business-document site: invoice and SOW collections, per-record preview pages, and a filtered engagement report with XLSX export. Built on @uniweb/business-docs and demonstrates the same foundation that ships the unipress invoice template.",
|
|
249
249
|
"tags": [
|
|
250
250
|
"business-docs",
|
|
@@ -256,6 +256,17 @@
|
|
|
256
256
|
"docx",
|
|
257
257
|
"pagedjs"
|
|
258
258
|
]
|
|
259
|
+
},
|
|
260
|
+
"paste": {
|
|
261
|
+
"name": "Paste a Design (Level 0)",
|
|
262
|
+
"description": "Bring an existing React design (AI-generated or hand-built) into Uniweb with zero rewiring — replace App.jsx and run. Ships lucide-react preinstalled and SPA-first. The Level 0 conversion starting point.",
|
|
263
|
+
"tags": [
|
|
264
|
+
"paste",
|
|
265
|
+
"level-0",
|
|
266
|
+
"convert",
|
|
267
|
+
"ai-generated",
|
|
268
|
+
"starter"
|
|
269
|
+
]
|
|
259
270
|
}
|
|
260
271
|
}
|
|
261
272
|
}
|
package/src/index.js
CHANGED
|
@@ -41,6 +41,7 @@ import { template } from './commands/template.js'
|
|
|
41
41
|
import {
|
|
42
42
|
resolveTemplate,
|
|
43
43
|
parseTemplateId,
|
|
44
|
+
buildTemplateChoices,
|
|
44
45
|
} from './templates/index.js'
|
|
45
46
|
import { validateTemplate } from './templates/validator.js'
|
|
46
47
|
import { scaffoldWorkspace, scaffoldFoundation, scaffoldSite, applyContent, applyStarter, mergeTemplateDependencies, getWorkspaceTemplateOutputs } from './utils/scaffold.js'
|
|
@@ -59,19 +60,11 @@ const colors = {
|
|
|
59
60
|
red: '\x1b[31m',
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
// Template choices for interactive prompt
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
{ title: 'Docs', value: 'docs', description: 'Documentation with sidebar and search' },
|
|
68
|
-
{ title: 'Academic', value: 'academic', description: 'Research site with publications and team' },
|
|
69
|
-
{ title: 'Dynamic', value: 'dynamic', description: 'Live API data fetching with loading states' },
|
|
70
|
-
{ title: 'International', value: 'international', description: 'Multilingual site with i18n and blog' },
|
|
71
|
-
{ title: 'Store', value: 'store', description: 'E-commerce with product grid' },
|
|
72
|
-
{ title: 'Extensions', value: 'extensions', description: 'Multi-foundation with visual effects extension' },
|
|
73
|
-
{ title: 'Blank workspace', value: 'blank', description: 'Empty workspace — grow with uniweb add' },
|
|
74
|
-
]
|
|
63
|
+
// Template choices for the interactive prompt — built-ins first, then every
|
|
64
|
+
// official template from the shared manifest (so the list never drifts from
|
|
65
|
+
// framework/templates/manifest.json), with "Blank" last. Composed by
|
|
66
|
+
// buildTemplateChoices() in templates/resolver.js.
|
|
67
|
+
const TEMPLATE_CHOICES = buildTemplateChoices()
|
|
75
68
|
|
|
76
69
|
// Files that may pre-exist in the target dir during `uniweb create .` and
|
|
77
70
|
// will be silently overwritten by the scaffold. Anything else colliding
|
package/src/templates/index.js
CHANGED
|
@@ -7,7 +7,7 @@ import { readFile, rm } from 'node:fs/promises'
|
|
|
7
7
|
import { join, resolve } from 'node:path'
|
|
8
8
|
import { homedir } from 'node:os'
|
|
9
9
|
|
|
10
|
-
import { parseTemplateId, getTemplateDisplayName, BUILTIN_TEMPLATES, OFFICIAL_TEMPLATES } from './resolver.js'
|
|
10
|
+
import { parseTemplateId, getTemplateDisplayName, BUILTIN_TEMPLATES, OFFICIAL_TEMPLATES, buildTemplateChoices } from './resolver.js'
|
|
11
11
|
import { fetchNpmTemplate } from './fetchers/npm.js'
|
|
12
12
|
import { fetchGitHubTemplate } from './fetchers/github.js'
|
|
13
13
|
import { fetchOfficialTemplate, listOfficialTemplates } from './fetchers/release.js'
|
|
@@ -182,4 +182,4 @@ export async function listAvailableTemplates() {
|
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
// Re-export for convenience
|
|
185
|
-
export { parseTemplateId, getTemplateDisplayName, BUILTIN_TEMPLATES, OFFICIAL_TEMPLATES }
|
|
185
|
+
export { parseTemplateId, getTemplateDisplayName, BUILTIN_TEMPLATES, OFFICIAL_TEMPLATES, buildTemplateChoices }
|
|
@@ -12,7 +12,8 @@ const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
|
12
12
|
export const BUILTIN_TEMPLATES = ['blank', 'starter', 'none']
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* Load the
|
|
15
|
+
* Load the official template metadata map — `{ id: { name, description,
|
|
16
|
+
* tags } }`, keyed by template id.
|
|
16
17
|
*
|
|
17
18
|
* There are two sources of truth depending on where the CLI is running:
|
|
18
19
|
*
|
|
@@ -24,59 +25,93 @@ export const BUILTIN_TEMPLATES = ['blank', 'starter', 'none']
|
|
|
24
25
|
*
|
|
25
26
|
* 2. **Published CLI (npm-installed)** — the monorepo isn't on disk, so
|
|
26
27
|
* we read the vendored framework index at `../framework-index.json`,
|
|
27
|
-
* which the publish pipeline
|
|
28
|
-
*
|
|
29
|
-
* that also carries `@uniweb/*` package versions
|
|
30
|
-
* versions.js), so both the template list and the version
|
|
31
|
-
* share one source of truth.
|
|
28
|
+
* which the publish pipeline rewrites just before `pnpm publish` runs,
|
|
29
|
+
* copying the manifest's `templates` verbatim. The framework index is a
|
|
30
|
+
* single snapshot file that also carries `@uniweb/*` package versions
|
|
31
|
+
* (consumed by versions.js), so both the template list and the version
|
|
32
|
+
* resolver share one source of truth.
|
|
32
33
|
*
|
|
33
34
|
* When both sources are available (local dev with a committed snapshot),
|
|
34
35
|
* the live workspace manifest wins so newly-added templates are visible
|
|
35
36
|
* without waiting for a CLI republish.
|
|
36
37
|
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
38
|
+
* Both files store `templates` as the same `{ id: { name, description,
|
|
39
|
+
* tags } }` shape, so one map feeds both name-resolution
|
|
40
|
+
* (OFFICIAL_TEMPLATES) and the interactive picker (buildTemplateChoices) —
|
|
41
|
+
* neither hardcodes a list. (An earlier version returned only the keys and
|
|
42
|
+
* left the `create` picker with its own hardcoded array, which silently
|
|
43
|
+
* drifted out of sync whenever a template was added.)
|
|
42
44
|
*/
|
|
43
|
-
function
|
|
45
|
+
function loadOfficialTemplateMap() {
|
|
44
46
|
// Local dev: framework/templates/manifest.json relative to this file
|
|
45
47
|
// at framework/cli/src/templates/resolver.js
|
|
46
48
|
const workspaceManifest = join(__dirname, '..', '..', '..', 'templates', 'manifest.json')
|
|
47
|
-
const picked =
|
|
49
|
+
const picked = tryReadTemplateMap(workspaceManifest)
|
|
48
50
|
if (picked) return picked
|
|
49
51
|
|
|
50
52
|
// Published CLI fallback: the framework index snapshot, one directory
|
|
51
53
|
// up at framework/cli/src/framework-index.json.
|
|
52
54
|
const indexPath = join(__dirname, '..', 'framework-index.json')
|
|
53
|
-
const fromIndex =
|
|
55
|
+
const fromIndex = tryReadTemplateMap(indexPath)
|
|
54
56
|
if (fromIndex) return fromIndex
|
|
55
57
|
|
|
56
|
-
// If both sources fail, return an empty
|
|
57
|
-
// hardcoded
|
|
58
|
+
// If both sources fail, return an empty map rather than a stale
|
|
59
|
+
// hardcoded list. An unknown template name then falls through to
|
|
58
60
|
// the npm `@uniweb/template-<name>` lookup path, which is the
|
|
59
61
|
// intended behavior for third-party templates.
|
|
60
|
-
return
|
|
62
|
+
return {}
|
|
61
63
|
}
|
|
62
64
|
|
|
63
|
-
function
|
|
65
|
+
function tryReadTemplateMap(path) {
|
|
64
66
|
try {
|
|
65
67
|
if (!statSync(path).isFile()) return null
|
|
66
68
|
const data = JSON.parse(readFileSync(path, 'utf8'))
|
|
67
69
|
if (data && data.templates && typeof data.templates === 'object') {
|
|
68
|
-
return
|
|
70
|
+
return data.templates
|
|
69
71
|
}
|
|
70
72
|
} catch {}
|
|
71
73
|
return null
|
|
72
74
|
}
|
|
73
75
|
|
|
74
|
-
// Official
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
|
|
79
|
-
|
|
76
|
+
// Official template metadata keyed by id. Derived from manifest.json (local
|
|
77
|
+
// dev) or framework-index.json (published CLI) at module load time — see
|
|
78
|
+
// loadOfficialTemplateMap() for details. Read once per process; to reflect a
|
|
79
|
+
// just-added template, restart the CLI or rerun the scaffolder.
|
|
80
|
+
export const OFFICIAL_TEMPLATE_MAP = loadOfficialTemplateMap()
|
|
81
|
+
|
|
82
|
+
// Official template ids, e.g. ['marketing', 'docs', 'academic', …].
|
|
83
|
+
export const OFFICIAL_TEMPLATES = Object.keys(OFFICIAL_TEMPLATE_MAP)
|
|
84
|
+
|
|
85
|
+
// Built-in (programmatic) picker entries — not in the manifest; the CLI
|
|
86
|
+
// generates these itself. "Blank" trails the official templates.
|
|
87
|
+
const BUILTIN_LEAD_CHOICES = [
|
|
88
|
+
{ title: 'None', value: 'none', description: 'Foundation + site with no content' },
|
|
89
|
+
{ title: 'Starter', value: 'starter', description: 'Foundation + site + sample content' }
|
|
90
|
+
]
|
|
91
|
+
const BLANK_CHOICE = {
|
|
92
|
+
title: 'Blank workspace',
|
|
93
|
+
value: 'blank',
|
|
94
|
+
description: 'Empty workspace — grow with uniweb add'
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Build the choices for the interactive `create` template prompt: the
|
|
99
|
+
* built-in leads (None, Starter), then every official template from the
|
|
100
|
+
* shared manifest in manifest order, then Blank last. Deriving the official
|
|
101
|
+
* entries from OFFICIAL_TEMPLATE_MAP keeps the picker in lockstep with
|
|
102
|
+
* framework/templates/manifest.json — adding a template there (and
|
|
103
|
+
* republishing the CLI) is all it takes for it to appear here.
|
|
104
|
+
*
|
|
105
|
+
* @returns {Array<{title: string, value: string, description: string}>}
|
|
106
|
+
*/
|
|
107
|
+
export function buildTemplateChoices() {
|
|
108
|
+
const official = Object.entries(OFFICIAL_TEMPLATE_MAP).map(([id, info]) => ({
|
|
109
|
+
title: info?.name || id,
|
|
110
|
+
value: id,
|
|
111
|
+
description: info?.description || ''
|
|
112
|
+
}))
|
|
113
|
+
return [...BUILTIN_LEAD_CHOICES, ...official, BLANK_CHOICE]
|
|
114
|
+
}
|
|
80
115
|
|
|
81
116
|
/**
|
|
82
117
|
* Parse a template identifier and determine its source type
|