uniweb 0.87.4 → 0.88.1

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.
@@ -14,43 +14,32 @@ import { fetchWithRetry as sharedFetchWithRetry } from '../../utils/fetch-retry.
14
14
  const TEMPLATES_REPO = 'uniweb/templates'
15
15
  const GITHUB_API = 'https://api.github.com'
16
16
 
17
- // Cache for manifest (avoid re-fetching in same session)
18
- let manifestCache = null
19
-
20
17
  /**
21
- * Fetch the manifest.json from the latest release
18
+ * Fetch manifest.json from one release of the official templates.
19
+ *
20
+ * ⛔ Always a named release — the one the CLI was published with — and never
21
+ * `latest`: the latest templates can be newer than the packages this CLI pins
22
+ * (templates/resolver.js, OFFICIAL_TEMPLATES_RELEASE).
22
23
  *
23
24
  * @param {Object} options - Fetch options
24
- * @param {string} options.version - Specific version tag (default: latest)
25
+ * @param {string} options.version - The release tag, e.g. `v0.18.4`
25
26
  * @param {Function} options.onProgress - Progress callback
26
- * @returns {Promise<Object>} { version, templates, downloadUrl }
27
+ * @returns {Promise<Object>} { version, templates, downloadUrlBase }
27
28
  */
28
29
  export async function fetchManifest(options = {}) {
29
30
  const { version, onProgress } = options
30
-
31
- // Return cached manifest if available and no specific version requested
32
- if (manifestCache && !version) {
33
- return manifestCache
34
- }
31
+ if (!version) throw new Error('fetchManifest needs the release tag to read')
35
32
 
36
33
  onProgress?.('Fetching template manifest...')
37
34
 
38
- // Get release info
39
- const releaseUrl = version
40
- ? `${GITHUB_API}/repos/${TEMPLATES_REPO}/releases/tags/${version}`
41
- : `${GITHUB_API}/repos/${TEMPLATES_REPO}/releases/latest`
42
-
43
- const releaseResponse = await fetchWithRetry(releaseUrl, {
44
- headers: getGitHubHeaders()
45
- })
35
+ const releaseResponse = await fetchWithRetry(
36
+ `${GITHUB_API}/repos/${TEMPLATES_REPO}/releases/tags/${version}`,
37
+ { headers: getGitHubHeaders() }
38
+ )
46
39
 
47
40
  if (!releaseResponse.ok) {
48
41
  if (releaseResponse.status === 404) {
49
- throw new Error(
50
- version
51
- ? `Release ${version} not found for ${TEMPLATES_REPO}`
52
- : `No releases found for ${TEMPLATES_REPO}`
53
- )
42
+ throw new Error(`Release ${version} not found for ${TEMPLATES_REPO}`)
54
43
  }
55
44
  await handleGitHubError(releaseResponse)
56
45
  }
@@ -80,28 +69,20 @@ export async function fetchManifest(options = {}) {
80
69
 
81
70
  const manifest = await manifestResponse.json()
82
71
 
83
- // Build result with download URL base
84
- const result = {
72
+ return {
85
73
  version: release.tag_name,
86
74
  templates: manifest.templates || {},
87
75
  // Base URL for downloading template tarballs
88
76
  downloadUrlBase: `https://github.com/${TEMPLATES_REPO}/releases/download/${release.tag_name}`
89
77
  }
90
-
91
- // Cache if this was a "latest" fetch
92
- if (!version) {
93
- manifestCache = result
94
- }
95
-
96
- return result
97
78
  }
98
79
 
99
80
  /**
100
- * Fetch a specific template from GitHub releases
81
+ * Fetch a template from one release of the official templates
101
82
  *
102
83
  * @param {string} name - Template name (e.g., 'marketing')
103
84
  * @param {Object} options - Fetch options
104
- * @param {string} options.version - Specific version tag (default: latest)
85
+ * @param {string} options.version - The release tag, e.g. `v0.18.4`
105
86
  * @param {Function} options.onProgress - Progress callback
106
87
  * @returns {Promise<Object>} { tempDir, version, metadata }
107
88
  */
@@ -166,33 +147,6 @@ export async function fetchOfficialTemplate(name, options = {}) {
166
147
  }
167
148
  }
168
149
 
169
- /**
170
- * List available templates from the latest release
171
- *
172
- * @param {Object} options - Fetch options
173
- * @param {Function} options.onProgress - Progress callback
174
- * @returns {Promise<Array>} List of template metadata
175
- */
176
- export async function listOfficialTemplates(options = {}) {
177
- try {
178
- const manifest = await fetchManifest(options)
179
- return Object.entries(manifest.templates).map(([id, info]) => ({
180
- id,
181
- ...info
182
- }))
183
- } catch {
184
- // Return empty list if can't fetch
185
- return []
186
- }
187
- }
188
-
189
- /**
190
- * Clear the manifest cache
191
- */
192
- export function clearManifestCache() {
193
- manifestCache = null
194
- }
195
-
196
150
  /**
197
151
  * Get GitHub API headers
198
152
  */
@@ -12,15 +12,13 @@ import {
12
12
  getTemplateDisplayName,
13
13
  BUILTIN_TEMPLATES,
14
14
  OFFICIAL_TEMPLATES,
15
- buildTemplateChoices
15
+ OFFICIAL_TEMPLATES_RELEASE,
16
+ buildTemplateChoices,
17
+ templatePickerPageSize
16
18
  } from './resolver.js'
17
19
  import { fetchNpmTemplate } from './fetchers/npm.js'
18
20
  import { fetchGitHubTemplate } from './fetchers/github.js'
19
- import {
20
- fetchOfficialTemplate,
21
- listOfficialTemplates
22
- } from './fetchers/release.js'
23
- import { validateTemplate } from './validator.js'
21
+ import { fetchOfficialTemplate } from './fetchers/release.js'
24
22
 
25
23
  /**
26
24
  * Resolve a template identifier and return the template path
@@ -61,12 +59,20 @@ export async function resolveTemplate(identifier, options = {}) {
61
59
  }
62
60
 
63
61
  /**
64
- * Resolve an official template from GitHub releases
62
+ * Resolve an official template from the release this CLI was published with —
63
+ * never `latest`, so its content matches the package versions the scaffold pins
64
+ * (resolver.js, OFFICIAL_TEMPLATES_RELEASE).
65
65
  */
66
66
  async function resolveOfficialTemplate(name, options = {}) {
67
67
  const { onProgress } = options
68
68
 
69
+ if (!OFFICIAL_TEMPLATES_RELEASE) {
70
+ throw new Error(
71
+ "This CLI's framework index names no @uniweb/templates release to download from."
72
+ )
73
+ }
69
74
  const { tempDir, baseTempDir, version } = await fetchOfficialTemplate(name, {
75
+ version: OFFICIAL_TEMPLATES_RELEASE,
70
76
  onProgress
71
77
  })
72
78
 
@@ -173,35 +179,12 @@ async function resolveLocalTemplate(templatePath, options = {}) {
173
179
  }
174
180
  }
175
181
 
176
- /**
177
- * List all available templates
178
- */
179
- export async function listAvailableTemplates() {
180
- const templates = []
181
-
182
- // Official templates from GitHub releases
183
- try {
184
- const official = await listOfficialTemplates()
185
- for (const t of official) {
186
- templates.push({
187
- type: 'official',
188
- id: t.id,
189
- name: t.name || t.id,
190
- description: t.description || ''
191
- })
192
- }
193
- } catch {
194
- // Ignore errors - templates just won't be listed
195
- }
196
-
197
- return templates
198
- }
199
-
200
182
  // Re-export for convenience
201
183
  export {
202
184
  parseTemplateId,
203
185
  getTemplateDisplayName,
204
186
  BUILTIN_TEMPLATES,
205
187
  OFFICIAL_TEMPLATES,
206
- buildTemplateChoices
188
+ buildTemplateChoices,
189
+ templatePickerPageSize
207
190
  }
@@ -2,93 +2,48 @@
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))
5
+ import { loadFrameworkIndex } from '../versions.js'
10
6
 
11
7
  // Built-in templates (programmatic, not file-based)
12
8
  export const BUILTIN_TEMPLATES = ['blank', 'starter', 'none']
13
9
 
14
10
  /**
15
- * Load the official template metadata map — `{ id: { name, description,
16
- * tags } }`, keyed by template id.
17
- *
18
- * There are two sources of truth depending on where the CLI is running:
11
+ * THE OFFICIAL TEMPLATES THIS CLI SCAFFOLDS — one snapshot.
19
12
  *
20
- * 1. **Local dev inside the Uniweb monorepo** — the authoritative file
21
- * is `framework/templates/manifest.json`. Adding a new template
22
- * there makes it immediately reachable from any locally-run CLI
23
- * without republishing. This is the only path that matters for
24
- * `node scripts/framework/sandbox.js create`.
13
+ * `../framework-index.json` is written when the CLI is published: the
14
+ * templates' manifest (`{ id: { name, description, tags } }`) and, in
15
+ * `packages`, the `@uniweb/templates` version whose release holds their
16
+ * content. The interactive picker, `uniweb template list` and the download in
17
+ * `create` all read it, so the CLI offers exactly the templates it can fetch,
18
+ * and fetches the content released with the package versions it pins. A CLI
19
+ * goes on scaffolding the templates it was published with until it is
20
+ * updated — consistent, not newest.
25
21
  *
26
- * 2. **Published CLI (npm-installed)** — the monorepo isn't on disk, so
27
- * we read the vendored framework index at `../framework-index.json`,
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.
22
+ * ⛔ Until 2026-10-08 a CLI run from a source checkout listed the templates
23
+ * repo's working manifest instead, and every CLI downloaded from the `latest`
24
+ * release. So a CLI could offer a template its release did not hold, and
25
+ * scaffold content newer than the packages it pinned: a template using a kit
26
+ * export the CLI's kit did not have yet failed to build.
33
27
  *
34
- * When both sources are available (local dev with a committed snapshot),
35
- * the live workspace manifest wins so newly-added templates are visible
36
- * without waiting for a CLI republish.
37
- *
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.)
28
+ * (An earlier version still hardcoded the picker's list, which silently
29
+ * drifted whenever a template was added — the reason no list is written here.)
44
30
  */
45
- function loadOfficialTemplateMap() {
46
- // Local dev: framework/templates/manifest.json relative to this file
47
- // at framework/cli/src/templates/resolver.js
48
- const workspaceManifest = join(
49
- __dirname,
50
- '..',
51
- '..',
52
- '..',
53
- 'templates',
54
- 'manifest.json'
55
- )
56
- const picked = tryReadTemplateMap(workspaceManifest)
57
- if (picked) return picked
58
-
59
- // Published CLI fallback: the framework index snapshot, one directory
60
- // up at framework/cli/src/framework-index.json.
61
- const indexPath = join(__dirname, '..', 'framework-index.json')
62
- const fromIndex = tryReadTemplateMap(indexPath)
63
- if (fromIndex) return fromIndex
64
-
65
- // If both sources fail, return an empty map rather than a stale
66
- // hardcoded list. An unknown template name then falls through to
67
- // the npm `@uniweb/template-<name>` lookup path, which is the
68
- // intended behavior for third-party templates.
69
- return {}
70
- }
71
-
72
- function tryReadTemplateMap(path) {
73
- try {
74
- if (!statSync(path).isFile()) return null
75
- const data = JSON.parse(readFileSync(path, 'utf8'))
76
- if (data && data.templates && typeof data.templates === 'object') {
77
- return data.templates
78
- }
79
- } catch {}
80
- return null
81
- }
31
+ const INDEX = loadFrameworkIndex()
82
32
 
83
- // Official template metadata keyed by id. Derived from manifest.json (local
84
- // dev) or framework-index.json (published CLI) at module load time — see
85
- // loadOfficialTemplateMap() for details. Read once per process; to reflect a
86
- // just-added template, restart the CLI or rerun the scaffolder.
87
- export const OFFICIAL_TEMPLATE_MAP = loadOfficialTemplateMap()
33
+ // Official template metadata keyed by id. Empty when the snapshot did not load:
34
+ // an unknown name then falls through to the npm `@uniweb/template-<name>`
35
+ // lookup, the path for third-party templates, rather than to a stale list.
36
+ export const OFFICIAL_TEMPLATE_MAP =
37
+ INDEX?.templates && typeof INDEX.templates === 'object' ? INDEX.templates : {}
88
38
 
89
39
  // Official template ids, e.g. ['marketing', 'docs', 'academic', …].
90
40
  export const OFFICIAL_TEMPLATES = Object.keys(OFFICIAL_TEMPLATE_MAP)
91
41
 
42
+ // The release the official templates download from — `v<@uniweb/templates
43
+ // version>`, the repo's tag — or null when the snapshot names none.
44
+ const TEMPLATES_VERSION = INDEX?.packages?.['@uniweb/templates']?.version
45
+ export const OFFICIAL_TEMPLATES_RELEASE = TEMPLATES_VERSION ? `v${TEMPLATES_VERSION}` : null
46
+
92
47
  // Built-in (programmatic) picker entries — not in the manifest; the CLI
93
48
  // generates these itself. "Blank" trails the official templates.
94
49
  const BUILTIN_LEAD_CHOICES = [
@@ -128,6 +83,24 @@ export function buildTemplateChoices() {
128
83
  return [...BUILTIN_LEAD_CHOICES, ...official, BLANK_CHOICE]
129
84
  }
130
85
 
86
+ /**
87
+ * How many options the `create` picker shows at once: all of them, when the
88
+ * terminal has the rows. prompts shows 10 by default and scrolls the rest, and
89
+ * past ten choices that put most of the official templates below the fold —
90
+ * a template you have to scroll to find reads as one that is not there.
91
+ *
92
+ * Seven rows are kept back: the question, and the highlighted choice's
93
+ * description, which wraps under it when it is long.
94
+ *
95
+ * @param {number} count - the picker's choices
96
+ * @param {number} [rows] - the terminal's height, when it reports one
97
+ * @returns {number}
98
+ */
99
+ export function templatePickerPageSize(count, rows = process.stdout.rows) {
100
+ const room = Number.isFinite(rows) && rows > 0 ? rows - 7 : count
101
+ return Math.max(10, Math.min(count, room))
102
+ }
103
+
131
104
  /**
132
105
  * Parse a template identifier and determine its source type
133
106
  *
@@ -1,84 +1,25 @@
1
1
  /**
2
- * Template validation - checks template.json and compatibility
2
+ * Template validation — reads template.json and finds the content directories
3
+ *
4
+ * ⛔ Two things this used to do are retired [2026-10-08]:
5
+ * - `compatible` (alias `uniweb`), a CLI version range a template declared. It
6
+ * was checked only when a caller passed the CLI's version, and none ever
7
+ * did. An official template now comes from the release its CLI was
8
+ * published with (resolver.js), so it cannot be newer than that CLI.
9
+ * - Unwrapping a package folder that keeps its code under `src/` — the layout of
10
+ * the v0.7 releases (`foundation/src/foundation.js`). Such a folder is now
11
+ * REFUSED, naming the fix (validateTemplate). ⛔ The unwrap went in 0.88.0
12
+ * with a note that the official templates were flat, but its condition was any
13
+ * `src/main.js` too, and `extensions/effects/` still relied on it: 0.88.0
14
+ * copied that folder nested, and every project made from `extensions` failed
15
+ * to build, saying only that it found no section types. The template is flat
16
+ * since templates 0.18.6.
3
17
  */
4
18
 
5
19
  import fs from 'node:fs/promises'
6
20
  import { existsSync } from 'node:fs'
7
21
  import path from 'node:path'
8
22
 
9
- /**
10
- * Simple semver satisfaction check
11
- * Supports: >=x.y.z, ^x.y.z, ~x.y.z, x.y.z, x.y.x, *, latest
12
- */
13
- export function satisfiesVersion(version, range) {
14
- if (!range || range === '*' || range === 'latest') {
15
- return true
16
- }
17
-
18
- const parseVersion = (v) => {
19
- const match = v.match(/^(\d+)\.(\d+)\.(\d+)/)
20
- if (!match) return null
21
- return {
22
- major: parseInt(match[1], 10),
23
- minor: parseInt(match[2], 10),
24
- patch: parseInt(match[3], 10)
25
- }
26
- }
27
-
28
- const current = parseVersion(version)
29
- if (!current) return true // Can't parse, assume compatible
30
-
31
- // Handle different range formats
32
- if (range.startsWith('>=')) {
33
- const min = parseVersion(range.slice(2))
34
- if (!min) return true
35
- if (current.major > min.major) return true
36
- if (current.major < min.major) return false
37
- if (current.minor > min.minor) return true
38
- if (current.minor < min.minor) return false
39
- return current.patch >= min.patch
40
- }
41
-
42
- if (range.startsWith('^')) {
43
- // ^x.y.z means >=x.y.z and <(x+1).0.0
44
- const min = parseVersion(range.slice(1))
45
- if (!min) return true
46
- if (current.major !== min.major)
47
- return current.major > min.major && min.major === 0
48
- if (current.minor > min.minor) return true
49
- if (current.minor < min.minor) return false
50
- return current.patch >= min.patch
51
- }
52
-
53
- if (range.startsWith('~')) {
54
- // ~x.y.z means >=x.y.z and <x.(y+1).0
55
- const min = parseVersion(range.slice(1))
56
- if (!min) return true
57
- if (current.major !== min.major) return false
58
- if (current.minor !== min.minor) return false
59
- return current.patch >= min.patch
60
- }
61
-
62
- // Exact version or x.y.x pattern
63
- if (range.includes('x')) {
64
- const parts = range.split('.')
65
- const min = parseVersion(range.replace(/x/g, '0'))
66
- if (!min) return true
67
- if (parts[0] !== 'x' && current.major !== min.major) return false
68
- if (parts[1] !== 'x' && current.minor !== min.minor) return false
69
- return true
70
- }
71
-
72
- // Exact match
73
- const exact = parseVersion(range)
74
- if (!exact) return true
75
- return (
76
- current.major === exact.major &&
77
- current.minor === exact.minor &&
78
- current.patch === exact.patch
79
- )
80
- }
81
-
82
23
  /**
83
24
  * Validation error with structured details
84
25
  */
@@ -96,20 +37,16 @@ export const ErrorCodes = {
96
37
  INVALID_TEMPLATE_JSON: 'INVALID_TEMPLATE_JSON',
97
38
  MISSING_CONTENT_DIR: 'MISSING_CONTENT_DIR',
98
39
  MISSING_REQUIRED_FIELD: 'MISSING_REQUIRED_FIELD',
99
- VERSION_MISMATCH: 'VERSION_MISMATCH'
40
+ NESTED_PACKAGE_CODE: 'NESTED_PACKAGE_CODE'
100
41
  }
101
42
 
102
43
  /**
103
44
  * Validate a template directory structure and metadata
104
45
  *
105
46
  * @param {string} templateRoot - Path to the template root (contains template.json)
106
- * @param {Object} options - Validation options
107
- * @param {string} options.uniwebVersion - Current Uniweb version to check compatibility
108
47
  * @returns {Object} Parsed and validated template metadata
109
48
  */
110
- export async function validateTemplate(templateRoot, options = {}) {
111
- const { uniwebVersion } = options
112
-
49
+ export async function validateTemplate(templateRoot) {
113
50
  // Check for template.json
114
51
  const metadataPath = path.join(templateRoot, 'template.json')
115
52
  if (!existsSync(metadataPath)) {
@@ -142,18 +79,6 @@ export async function validateTemplate(templateRoot, options = {}) {
142
79
  )
143
80
  }
144
81
 
145
- // Check version compatibility (support both `compatible` and `uniweb` fields)
146
- const versionRange = metadata.compatible || metadata.uniweb
147
- if (uniwebVersion && versionRange) {
148
- if (!satisfiesVersion(uniwebVersion, versionRange)) {
149
- throw new ValidationError(
150
- `Template requires Uniweb ${versionRange}, but current version is ${uniwebVersion}`,
151
- ErrorCodes.VERSION_MISMATCH,
152
- { required: versionRange, current: uniwebVersion }
153
- )
154
- }
155
- }
156
-
157
82
  // Format 2: content template — foundation/ and/or site/ directories alongside template.json
158
83
  const contentDirs = resolveContentDirs(templateRoot, metadata)
159
84
 
@@ -165,6 +90,23 @@ export async function validateTemplate(templateRoot, options = {}) {
165
90
  )
166
91
  }
167
92
 
93
+ // A foundation or extension folder is copied over the package the CLI
94
+ // scaffolds, as it is. Code kept under src/ would land beside the scaffold's
95
+ // own empty sections/, and the project would fail to build saying only that it
96
+ // found no section types — so it stops here, saying what to move.
97
+ for (const { type, name, dir } of contentDirs) {
98
+ if (type !== 'foundation' && type !== 'extension') continue
99
+ if (['main.js', 'foundation.js'].some((file) => existsSync(path.join(dir, 'src', file)))) {
100
+ throw new ValidationError(
101
+ `The template's ${name}/ keeps its code in ${name}/src/, a layout this CLI does not scaffold. ` +
102
+ `A foundation or extension folder holds main.js, sections/ and styles.css at its root: ` +
103
+ `move ${name}/src/'s contents up into ${name}/.`,
104
+ ErrorCodes.NESTED_PACKAGE_CODE,
105
+ { dir }
106
+ )
107
+ }
108
+ }
109
+
168
110
  return {
169
111
  ...metadata,
170
112
  format: 2,
@@ -178,7 +120,7 @@ export async function validateTemplate(templateRoot, options = {}) {
178
120
  *
179
121
  * @param {string} templateRoot - Root of the template (contains template.json)
180
122
  * @param {Object} metadata - Parsed template.json
181
- * @returns {Array<Object>} Content directories: [{ type, name, dir, foundation?, renames? }]
123
+ * @returns {Array<Object>} Content directories: [{ type, name, dir, foundation? }]
182
124
  */
183
125
  export function resolveContentDirs(templateRoot, metadata) {
184
126
  const dirs = []
@@ -188,29 +130,19 @@ export function resolveContentDirs(templateRoot, metadata) {
188
130
  for (const pkg of metadata.packages) {
189
131
  const dir = path.join(templateRoot, pkg.name)
190
132
  if (existsSync(dir)) {
191
- const entry = {
133
+ dirs.push({
192
134
  type: pkg.type,
193
135
  name: pkg.name,
194
136
  dir,
195
137
  ...(pkg.foundation ? { foundation: pkg.foundation } : {})
196
- }
197
- if (entry.type === 'foundation' || entry.type === 'extension') {
198
- applyLegacyFoundationLayout(entry)
199
- }
200
- dirs.push(entry)
138
+ })
201
139
  }
202
140
  }
203
141
  } else {
204
142
  // Standard template: look for foundation/ and site/
205
143
  const foundationDir = path.join(templateRoot, 'foundation')
206
144
  if (existsSync(foundationDir)) {
207
- const entry = {
208
- type: 'foundation',
209
- name: 'foundation',
210
- dir: foundationDir
211
- }
212
- applyLegacyFoundationLayout(entry)
213
- dirs.push(entry)
145
+ dirs.push({ type: 'foundation', name: 'foundation', dir: foundationDir })
214
146
  }
215
147
 
216
148
  const siteDir = path.join(templateRoot, 'site')
@@ -221,83 +153,3 @@ export function resolveContentDirs(templateRoot, metadata) {
221
153
 
222
154
  return dirs
223
155
  }
224
-
225
- /**
226
- * Detect and unwrap the legacy foundation layout.
227
- *
228
- * Old templates (published in the `uniweb/templates` releases up to
229
- * v0.7.x) shipped foundation content nested one level deeper, with the
230
- * package source under `foundation/src/` and the user-authored
231
- * declarations file named `foundation.js`:
232
- *
233
- * <template>/foundation/src/foundation.js
234
- * <template>/foundation/src/sections/...
235
- * <template>/foundation/src/components/...
236
- *
237
- * The current layout is flat — the foundation package root contains
238
- * the source directly, and the declarations file is named `main.js`:
239
- *
240
- * <template>/foundation/main.js
241
- * <template>/foundation/sections/...
242
- *
243
- * The CLI scaffolds the new flat shape into the project's `src/`
244
- * directory, so an unmodified copy of an old-format template would
245
- * land at `src/src/foundation.js` (extra `src/` layer + old name).
246
- *
247
- * This helper detects the legacy marker (`<dir>/src/foundation.js`),
248
- * mutates the contentDir entry to point at the inner `src/` directory,
249
- * and records a top-level rename so `foundation.js` is written as
250
- * `main.js`. Once `uniweb/templates` is republished with the flat
251
- * layout, this branch becomes a no-op.
252
- */
253
- function applyLegacyFoundationLayout(entry) {
254
- const innerSrc = path.join(entry.dir, 'src')
255
- if (!existsSync(innerSrc)) return
256
-
257
- const legacyMain = path.join(innerSrc, 'foundation.js')
258
- const newMain = path.join(innerSrc, 'main.js')
259
- if (!existsSync(legacyMain) && !existsSync(newMain)) return
260
-
261
- entry.dir = innerSrc
262
- if (existsSync(legacyMain)) {
263
- entry.renames = { ...(entry.renames || {}), 'foundation.js': 'main.js' }
264
- }
265
- }
266
-
267
- /**
268
- * Get list of available templates in a templates directory
269
- *
270
- * @param {string} templatesDir - Path to directory containing templates
271
- * @returns {Array<Object>} List of template metadata
272
- */
273
- export async function listTemplates(templatesDir) {
274
- if (!existsSync(templatesDir)) {
275
- return []
276
- }
277
-
278
- const entries = await fs.readdir(templatesDir, { withFileTypes: true })
279
- const templates = []
280
-
281
- for (const entry of entries) {
282
- if (!entry.isDirectory()) continue
283
-
284
- const templatePath = path.join(templatesDir, entry.name)
285
- const metadataPath = path.join(templatePath, 'template.json')
286
-
287
- if (existsSync(metadataPath)) {
288
- try {
289
- const content = await fs.readFile(metadataPath, 'utf8')
290
- const metadata = JSON.parse(content)
291
- templates.push({
292
- id: entry.name,
293
- ...metadata,
294
- path: templatePath
295
- })
296
- } catch {
297
- // Skip invalid templates
298
- }
299
- }
300
- }
301
-
302
- return templates
303
- }