uniweb 0.11.0 → 0.12.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.
package/src/utils/auth.js CHANGED
@@ -5,6 +5,22 @@
5
5
  * User-global (not workspace-local) — you publish as yourself, not as a project.
6
6
  *
7
7
  * Used by `login`, `publish`, and `deploy` commands.
8
+ *
9
+ * Stored shape (auth.json):
10
+ * {
11
+ * token: string, // bearer JWT, sent in Authorization: Bearer <token>
12
+ * email: string, // signup_email; permanent, deliverable
13
+ * loginName?: string, // PHP session login_name; immutable per session model
14
+ * sub?: string, // memberId from JWT; permanent, numeric
15
+ * namespaces?: string[], // org handles the user can publish under
16
+ * expiresAt?: string // ISO timestamp; JWT exp claim
17
+ * }
18
+ *
19
+ * The extra identity fields (loginName, sub, namespaces) are decoded from
20
+ * the JWT at write time and persisted alongside the token. They're cheap
21
+ * to derive (HS256 payload is base64url-encoded JSON), but persisting them
22
+ * means callers don't need to decode the JWT themselves to ask
23
+ * "who is the user?" — they just `readAuth()`.
8
24
  */
9
25
 
10
26
  import { existsSync } from 'node:fs'
@@ -29,28 +45,88 @@ export function getAuthPath() {
29
45
  }
30
46
 
31
47
  /**
32
- * Read stored credentials.
33
- * @returns {Promise<{ token: string, email: string, expiresAt?: string } | null>}
48
+ * Decode the payload of a JWT. Returns `null` for malformed tokens.
49
+ * No signature verification that's the server's job; we just want to
50
+ * read the claims locally.
51
+ *
52
+ * @param {string} token
53
+ * @returns {Object|null}
54
+ */
55
+ export function decodeJwtPayload(token) {
56
+ if (typeof token !== 'string') return null
57
+ const parts = token.split('.')
58
+ if (parts.length < 2) return null
59
+ try {
60
+ // base64url → base64
61
+ const b64 = parts[1].replace(/-/g, '+').replace(/_/g, '/')
62
+ return JSON.parse(Buffer.from(b64, 'base64').toString('utf8'))
63
+ } catch {
64
+ return null
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Read stored credentials. If the persisted record predates the
70
+ * identity-fields plumbing (no loginName/sub/namespaces) but has a
71
+ * token, derive the missing fields from the JWT in memory so callers
72
+ * see a consistent shape regardless of write generation.
73
+ *
74
+ * @returns {Promise<{ token: string, email: string, loginName?: string, sub?: string, namespaces?: string[], expiresAt?: string } | null>}
34
75
  */
35
76
  export async function readAuth() {
36
77
  const authPath = getAuthPath()
37
78
  if (!existsSync(authPath)) return null
38
79
 
80
+ let auth
39
81
  try {
40
- return JSON.parse(await readFile(authPath, 'utf8'))
82
+ auth = JSON.parse(await readFile(authPath, 'utf8'))
41
83
  } catch {
42
84
  return null
43
85
  }
86
+
87
+ // Backfill identity fields from the JWT for older auth.json files
88
+ // that were written before this plumbing existed. Read-only — the
89
+ // file isn't rewritten until the next login.
90
+ if (auth?.token && (auth.loginName === undefined || auth.sub === undefined || auth.namespaces === undefined)) {
91
+ const payload = decodeJwtPayload(auth.token)
92
+ if (payload) {
93
+ if (auth.loginName === undefined && typeof payload.loginName === 'string') {
94
+ auth.loginName = payload.loginName
95
+ }
96
+ if (auth.sub === undefined && typeof payload.sub === 'string') {
97
+ auth.sub = payload.sub
98
+ }
99
+ if (auth.namespaces === undefined && Array.isArray(payload.namespaces)) {
100
+ auth.namespaces = payload.namespaces
101
+ }
102
+ }
103
+ }
104
+
105
+ return auth
44
106
  }
45
107
 
46
108
  /**
47
- * Write credentials to storage.
48
- * @param {{ token: string, email: string, expiresAt?: string }} auth
109
+ * Write credentials to storage. Decodes the JWT and persists the
110
+ * identity claims (loginName, sub, namespaces) alongside the token,
111
+ * so future `readAuth()` calls don't have to decode it themselves.
112
+ *
113
+ * @param {{ token: string, email: string, expiresAt?: string }} auth - Caller passes the basics; identity fields are derived.
49
114
  */
50
115
  export async function writeAuth(auth) {
116
+ const record = { ...auth }
117
+
118
+ if (record.token) {
119
+ const payload = decodeJwtPayload(record.token)
120
+ if (payload) {
121
+ if (typeof payload.loginName === 'string') record.loginName = payload.loginName
122
+ if (typeof payload.sub === 'string') record.sub = payload.sub
123
+ if (Array.isArray(payload.namespaces)) record.namespaces = payload.namespaces
124
+ }
125
+ }
126
+
51
127
  const dir = getAuthDir()
52
128
  await mkdir(dir, { recursive: true })
53
- await writeFile(join(dir, 'auth.json'), JSON.stringify(auth, null, 2))
129
+ await writeFile(join(dir, 'auth.json'), JSON.stringify(record, null, 2))
54
130
  }
55
131
 
56
132
  /**
@@ -14,12 +14,19 @@ import { join } from 'node:path'
14
14
  * Names that must not be used as package names.
15
15
  * - JS module keywords: default, undefined, null, true, false
16
16
  * - Node/filesystem: node_modules, package
17
- * - Common directory names that would cause confusion: src, dist, build
17
+ * - Common build-output directories: dist, build (would shadow `dist/` /
18
+ * `build/` references)
19
+ *
20
+ * Note: `src` is NOT reserved. A foundation in `src/` whose package name is
21
+ * also `src` is the default scaffold pattern — folder name and package name
22
+ * match. Multi-foundation co-located workspaces still use suffixes
23
+ * (`<project>-src`) because pnpm requires workspace-unique package names;
24
+ * that's a real constraint, not aesthetic.
18
25
  */
19
26
  const RESERVED_NAMES = new Set([
20
27
  'default', 'undefined', 'null', 'true', 'false',
21
28
  'node_modules', 'package',
22
- 'src', 'dist', 'build',
29
+ 'dist', 'build',
23
30
  ])
24
31
 
25
32
  /**
@@ -1,20 +1,36 @@
1
1
  /**
2
2
  * Local Foundation Registry
3
3
  *
4
- * Manages published foundations in .unicloud/registry/.
5
- * Same on-disk format as scripts/platform/registry.js, so
6
- * scripts/platform/serve.js can still serve them.
4
+ * Manages published foundations in .unicloud/registry/. The on-disk
5
+ * shape mirrors uniweb-edge's registry index (versions as an array of
6
+ * { version, ... } objects, plus top-level namespace and latest) so
7
+ * `--local` exercises the same data shape that ships in production.
7
8
  *
8
9
  * Layout:
9
10
  * .unicloud/
10
11
  * registry/
11
- * index.json # { "name": { versions: { "1.0.0": { ... } } } }
12
+ * index.json # see "Index format" below
12
13
  * packages/
13
14
  * name/
14
15
  * 1.0.0/
15
16
  * foundation.js
16
17
  * schema.json
17
18
  * assets/...
19
+ *
20
+ * Index format:
21
+ * {
22
+ * "@ns/name": {
23
+ * namespace: "ns",
24
+ * versions: [
25
+ * { version: "1.0.0", publishedAt, publishedBy, ... },
26
+ * ...
27
+ * ],
28
+ * latest: "1.0.0"
29
+ * }
30
+ * }
31
+ *
32
+ * Legacy entries (versions as an object keyed by version) are migrated
33
+ * to this shape on read. The next write persists the new shape.
18
34
  */
19
35
 
20
36
  import { existsSync } from 'node:fs'
@@ -37,7 +53,7 @@ export function getRegistryDir(startDir = process.cwd()) {
37
53
 
38
54
  /**
39
55
  * Sanitize a package name for filesystem use.
40
- * '@org/pkg' → '@org__pkg'
56
+ * '@org/pkg' → 'org/pkg'
41
57
  * @param {string} name
42
58
  * @returns {string}
43
59
  */
@@ -46,6 +62,38 @@ function sanitizeName(name) {
46
62
  return name.startsWith('@') ? name.slice(1) : name
47
63
  }
48
64
 
65
+ /**
66
+ * Parse the namespace out of a scoped package name. '@org/pkg' → 'org';
67
+ * unscoped → ''.
68
+ * @param {string} name
69
+ * @returns {string}
70
+ */
71
+ function parseNamespace(name) {
72
+ const m = /^@([a-z0-9_-]+)\//.exec(name)
73
+ return m ? m[1] : ''
74
+ }
75
+
76
+ /**
77
+ * Migrate a legacy index entry (versions as object, no namespace/latest)
78
+ * to the current shape (versions as array, namespace + latest at top).
79
+ * Mutates and returns the entry.
80
+ */
81
+ function normalizeEntry(name, entry) {
82
+ if (!entry) return entry
83
+ if (entry.versions && !Array.isArray(entry.versions) && typeof entry.versions === 'object') {
84
+ entry.versions = Object.entries(entry.versions).map(([version, data]) => ({
85
+ version,
86
+ ...data,
87
+ }))
88
+ }
89
+ if (!Array.isArray(entry.versions)) entry.versions = []
90
+ if (!entry.namespace) entry.namespace = parseNamespace(name)
91
+ if (!entry.latest && entry.versions.length > 0) {
92
+ entry.latest = entry.versions[entry.versions.length - 1].version
93
+ }
94
+ return entry
95
+ }
96
+
49
97
  /**
50
98
  * Local registry — stores published foundations in .unicloud/registry/
51
99
  */
@@ -58,7 +106,11 @@ export class LocalRegistry {
58
106
 
59
107
  async _readIndex() {
60
108
  if (!existsSync(this.indexPath)) return {}
61
- return JSON.parse(await readFile(this.indexPath, 'utf8'))
109
+ const raw = JSON.parse(await readFile(this.indexPath, 'utf8'))
110
+ for (const name of Object.keys(raw)) {
111
+ normalizeEntry(name, raw[name])
112
+ }
113
+ return raw
62
114
  }
63
115
 
64
116
  async _writeIndex(index) {
@@ -74,17 +126,20 @@ export class LocalRegistry {
74
126
  */
75
127
  async exists(name, version) {
76
128
  const index = await this._readIndex()
77
- return !!index[name]?.versions?.[version]
129
+ const versions = index[name]?.versions
130
+ if (!Array.isArray(versions)) return false
131
+ return versions.some(v => v.version === version)
78
132
  }
79
133
 
80
134
  /**
81
- * Get all published versions for a package.
135
+ * Get all published versions for a package as an array of
136
+ * `{ version, publishedAt, ... }` entries (matches uniweb-edge).
82
137
  * @param {string} name
83
- * @returns {Promise<Object>}
138
+ * @returns {Promise<Array>}
84
139
  */
85
140
  async getVersions(name) {
86
141
  const index = await this._readIndex()
87
- return index[name]?.versions || {}
142
+ return index[name]?.versions || []
88
143
  }
89
144
 
90
145
  /**
@@ -104,12 +159,26 @@ export class LocalRegistry {
104
159
 
105
160
  const index = await this._readIndex()
106
161
  if (!index[name]) {
107
- index[name] = { versions: {} }
162
+ index[name] = {
163
+ namespace: parseNamespace(name),
164
+ versions: [],
165
+ latest: null,
166
+ }
108
167
  }
109
- index[name].versions[version] = {
168
+
169
+ const versionEntry = {
170
+ version,
110
171
  publishedAt: new Date().toISOString(),
111
172
  ...metadata,
112
173
  }
174
+ const existingIdx = index[name].versions.findIndex(v => v.version === version)
175
+ if (existingIdx >= 0) {
176
+ index[name].versions[existingIdx] = versionEntry
177
+ } else {
178
+ index[name].versions.push(versionEntry)
179
+ }
180
+ index[name].latest = version
181
+
113
182
  await this._writeIndex(index)
114
183
  }
115
184
 
@@ -165,20 +234,23 @@ export class RemoteRegistry {
165
234
  async exists(name, version) {
166
235
  try {
167
236
  const index = await this._fetchIndex()
168
- return !!index[name]?.versions?.[version]
237
+ const versions = index[name]?.versions
238
+ if (!Array.isArray(versions)) return false
239
+ return versions.some(v => v.version === version)
169
240
  } catch {
170
241
  return false
171
242
  }
172
243
  }
173
244
 
174
245
  /**
175
- * Get all published versions for a package.
246
+ * Get all published versions for a package as an array of
247
+ * `{ version, publishedAt, ... }` entries.
176
248
  * @param {string} name
177
- * @returns {Promise<Object>}
249
+ * @returns {Promise<Array>}
178
250
  */
179
251
  async getVersions(name) {
180
252
  const index = await this._fetchIndex()
181
- return index[name]?.versions || {}
253
+ return index[name]?.versions || []
182
254
  }
183
255
 
184
256
  /**
@@ -94,12 +94,23 @@ export async function scaffoldSite(targetDir, context, options = {}) {
94
94
  * Apply content overlay from a content directory onto a target
95
95
  *
96
96
  * Content files overwrite scaffolded defaults. Structural files
97
- * (package.json, vite.config.js, main.js, index.html) are NOT overwritten.
97
+ * (package.json, vite.config.js, entry.js, index.html) are NOT overwritten.
98
+ *
99
+ * Note: a foundation's `main.js` (the user-authored declarations file) is
100
+ * NOT structural — templates legitimately provide their own `main.js` to
101
+ * override the empty scaffold default. The site's `entry.js` (formerly
102
+ * `main.js`) IS structural — the boilerplate `start({...})` is identical
103
+ * across all sites and shouldn't be overwritten.
98
104
  *
99
105
  * @param {string} contentDir - Source content directory (e.g., starter/foundation/)
100
106
  * @param {string} targetDir - Target directory to overlay onto
101
107
  * @param {Object} context - Handlebars context for .hbs files
102
108
  * @param {Object} [options] - Processing options
109
+ * @param {Object} [options.renames] - Top-level filename remapping
110
+ * (e.g. `{ 'foundation.js': 'main.js' }`). Applied only at depth 0
111
+ * so renames don't accidentally rewrite same-named files in nested
112
+ * directories. Used to migrate legacy `foundation/foundation.js`
113
+ * templates onto the new flat `src/main.js` layout.
103
114
  */
104
115
  export async function applyContent(contentDir, targetDir, context, options = {}) {
105
116
  if (!existsSync(contentDir)) return
@@ -110,7 +121,7 @@ export async function applyContent(contentDir, targetDir, context, options = {})
110
121
  const STRUCTURAL_FILES = new Set([
111
122
  'package.json',
112
123
  'vite.config.js',
113
- 'main.js',
124
+ 'entry.js',
114
125
  'index.html',
115
126
  '.gitignore',
116
127
  ])
@@ -121,29 +132,41 @@ export async function applyContent(contentDir, targetDir, context, options = {})
121
132
  'site.yml': ['name', 'foundation'],
122
133
  }
123
134
 
124
- await copyContentRecursive(contentDir, targetDir, context, STRUCTURAL_FILES, MERGE_FILES, options)
135
+ await copyContentRecursive(contentDir, targetDir, context, STRUCTURAL_FILES, MERGE_FILES, options, 0)
125
136
  }
126
137
 
127
138
  /**
128
- * Recursively copy content files, skipping structural files
139
+ * Recursively copy content files, skipping structural files.
140
+ *
141
+ * `depth` is tracked so the `renames` map (passed via options) only
142
+ * applies at depth 0 — the top of the content directory. Without this
143
+ * guard, a rename like `foundation.js → main.js` would also rewrite a
144
+ * nested `sections/foo/foundation.js` if one existed, which is not the
145
+ * intent.
129
146
  */
130
- async function copyContentRecursive(sourceDir, targetDir, context, structuralFiles, mergeFiles, options) {
147
+ async function copyContentRecursive(sourceDir, targetDir, context, structuralFiles, mergeFiles, options, depth = 0) {
131
148
  await fs.mkdir(targetDir, { recursive: true })
132
149
 
133
150
  const entries = readdirSync(sourceDir, { withFileTypes: true })
151
+ const renames = (depth === 0 && options.renames) || null
134
152
 
135
153
  for (const entry of entries) {
136
154
  const sourcePath = join(sourceDir, entry.name)
137
155
 
138
156
  if (entry.isDirectory()) {
139
157
  const targetSubDir = join(targetDir, entry.name)
140
- await copyContentRecursive(sourcePath, targetSubDir, context, structuralFiles, mergeFiles, options)
158
+ await copyContentRecursive(sourcePath, targetSubDir, context, structuralFiles, mergeFiles, options, depth + 1)
141
159
  } else {
142
160
  // Determine the output filename (strip .hbs extension)
143
- const outputName = entry.name.endsWith('.hbs')
161
+ let outputName = entry.name.endsWith('.hbs')
144
162
  ? entry.name.slice(0, -4)
145
163
  : entry.name
146
164
 
165
+ // Apply top-level rename (e.g. legacy `foundation.js` → `main.js`)
166
+ if (renames && Object.prototype.hasOwnProperty.call(renames, outputName)) {
167
+ outputName = renames[outputName]
168
+ }
169
+
147
170
  // Skip structural files
148
171
  if (structuralFiles.has(outputName)) continue
149
172
 
@@ -184,8 +207,17 @@ async function copyContentRecursive(sourceDir, targetDir, context, structuralFil
184
207
  for (const key of preserveKeys) {
185
208
  if (existing[key] === undefined) continue
186
209
  const baseLine = matchTopLevelLine(existingContent, key)
187
- if (baseLine) {
210
+ if (!baseLine) continue
211
+ // If the new content carries the key, replace its line with
212
+ // the scaffolded value (preserving the user's project/foundation
213
+ // choice). Otherwise insert the line — older content templates
214
+ // (notably `docs/site/site.yml.hbs`) omit `foundation:` entirely,
215
+ // and dropping it leaves the site without a foundation ref so
216
+ // the entry's `import '#foundation/styles'` fails at build time.
217
+ if (matchTopLevelLine(merged, key)) {
188
218
  merged = replaceTopLevelLine(merged, key, baseLine)
219
+ } else {
220
+ merged = insertTopLevelLine(merged, baseLine)
189
221
  }
190
222
  }
191
223
 
@@ -207,14 +239,20 @@ async function copyContentRecursive(sourceDir, targetDir, context, structuralFil
207
239
  /**
208
240
  * Apply the built-in starter content
209
241
  *
242
+ * The starter ships its foundation content under `cli/starter/foundation/`
243
+ * (a description of the *kind* of content), and is applied into whatever
244
+ * folder the workspace uses for the foundation package — which is `src/`
245
+ * in the current single-foundation layout, not `foundation/`. Callers
246
+ * can override via options when scaffolding multi-foundation workspaces.
247
+ *
210
248
  * @param {string} projectDir - Root project directory
211
249
  * @param {Object} context - Template context
212
250
  * @param {Object} [options] - Processing options
213
- * @param {string} [options.foundationDir] - Foundation directory name (default: 'foundation')
251
+ * @param {string} [options.foundationDir] - Foundation directory name (default: 'src')
214
252
  * @param {string} [options.siteDir] - Site directory name (default: 'site')
215
253
  */
216
254
  export async function applyStarter(projectDir, context, options = {}) {
217
- const foundationDir = options.foundationDir || 'foundation'
255
+ const foundationDir = options.foundationDir || 'src'
218
256
  const siteDir = options.siteDir || 'site'
219
257
 
220
258
  // Apply foundation starter content
@@ -260,6 +298,25 @@ function replaceTopLevelLine(content, key, replacement) {
260
298
  )
261
299
  }
262
300
 
301
+ /**
302
+ * Insert a top-level YAML line into a content string.
303
+ *
304
+ * Used by the merge path when the content template omits a key that
305
+ * the scaffolded base file declared (e.g. an older `site.yml.hbs` with
306
+ * no `foundation:` line). Inserts immediately after the `name:` line
307
+ * if one exists — that's the conventional position for site
308
+ * configuration — and otherwise prepends to the file. The original
309
+ * trailing newline (or lack thereof) of the file is preserved.
310
+ */
311
+ function insertTopLevelLine(content, line) {
312
+ const nameMatch = content.match(/^name:.*$/m)
313
+ if (nameMatch) {
314
+ const idx = nameMatch.index + nameMatch[0].length
315
+ return content.slice(0, idx) + '\n' + line + content.slice(idx)
316
+ }
317
+ return line + '\n' + content
318
+ }
319
+
263
320
  /**
264
321
  * Resolve a dependency version string from a template.json entry.
265
322
  *
@@ -10,6 +10,7 @@ import { existsSync, readdirSync, readFileSync } from 'node:fs'
10
10
  import { readFile } from 'node:fs/promises'
11
11
  import { resolve, dirname, join } from 'node:path'
12
12
  import yaml from 'js-yaml'
13
+ import { classifyPackage as classifyPackageSync } from '@uniweb/build'
13
14
 
14
15
  /**
15
16
  * Check if a directory is a workspace root.
@@ -124,38 +125,17 @@ export async function getWorkspacePackages(workspaceRoot) {
124
125
  }
125
126
 
126
127
  /**
127
- * Classify a package as foundation, site, or unknown
128
+ * Classify a package as foundation, site, or unknown.
128
129
  *
129
- * Classification logic:
130
- * - Site: has @uniweb/runtime in dependencies (checked first, more specific)
131
- * - Foundation: has @uniweb/build in devDependencies but NOT @uniweb/runtime
132
- *
133
- * Note: Sites also have @uniweb/build for the Vite plugin, so we check
134
- * for @uniweb/runtime first to distinguish them.
130
+ * Re-exports the canonical (sync) classifier from @uniweb/build, kept
131
+ * async-shaped here so existing call sites continue to work without an
132
+ * await-removal sweep. New code should import directly from @uniweb/build.
135
133
  *
136
134
  * @param {string} packagePath - Full path to package directory
137
135
  * @returns {Promise<'foundation'|'site'|null>}
138
136
  */
139
137
  export async function classifyPackage(packagePath) {
140
- const pkgJsonPath = join(packagePath, 'package.json')
141
- if (!existsSync(pkgJsonPath)) return null
142
-
143
- try {
144
- const pkg = JSON.parse(await readFile(pkgJsonPath, 'utf-8'))
145
-
146
- // Site: has @uniweb/runtime in dependencies (check first - more specific)
147
- if (pkg.dependencies?.['@uniweb/runtime']) {
148
- return 'site'
149
- }
150
- // Foundation: has @uniweb/build in devDependencies (and not a site)
151
- if (pkg.devDependencies?.['@uniweb/build']) {
152
- return 'foundation'
153
- }
154
- } catch {
155
- // Ignore parse errors
156
- }
157
-
158
- return null
138
+ return classifyPackageSync(packagePath)
159
139
  }
160
140
 
161
141
  /**
@@ -165,16 +145,7 @@ export async function classifyPackage(packagePath) {
165
145
  */
166
146
  export async function findFoundations(workspaceRoot) {
167
147
  const packages = await getWorkspacePackages(workspaceRoot)
168
- const foundations = []
169
-
170
- for (const pkg of packages) {
171
- const fullPath = join(workspaceRoot, pkg)
172
- if ((await classifyPackage(fullPath)) === 'foundation') {
173
- foundations.push(pkg)
174
- }
175
- }
176
-
177
- return foundations
148
+ return packages.filter(pkg => classifyPackageSync(join(workspaceRoot, pkg)) === 'foundation')
178
149
  }
179
150
 
180
151
  /**
@@ -184,16 +155,7 @@ export async function findFoundations(workspaceRoot) {
184
155
  */
185
156
  export async function findSites(workspaceRoot) {
186
157
  const packages = await getWorkspacePackages(workspaceRoot)
187
- const sites = []
188
-
189
- for (const pkg of packages) {
190
- const fullPath = join(workspaceRoot, pkg)
191
- if ((await classifyPackage(fullPath)) === 'site') {
192
- sites.push(pkg)
193
- }
194
- }
195
-
196
- return sites
158
+ return packages.filter(pkg => classifyPackageSync(join(workspaceRoot, pkg)) === 'site')
197
159
  }
198
160
 
199
161
  /**
@@ -8,7 +8,7 @@ align: center
8
8
 
9
9
  ## Your Uniweb project is ready
10
10
 
11
- This is a minimal starting point for your Uniweb project. Edit the content in `site/pages/` and build section types in `foundation/src/sections/`.
11
+ This is a minimal starting point for your Uniweb project. Edit the content in `site/pages/` and build section types in `src/sections/`.
12
12
 
13
13
  [About](/about)
14
14
  [Documentation](https://github.com/uniweb)
@@ -19,7 +19,7 @@ export default {
19
19
  {{else}}
20
20
  export default {
21
21
  // ─── Layout ─────────────────────────────────────────────────────────────────
22
- // Create layouts in src/layouts/MyLayout/index.jsx (auto-discovered).
22
+ // Create layouts in layouts/MyLayout/index.jsx (auto-discovered).
23
23
  // defaultLayout: 'MyLayout',
24
24
 
25
25
  // ─── Props ──────────────────────────────────────────────────────────────────
@@ -3,18 +3,18 @@
3
3
  "version": "0.1.0",
4
4
  "description": "{{projectName}} foundation",
5
5
  "type": "module",
6
- "main": "./src/_entry.generated.js",
6
+ "main": "./_entry.generated.js",
7
7
  "exports": {
8
- ".": "./src/_entry.generated.js",
9
- "./styles": "./src/styles.css",
8
+ ".": "./_entry.generated.js",
9
+ "./styles": "./styles.css",
10
10
  "./dist": "./dist/foundation.js",
11
11
  "./dist/styles": "./dist/assets/style.css"
12
12
  },
13
13
  "imports": {
14
- "#components/*": "./src/components/*",
15
- "#utils/*": "./src/utils/*"
14
+ "#components/*": "./components/*",
15
+ "#utils/*": "./utils/*"
16
16
  },
17
- "files": ["dist", "src"],
17
+ "files": ["dist", "sections", "components", "layouts", "utils", "main.js", "styles.css", "_entry.generated.js"],
18
18
  "scripts": {
19
19
  "dev": "vite",
20
20
  "build": "uniweb build",
@@ -3,4 +3,4 @@
3
3
 
4
4
  @source "./sections/**/*.{js,jsx}";
5
5
  @source "./layouts/**/*.{js,jsx}";
6
- @source "../node_modules/@uniweb/kit/src/**/*.{js,jsx}";
6
+ @source "./node_modules/@uniweb/kit/src/**/*.{js,jsx}";
@@ -8,6 +8,6 @@
8
8
  </head>
9
9
  <body>
10
10
  <div id="root"></div>
11
- <script type="module" src="/main.js"></script>
11
+ <script type="module" src="/entry.js"></script>
12
12
  </body>
13
13
  </html>
@@ -93,7 +93,7 @@ colors:
93
93
 
94
94
  # ─── Foundation Variables ──────────────────────────────────────────────────────
95
95
  # Override variables declared by the foundation (e.g., header-height, max-width).
96
- # Available variables depend on the foundation — check its foundation.js.
96
+ # Available variables depend on the foundation — check its main.js.
97
97
 
98
98
  # vars:
99
99
  # header-height: 5rem
@@ -8,10 +8,10 @@ A website built with [Uniweb](https://github.com/uniweb/cli) — a component web
8
8
 
9
9
  ```
10
10
  {{projectName}}/
11
- ├── foundation/ # React component library
12
- │ ├── src/
13
- ├── sections/ # Section types (selectable by content authors)
14
- │ └── styles.css # Tailwind CSS v4 theme
11
+ ├── src/ # Foundation package (the site's source code; pkg name: src)
12
+ │ ├── sections/ # Section types (selectable by content authors)
13
+ │ ├── styles.css # Tailwind CSS v4 theme
14
+ ├── main.js # Foundation declarations (vars, defaultLayout, props)
15
15
  │ └── vite.config.js # defineFoundationConfig()
16
16
 
17
17
  ├── site/ # Content (markdown)
@@ -19,12 +19,14 @@ A website built with [Uniweb](https://github.com/uniweb/cli) — a component web
19
19
  │ │ └── home/
20
20
  │ │ ├── page.yml
21
21
  │ │ └── 1-welcome.md
22
- │ ├── site.yml # Site configuration
22
+ │ ├── site.yml # Site configuration (foundation: src)
23
23
  │ └── vite.config.js # defineSiteConfig()
24
24
 
25
25
  └── AGENTS.md # Developer guide (human + AI)
26
26
  ```
27
27
 
28
+ A site is pure content. A foundation is the site's source code — that's why it lives in `src/`.
29
+
28
30
  ## Content Authoring
29
31
 
30
32
  Content lives in markdown files under `site/pages/`:
@@ -46,10 +48,10 @@ Your content here.
46
48
 
47
49
  ## Component Development
48
50
 
49
- Section types live in `foundation/src/sections/`. Each is a folder with an `index.jsx` and a `meta.js`:
51
+ Section types live in `src/sections/`. Each is a folder with an `index.jsx` and a `meta.js`:
50
52
 
51
53
  ```jsx
52
- // foundation/src/sections/Hero/index.jsx
54
+ // src/sections/Hero/index.jsx
53
55
  import { H1, P } from '@uniweb/kit'
54
56
 
55
57
  export default function Hero({ content }) {