uniweb 0.10.13 → 0.12.0

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.
@@ -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,7 +94,13 @@ 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
@@ -110,7 +116,7 @@ export async function applyContent(contentDir, targetDir, context, options = {})
110
116
  const STRUCTURAL_FILES = new Set([
111
117
  'package.json',
112
118
  'vite.config.js',
113
- 'main.js',
119
+ 'entry.js',
114
120
  'index.html',
115
121
  '.gitignore',
116
122
  ])
@@ -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 }) {
File without changes