@uniweb/build 0.44.2 → 0.44.4

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/README.md CHANGED
@@ -324,11 +324,15 @@ After building, your foundation will contain:
324
324
 
325
325
  ```
326
326
  dist/
327
- ├── foundation.js # Bundled components (~6KB typical)
328
- ├── foundation.js.map # Source map
327
+ ├── entry.js # The foundation section types, layouts, declarations
328
+ ├── entry.js.map # Source map
329
+ ├── entry-ssr.js # Single-file twin of entry.js, for server-side prerender
330
+ ├── runtime-pin.json # The @uniweb/runtime version it was built against
331
+ ├── *.js # Code-split chunks, when the foundation has any
332
+ ├── assets/ # Styles and static assets
329
333
  └── meta/ # Editor metadata (not needed at runtime)
330
334
  ├── schema.json # Full component metadata for editors
331
- └── previews/ # Preset preview images
335
+ └── previews/ # Preset preview images, when presets declare them
332
336
  └── Hero/
333
337
  └── default.webp
334
338
  ```
@@ -357,9 +361,9 @@ The `_self` object contains foundation-level metadata:
357
361
  | `name` | `package.json` | Foundation package name |
358
362
  | `version` | `package.json` | Foundation version |
359
363
  | `description` | `package.json` | Foundation description |
360
- | `vars` | `foundation.js` | CSS custom properties sites can override |
364
+ | `vars` | `main.js` | CSS custom properties sites can override |
361
365
 
362
- Identity fields (`name`, `version`, `description`) come from the foundation's `package.json`. Configuration fields (`vars`, etc.) come from `src/foundation.js`.
366
+ Identity fields (`name`, `version`, `description`) come from the foundation's `package.json`. Configuration fields (`vars`, etc.) come from the foundation's `main.js`.
363
367
 
364
368
  ## API Reference
365
369
 
@@ -370,7 +374,7 @@ Identity fields (`name`, `version`, `description`) come from the foundation's `p
370
374
  | `discoverComponents(srcDir)` | Discover all section types (folders with meta.js) |
371
375
  | `loadComponentMeta(componentDir)` | Load meta file for a component |
372
376
  | `loadPackageJson(srcDir)` | Load identity from package.json |
373
- | `loadFoundationConfig(srcDir)` | Load foundation.js configuration |
377
+ | `loadFoundationConfig(srcDir)` | Load the foundation's `main.js` declarations. A leftover `foundation.js` with no `main.js` beside it throws |
374
378
  | `buildSchema(srcDir)` | Build complete schema object |
375
379
 
376
380
  ### Entry Generation
@@ -387,7 +391,7 @@ The generated `_entry.generated.js` file exports:
387
391
  |--------|-------------|
388
392
  | `components` | Object map of component name → React component |
389
393
  | Named exports | Each component exported by name (e.g., `Hero`, `Features`) |
390
- | `capabilities` | Custom Layout and props from `src/foundation.js` (or `null`) |
394
+ | `capabilities` | Custom Layout and props from `main.js` (or `null`) |
391
395
  | `meta` | Runtime metadata extracted from component `meta.js` files |
392
396
 
393
397
  #### Runtime Metadata (`meta` export)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/build",
3
- "version": "0.44.2",
3
+ "version": "0.44.4",
4
4
  "description": "Build tooling for the Uniweb Component Web Platform",
5
5
  "type": "module",
6
6
  "exports": {
@@ -57,15 +57,15 @@
57
57
  "js-yaml": "^4.1.0",
58
58
  "sharp": "^0.35.3",
59
59
  "yaml": "^2.5.0",
60
+ "@uniweb/content-writer": "^0.3.4",
60
61
  "@uniweb/projections": "^0.6.0",
62
+ "@uniweb/schemas": "^0.2.13",
61
63
  "@uniweb/semantic-parser": "^1.4.0",
62
64
  "@uniweb/theming": "^0.1.15",
63
- "@uniweb/content-writer": "^0.3.4",
64
- "@uniweb/schemas": "^0.2.13",
65
65
  "@uniweb/content-reader": "^1.2.4"
66
66
  },
67
67
  "optionalDependencies": {
68
- "@uniweb/runtime": "^0.19.4"
68
+ "@uniweb/runtime": "^0.19.5"
69
69
  },
70
70
  "peerDependencies": {
71
71
  "vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
@@ -74,7 +74,7 @@
74
74
  "@tailwindcss/vite": "^4.0.0",
75
75
  "@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
76
76
  "vite-plugin-svgr": "^4.0.0",
77
- "@uniweb/core": "^0.24.3"
77
+ "@uniweb/core": "^0.24.4"
78
78
  },
79
79
  "peerDependenciesMeta": {
80
80
  "vite": {
package/src/schema.js CHANGED
@@ -184,7 +184,11 @@ export async function loadPackageJson(srcDir) {
184
184
  }
185
185
 
186
186
  /**
187
- * Load foundation-level config file (main.js, fallback foundation.js)
187
+ * Load the foundation's declarations from `src/main.js`.
188
+ *
189
+ * ⛔ There is no `foundation.js` fallback — the legacy name was removed
190
+ * 2026-09-05. A leftover `foundation.js` with no `main.js` beside it is
191
+ * REFUSED rather than ignored; see `loadFoundationConfig`.
188
192
  *
189
193
  * Contains foundation-wide configuration:
190
194
  * - vars: CSS custom properties sites can override
@@ -340,7 +344,30 @@ export async function loadFoundationConfig(srcDir) {
340
344
  break
341
345
  }
342
346
  }
343
- if (!filePath) return {}
347
+ if (!filePath) {
348
+ // ⛔ A LEFTOVER `foundation.js` IS NOT "NO CONFIG", AND MUST NOT READ AS ONE.
349
+ //
350
+ // The legacy name was removed 2026-09-05 (`FOUNDATION_FILE_NAMES` is
351
+ // `main.js` alone). This line used to return `{}` here regardless — so a
352
+ // project that kept the old name built "successfully" with every
353
+ // declaration silently gone: no `vars`, no `defaultLayout`, no
354
+ // `defaultSection`, no `props`. That is the exact outcome the guard below
355
+ // refuses on the import-failure path, reached through a door it did not
356
+ // cover. Found 2026-09-10 checking two downstream projects before an update;
357
+ // one would have lost its default layout and its output-format declarations.
358
+ //
359
+ // ⇒ Throw, like the guard below, rather than warn: a build log is where a
360
+ // warning goes to be missed, and the fix is a one-file rename.
361
+ const legacy = join(srcDir, 'foundation.js')
362
+ if (existsSync(legacy)) {
363
+ throw new Error(
364
+ `${legacy} is no longer read — rename it to main.js.\n` +
365
+ ` A foundation's declarations (vars, defaultLayout, defaultSection, props, name) live in\n` +
366
+ ` main.js. Left as foundation.js, the build would continue with none of them.`,
367
+ )
368
+ }
369
+ return {}
370
+ }
344
371
 
345
372
  let module
346
373
  try {
@@ -244,6 +244,7 @@ export async function defineSiteConfig(options = {}) {
244
244
  const agreement = checkFoundationResolution({
245
245
  name: foundationInfo.name,
246
246
  generatedInto: srcDir,
247
+ generatedEntry: entryPath,
247
248
  siteRoot,
248
249
  })
249
250
  if (!agreement.ok) console.warn(`\n${agreement.message}\n`)
@@ -84,7 +84,7 @@ function warnRetiredFetcherKeys(fetcher) {
84
84
  `[uniweb] site.yml fetcher: ${retired.map((k) => `\`${k}\``).join(', ')} ` +
85
85
  `${retired.length === 1 ? 'is' : 'are'} retired and ignored. The default fetcher reads a ` +
86
86
  `site's own files and a host's records lane; a backend of your own is reached through a ` +
87
- `foundation transport (docs: development/connecting-a-backend.md). Kept: ` +
87
+ `foundation transport (docs: development/data-sources.md). Kept: ` +
88
88
  `\`fetcher.transports\` and a transport's own binding config.`
89
89
  )
90
90
  }
@@ -59,7 +59,7 @@
59
59
  * consulted. That is the step whose answer we need.
60
60
  */
61
61
 
62
- import { existsSync, realpathSync } from 'node:fs'
62
+ import { existsSync, readFileSync, realpathSync } from 'node:fs'
63
63
  import { dirname, join, parse } from 'node:path'
64
64
 
65
65
  /**
@@ -85,6 +85,50 @@ export function findPackageDir(name, fromDir) {
85
85
  }
86
86
  }
87
87
 
88
+ /**
89
+ * Does Vite's resolution of this package land on the very file we generated?
90
+ *
91
+ * ⭐ **This is the question the check exists to ask.** Comparing DIRECTORIES
92
+ * asked a different one and got it wrong for the most common layout there is: a
93
+ * package rooted at `foundations/<x>/`, its entry generated into
94
+ * `foundations/<x>/src/`, and `main` pointing there. That is two directories and
95
+ * ONE file. Measured 2026-09-10 against every site of a ten-site project: the
96
+ * directory check fired for six — five healthy `src/` layouts, and the single
97
+ * genuine case, a pnpm `file:` dependency copied into the store, buried among
98
+ * them. The entry comparison leaves exactly that one.
99
+ * A warning that fires on healthy projects is the failure this module's own
100
+ * header warns against.
101
+ *
102
+ * ⛔ **True only when the match is PROVEN** — both files exist and realpath to
103
+ * one place. Any failure to resolve returns false, so the caller reports the
104
+ * directory disagreement rather than silencing it: a copy with no entry, or one
105
+ * carrying a stale entry of its own, is still caught.
106
+ *
107
+ * Follows what Vite reads for a bare specifier: `exports['.']` (a string, or its
108
+ * `import`/`default` condition), else `main`, else `index.js`.
109
+ */
110
+ function resolvesToSameEntry(generatedEntry, packageDir) {
111
+ let pkg
112
+ try {
113
+ pkg = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8'))
114
+ } catch {
115
+ return false
116
+ }
117
+ const pick = (v) =>
118
+ typeof v === 'string' ? v : v && typeof v === 'object' ? pick(v.import ?? v.default) : null
119
+ const exp = pkg.exports
120
+ let rel = null
121
+ if (typeof exp === 'string') rel = exp
122
+ else if (exp && typeof exp === 'object') rel = pick('.' in exp ? exp['.'] : exp)
123
+ rel ??= typeof pkg.main === 'string' ? pkg.main : null
124
+ rel ??= 'index.js'
125
+ try {
126
+ return realpathSync(generatedEntry) === realpathSync(join(packageDir, rel))
127
+ } catch {
128
+ return false
129
+ }
130
+ }
131
+
88
132
  /**
89
133
  * Compare where we generated the foundation entry against where vite will look.
90
134
  *
@@ -100,9 +144,11 @@ export function findPackageDir(name, fromDir) {
100
144
  * @param {string} args.name - the foundation's declared name (the bare specifier)
101
145
  * @param {string} args.generatedInto - the directory we wrote `_entry.generated.js` to
102
146
  * @param {string} args.siteRoot - where vite resolves the bare specifier from
147
+ * @param {string} [args.generatedEntry] - the entry file we wrote; defaults to
148
+ * `_entry.generated.js` inside `generatedInto`
103
149
  * @returns {{ ok: true } | { ok: false, ours: string, theirs: string, message: string }}
104
150
  */
105
- export function checkFoundationResolution({ name, generatedInto, siteRoot }) {
151
+ export function checkFoundationResolution({ name, generatedInto, siteRoot, generatedEntry }) {
106
152
  const theirsRaw = findPackageDir(name, siteRoot)
107
153
  if (!theirsRaw) return { ok: true }
108
154
 
@@ -118,6 +164,13 @@ export function checkFoundationResolution({ name, generatedInto, siteRoot }) {
118
164
 
119
165
  if (ours === theirs) return { ok: true }
120
166
 
167
+ // Two directories are not two foundations. When the package Vite reaches
168
+ // resolves to the entry file we generated, the build reads what we wrote —
169
+ // the `src/` layout, where the entry sits one level below the package root.
170
+ if (resolvesToSameEntry(generatedEntry ?? join(generatedInto, '_entry.generated.js'), theirsRaw)) {
171
+ return { ok: true }
172
+ }
173
+
121
174
  return {
122
175
  ok: false,
123
176
  ours,
package/src/uwx/site.js CHANGED
@@ -1212,16 +1212,18 @@ export async function siteProjectToDocument(siteRoot, opts = {}) {
1212
1212
  // a bug fix.
1213
1213
  //
1214
1214
  // ⭐ `api` is the one service a site does not AUTHOR. It is a real backend that is
1215
- // provisioned and paid for, so its address is the host's to supply — it arrives as
1215
+ // provisioned, so its address is the host's to supply — it arrives as
1216
1216
  // `config.services.api` and `@uniweb/api` reads it there (`resolveBase`). An
1217
- // authored `api:` is the SITE tier, which outranks the host permanently.
1217
+ // authored `api:` is the SITE tier: it yields to a host that offers the service,
1218
+ // and is the answer wherever one does not.
1218
1219
  //
1219
- // ⇒ Carrying it would turn a local-dev override into a production one the moment
1220
- // someone pushed: the host would store `info.api` and serve it back as `config.api`,
1221
- // which wins over the address of the backend the site actually has. The vanish on
1222
- // this lane is the correct behaviour, not the bug the comments above describe —
1223
- // there, a dropped block leaves a site with NO endpoint; here it leaves the site
1224
- // with the RIGHT one.
1220
+ // ⇒ Carrying it would turn a local-dev address into a production one the moment
1221
+ // someone pushed: the host would store `settings.api` and serve it back as
1222
+ // `config.api`, and a site with no `api` service of its own would draw sign-in
1223
+ // against an address nobody answers where the absence would have drawn
1224
+ // nothing. The vanish on this lane is the correct behaviour, not the bug the
1225
+ // comments above describe — there, a dropped block leaves a site with NO
1226
+ // endpoint; here it leaves the site with the RIGHT answer.
1225
1227
  //
1226
1228
  // The provisioned record rides the `$services` section instead (see servicesNested).
1227
1229
  // ⭐ DESUGARED, like every other tier. `data:` is the shorthand for `fetch:`