uniweb 0.9.2 → 0.9.3
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "uniweb",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
4
4
|
"description": "Create structured Vite + React sites with content/code separation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -41,13 +41,13 @@
|
|
|
41
41
|
"js-yaml": "^4.1.0",
|
|
42
42
|
"prompts": "^2.4.2",
|
|
43
43
|
"tar": "^7.0.0",
|
|
44
|
-
"@uniweb/runtime": "0.7.2",
|
|
45
44
|
"@uniweb/core": "0.6.1",
|
|
45
|
+
"@uniweb/runtime": "0.7.3",
|
|
46
46
|
"@uniweb/kit": "0.8.1"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
|
+
"@uniweb/build": "0.9.3",
|
|
49
50
|
"@uniweb/content-reader": "1.1.4",
|
|
50
|
-
"@uniweb/build": "0.9.2",
|
|
51
51
|
"@uniweb/semantic-parser": "1.1.9"
|
|
52
52
|
},
|
|
53
53
|
"peerDependenciesMeta": {
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
}
|
|
@@ -2,11 +2,76 @@
|
|
|
2
2
|
* Template resolver - parses template identifiers and determines source type
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { readFileSync, statSync } from 'node:fs'
|
|
6
|
+
import { dirname, join } from 'node:path'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
10
|
+
|
|
5
11
|
// Built-in templates (programmatic, not file-based)
|
|
6
12
|
export const BUILTIN_TEMPLATES = ['blank', 'starter', 'none']
|
|
7
13
|
|
|
8
|
-
|
|
9
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Load the list of official template names.
|
|
16
|
+
*
|
|
17
|
+
* There are two sources of truth depending on where the CLI is running:
|
|
18
|
+
*
|
|
19
|
+
* 1. **Local dev inside the Uniweb monorepo** — the authoritative file
|
|
20
|
+
* is `framework/templates/manifest.json`. Adding a new template
|
|
21
|
+
* there makes it immediately reachable from any locally-run CLI
|
|
22
|
+
* without republishing. This is the only path that matters for
|
|
23
|
+
* `node scripts/framework/sandbox.js create`.
|
|
24
|
+
*
|
|
25
|
+
* 2. **Published CLI (npm-installed)** — the monorepo isn't on disk, so
|
|
26
|
+
* we read a vendored snapshot file (`./official-templates.snapshot.json`)
|
|
27
|
+
* that the publish script rewrites from the workspace manifest just
|
|
28
|
+
* before `pnpm publish` runs. The snapshot is committed so every CLI
|
|
29
|
+
* version in git carries the template list it shipped with.
|
|
30
|
+
*
|
|
31
|
+
* When both sources are available (local dev with a committed snapshot),
|
|
32
|
+
* the live workspace manifest wins so newly-added templates are visible
|
|
33
|
+
* without waiting for a CLI republish.
|
|
34
|
+
*
|
|
35
|
+
* The previous implementation hardcoded a string array here, which
|
|
36
|
+
* silently duplicated `framework/templates/manifest.json` and required
|
|
37
|
+
* a two-repo edit whenever a template was added.
|
|
38
|
+
*/
|
|
39
|
+
function loadOfficialTemplateList() {
|
|
40
|
+
// Local dev: framework/templates/manifest.json relative to this file
|
|
41
|
+
// at framework/cli/src/templates/resolver.js
|
|
42
|
+
const workspaceManifest = join(__dirname, '..', '..', '..', 'templates', 'manifest.json')
|
|
43
|
+
const picked = tryReadManifest(workspaceManifest)
|
|
44
|
+
if (picked) return picked
|
|
45
|
+
|
|
46
|
+
// Published CLI fallback: vendored snapshot next to this file
|
|
47
|
+
const snapshotPath = join(__dirname, 'official-templates.snapshot.json')
|
|
48
|
+
const snapshot = tryReadManifest(snapshotPath)
|
|
49
|
+
if (snapshot) return snapshot
|
|
50
|
+
|
|
51
|
+
// If both fail, return an empty list rather than a stale hardcoded
|
|
52
|
+
// array. An unknown template name then falls through to the npm
|
|
53
|
+
// `@uniweb/template-<name>` lookup path, which is the intended
|
|
54
|
+
// behavior for third-party templates.
|
|
55
|
+
return []
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function tryReadManifest(path) {
|
|
59
|
+
try {
|
|
60
|
+
if (!statSync(path).isFile()) return null
|
|
61
|
+
const manifest = JSON.parse(readFileSync(path, 'utf8'))
|
|
62
|
+
if (manifest && manifest.templates && typeof manifest.templates === 'object') {
|
|
63
|
+
return Object.keys(manifest.templates)
|
|
64
|
+
}
|
|
65
|
+
} catch {}
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Official templates from the templates repo. Derived from manifest.json
|
|
70
|
+
// at module load time — see loadOfficialTemplateList() for the two source
|
|
71
|
+
// paths (local workspace vs. vendored snapshot). If the list needs to
|
|
72
|
+
// reflect a just-added template, restart the CLI process or rerun the
|
|
73
|
+
// scaffolder; this constant is read once per process.
|
|
74
|
+
export const OFFICIAL_TEMPLATES = loadOfficialTemplateList()
|
|
10
75
|
|
|
11
76
|
/**
|
|
12
77
|
* Parse a template identifier and determine its source type
|
package/src/utils/scaffold.js
CHANGED
|
@@ -10,6 +10,7 @@ import { existsSync, readdirSync } from 'node:fs'
|
|
|
10
10
|
import { join, dirname } from 'node:path'
|
|
11
11
|
import { fileURLToPath } from 'node:url'
|
|
12
12
|
import yaml from 'js-yaml'
|
|
13
|
+
import Handlebars from 'handlebars'
|
|
13
14
|
import { copyTemplateDirectory, registerVersions } from '../templates/processor.js'
|
|
14
15
|
import { getVersionsForTemplates, getCliVersion } from '../versions.js'
|
|
15
16
|
|
|
@@ -157,19 +158,38 @@ async function copyContentRecursive(sourceDir, targetDir, context, structuralFil
|
|
|
157
158
|
newContent = template(context)
|
|
158
159
|
}
|
|
159
160
|
|
|
160
|
-
// Merge config files instead of overwriting
|
|
161
|
+
// Merge config files instead of overwriting.
|
|
162
|
+
//
|
|
163
|
+
// The "merge" here is narrow: take the content template's output
|
|
164
|
+
// as the source of truth (so comments, formatting, and the full
|
|
165
|
+
// educational structure of the content template survive) and
|
|
166
|
+
// override only the specific top-level keys listed in
|
|
167
|
+
// preserveKeys with the values from the already-scaffolded base
|
|
168
|
+
// file (so the user's chosen project name and foundation ref
|
|
169
|
+
// don't get replaced by whatever the content template hardcoded).
|
|
170
|
+
//
|
|
171
|
+
// Earlier versions of this code parsed both files through
|
|
172
|
+
// js-yaml, merged the objects, and re-emitted the result via
|
|
173
|
+
// yaml.dump() — which stripped every comment and blank line on
|
|
174
|
+
// the way through. For templates like `cv/site/site.yml.hbs`
|
|
175
|
+
// whose comments are the template's educational payload, that
|
|
176
|
+
// was devastating. The line-level override below preserves the
|
|
177
|
+
// content template's text verbatim except for the preserved keys.
|
|
161
178
|
const preserveKeys = mergeFiles[outputName]
|
|
162
179
|
if (preserveKeys && existsSync(targetPath)) {
|
|
163
180
|
const existingContent = await fs.readFile(targetPath, 'utf-8')
|
|
164
181
|
const existing = yaml.load(existingContent) || {}
|
|
165
|
-
|
|
182
|
+
let merged = newContent ?? await fs.readFile(sourcePath, 'utf-8')
|
|
166
183
|
|
|
167
|
-
// Template values as base, preserve specified keys from scaffolded version
|
|
168
|
-
const merged = { ...incoming }
|
|
169
184
|
for (const key of preserveKeys) {
|
|
170
|
-
if (existing[key]
|
|
185
|
+
if (existing[key] === undefined) continue
|
|
186
|
+
const baseLine = matchTopLevelLine(existingContent, key)
|
|
187
|
+
if (baseLine) {
|
|
188
|
+
merged = replaceTopLevelLine(merged, key, baseLine)
|
|
189
|
+
}
|
|
171
190
|
}
|
|
172
|
-
|
|
191
|
+
|
|
192
|
+
await fs.writeFile(targetPath, merged)
|
|
173
193
|
} else if (newContent !== undefined) {
|
|
174
194
|
await fs.writeFile(targetPath, newContent)
|
|
175
195
|
} else {
|
|
@@ -208,14 +228,76 @@ export async function applyStarter(projectDir, context, options = {}) {
|
|
|
208
228
|
await applyContent(siteContentDir, siteTargetDir, context, options)
|
|
209
229
|
}
|
|
210
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Escape a string for safe inclusion in a RegExp literal. Used by the
|
|
233
|
+
* site.yml line-level merge path.
|
|
234
|
+
*/
|
|
235
|
+
function escapeRegex(s) {
|
|
236
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Find the verbatim text of a single-line top-level YAML entry like
|
|
241
|
+
* `name: foo bar` or `foundation: my-foundation`. Returns the matched
|
|
242
|
+
* line (including any inline comment) or null if the key isn't present.
|
|
243
|
+
* Multi-line values (block scalars, nested maps) are deliberately
|
|
244
|
+
* unsupported — the merge path only uses this for simple scalar keys.
|
|
245
|
+
*/
|
|
246
|
+
function matchTopLevelLine(content, key) {
|
|
247
|
+
const match = content.match(new RegExp(`^${escapeRegex(key)}:.*$`, 'm'))
|
|
248
|
+
return match ? match[0] : null
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Replace the verbatim text of a single-line top-level YAML entry.
|
|
253
|
+
* Returns the content unchanged if the key isn't present. See
|
|
254
|
+
* matchTopLevelLine for scope.
|
|
255
|
+
*/
|
|
256
|
+
function replaceTopLevelLine(content, key, replacement) {
|
|
257
|
+
return content.replace(
|
|
258
|
+
new RegExp(`^${escapeRegex(key)}:.*$`, 'm'),
|
|
259
|
+
() => replacement,
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Resolve a dependency version string from a template.json entry.
|
|
265
|
+
*
|
|
266
|
+
* Template authors can either hardcode a concrete spec (`"^0.2.1"`) or
|
|
267
|
+
* use the same `{{version}}` Handlebars helper that `package.json.hbs`
|
|
268
|
+
* uses (`"{{version \"@uniweb/press\"}}"`). The helper is populated
|
|
269
|
+
* earlier in the scaffold flow via `registerVersions()`, so by the time
|
|
270
|
+
* this runs `versionData` already holds the current on-disk versions
|
|
271
|
+
* of every `@uniweb/*` package.
|
|
272
|
+
*
|
|
273
|
+
* Plain strings without `{{…}}` pass through untouched. Strings that
|
|
274
|
+
* fail to compile (rare — e.g. malformed mustache) fall back to the
|
|
275
|
+
* original literal so a bad template.json doesn't break scaffolding.
|
|
276
|
+
*/
|
|
277
|
+
function resolveDependencyVersion(rawValue) {
|
|
278
|
+
if (typeof rawValue !== 'string' || !rawValue.includes('{{')) {
|
|
279
|
+
return rawValue
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
return Handlebars.compile(rawValue)({})
|
|
283
|
+
} catch {
|
|
284
|
+
return rawValue
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
211
288
|
/**
|
|
212
289
|
* Merge additional dependencies from a content template into a scaffolded package.json
|
|
213
290
|
*
|
|
214
291
|
* Reads the package.json at the given path, adds any deps not already present
|
|
215
292
|
* (in either dependencies or devDependencies), and writes it back.
|
|
216
293
|
*
|
|
294
|
+
* Each version string in `deps` is first processed through the shared
|
|
295
|
+
* Handlebars pipeline so template.json entries can reference live
|
|
296
|
+
* workspace versions via `{{version "@uniweb/press"}}` instead of
|
|
297
|
+
* hardcoding a spec that goes stale on the next publish.
|
|
298
|
+
*
|
|
217
299
|
* @param {string} packageJsonPath - Absolute path to package.json
|
|
218
|
-
* @param {Object} deps - Dependencies to merge (name → version)
|
|
300
|
+
* @param {Object} deps - Dependencies to merge (name → version spec)
|
|
219
301
|
*/
|
|
220
302
|
export async function mergeTemplateDependencies(packageJsonPath, deps) {
|
|
221
303
|
if (!deps || Object.keys(deps).length === 0) return
|
|
@@ -223,7 +305,7 @@ export async function mergeTemplateDependencies(packageJsonPath, deps) {
|
|
|
223
305
|
if (!pkg.dependencies) pkg.dependencies = {}
|
|
224
306
|
for (const [name, version] of Object.entries(deps)) {
|
|
225
307
|
if (!pkg.dependencies[name] && !pkg.devDependencies?.[name]) {
|
|
226
|
-
pkg.dependencies[name] = version
|
|
308
|
+
pkg.dependencies[name] = resolveDependencyVersion(version)
|
|
227
309
|
}
|
|
228
310
|
}
|
|
229
311
|
await fs.writeFile(packageJsonPath, JSON.stringify(pkg, null, 2) + '\n')
|