@uniweb/build 0.73.1 → 0.73.2

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/build",
3
- "version": "0.73.1",
3
+ "version": "0.73.2",
4
4
  "description": "Build tooling for the Uniweb Component Web Platform",
5
5
  "type": "module",
6
6
  "exports": {
@@ -57,11 +57,11 @@
57
57
  "js-yaml": "^4.1.0",
58
58
  "sharp": "^0.35.3",
59
59
  "yaml": "^2.5.0",
60
- "@uniweb/content-reader": "^1.2.7",
61
- "@uniweb/schemas": "^0.9.2",
60
+ "@uniweb/schemas": "^0.9.3",
61
+ "@uniweb/content-writer": "^0.3.4",
62
62
  "@uniweb/projections": "^0.7.11",
63
63
  "@uniweb/semantic-parser": "^1.4.1",
64
- "@uniweb/content-writer": "^0.3.4",
64
+ "@uniweb/content-reader": "^1.2.7",
65
65
  "@uniweb/theming": "^0.2.0"
66
66
  },
67
67
  "optionalDependencies": {
package/src/prerender.js CHANGED
@@ -32,14 +32,34 @@ import { FONT_LINKS_MARKER } from './site/head-markers.js'
32
32
  import { getAdapter } from './hosts/index.js'
33
33
  import { detectCiContext } from './hosts/detect-ci-context.js'
34
34
  import { stripBasePath } from './site/extension-urls.js'
35
+ import { isSiteRelative, resolveExtensionDist } from './site/emit-extensions.js'
35
36
 
36
37
  /**
37
- * Resolve an extension URL to a filesystem path for prerender.
38
+ * Resolve an extension URL to the file prerender imports.
38
39
  * Browser URLs like "/effects/entry.js" need mapping to local files.
39
40
  *
41
+ * ⭐ AN EXTENSION IS IMPORTED FROM ITS OWN BUILD, as the primary foundation is
42
+ * (`<foundation>/dist/entry.js`, in prerenderSite). A foundation build leaves its
43
+ * externals — `@uniweb/core`, `react`, `react/jsx-runtime` — as bare imports, and
44
+ * Node resolves them from where the FILE sits: inside the extension's own
45
+ * package, which depends on them.
46
+ *
47
+ * ⛔ Not from the copy the site build emits into `dist/` (site/emit-extensions.js).
48
+ * That copy is the browser's, which resolves the same imports through the page's
49
+ * import map; here it sits under the site package, which need not depend on
50
+ * `@uniweb/core` at all. It was the first candidate until 2026-09-28 — an order
51
+ * written when nothing put a file there — and once the build began emitting one
52
+ * (2026-08-05), every workspace extension failed here with "Cannot find package
53
+ * '@uniweb/core'", and the static HTML carried `Component not found` where its
54
+ * sections belonged until the page ran in the browser.
55
+ *
40
56
  * Resolution order:
41
- * 1. dist directory (post-build copy target, e.g., site/dist/effects/entry.js)
42
- * 2. Project root with dist subdir (dev layout, e.g., project/effects/dist/entry.js)
57
+ * 1. The extension's own build — `<project>/<name>/dist/` or
58
+ * `<project>/extensions/<name>/dist/` — found by the emitter's lookup
59
+ * (`resolveExtensionDist`), so the two lanes find the same build.
60
+ * 2. A file in the site's dist, for an extension no workspace package builds (a
61
+ * pre-built module placed in `public/`). Its bare imports resolve only where
62
+ * the site's own dependencies carry them.
43
63
  * 3. Original URL (absolute or remote — let import() handle it)
44
64
  */
45
65
  export function resolveExtensionPath(url, distDir, projectRoot, base) {
@@ -50,32 +70,18 @@ export function resolveExtensionPath(url, distDir, projectRoot, base) {
50
70
  // so a payload produced before this change still resolves.
51
71
  url = stripBasePath(url, base)
52
72
 
53
- // Only resolve URLs that look like root-relative paths
54
- if (url.startsWith('/')) {
55
- // Try dist directory first (production: files copied to site/dist/)
56
- const distPath = join(distDir, url)
57
- if (existsSync(distPath)) return distPath
58
-
59
- // Workspace layouts: "/effects/entry.js" → "<pkg>/dist/entry.js", checked
60
- // both at the project root and under an `extensions/` parent — the standard
61
- // multi-foundation layout puts extensions in `extensions/<name>/`, so the
62
- // bare-root candidate alone misses the built module and prerender can't load
63
- // the extension.
64
- const parts = url.slice(1).split('/')
65
- if (parts.length >= 2) {
66
- const pkgName = parts[0]
67
- const rest = parts.slice(1).join('/')
68
- const candidates = [
69
- join(projectRoot, pkgName, 'dist', rest),
70
- join(projectRoot, 'extensions', pkgName, 'dist', rest),
71
- ]
72
- for (const devPath of candidates) {
73
- if (existsSync(devPath)) return devPath
74
- }
75
- }
73
+ // Return as-is for absolute paths or remote URLs
74
+ if (!isSiteRelative(url)) return url
75
+
76
+ const build = resolveExtensionDist(url, projectRoot)
77
+ if (build) {
78
+ const inBuild = join(build.distDir, build.rest)
79
+ if (existsSync(inBuild)) return inBuild
76
80
  }
77
81
 
78
- // Return as-is for absolute paths or remote URLs
82
+ const inDist = join(distDir, url)
83
+ if (existsSync(inDist)) return inDist
84
+
79
85
  return url
80
86
  }
81
87
 
package/src/schema.js CHANGED
@@ -17,7 +17,7 @@ import { isFontVar } from '@uniweb/theming'
17
17
  import { join, dirname, extname, basename } from 'node:path'
18
18
  import { pathToFileURL } from 'node:url'
19
19
  import { describeChildren, describeVisuals } from '@uniweb/schemas/component'
20
- import { SECTION_PARAMS } from '@uniweb/schemas/section'
20
+ import { SECTION_PARAMS, SECTION_KEYS } from '@uniweb/schemas/section'
21
21
  import { collectSchemaRefs, buildDataSchemaMap, ownSchemaRefs } from './resolve-data-schema.js'
22
22
  import {
23
23
  composeSupports,
@@ -958,19 +958,32 @@ export function reportPlacementDeclarations(components) {
958
958
  `provides renders "Component not found".`
959
959
  )
960
960
  }
961
- // A name framework reserves in a section's params whose value the component never
962
- // receives — `component: false` in `@uniweb/schemas/section`, as `grid` is.
961
+ // A name framework reserves in a section's params: a setting of the SECTION, which framework
962
+ // applies — `component: false` in `@uniweb/schemas/section`. A component that declares one in
963
+ // its `params:` declares a setting it is never given. ⛔ Until 2026-09-28 this said the param
964
+ // "never receives the author's theme:" and to rename it — read as "the section loses the
965
+ // author's value", the opposite of what happens: the section gets it, and framework applies it.
963
966
  const params = entry.params && typeof entry.params === 'object' ? entry.params : {}
964
967
  for (const [param, reserved] of Object.entries(SECTION_PARAMS)) {
965
968
  if (reserved.component || !Object.hasOwn(params, param)) continue
966
969
  console.warn(
967
- `Warning: ${name} (meta.js): a param named "${param}" never receives the author's ${param}: — framework ` +
968
- `reserves it in a section's params (@uniweb/schemas/section). Rename the param.` +
970
+ `Warning: ${name} (meta.js): params declares "${param}", a setting of the section, not of its ` +
971
+ `component — framework applies an author's ${param}: to the section and never passes it to the ` +
972
+ `component, and an editor offers it for every section (@uniweb/schemas/section). Remove it from params:.` +
969
973
  (param === 'grid'
970
974
  ? ` To offer layouts for the child sections, declare children: { grid: [...] } and lay them out with kit's ChildGrid.`
971
975
  : '')
972
976
  )
973
977
  }
978
+ // …and a key of a section's frontmatter that is not a param at all (`SECTION_KEYS`): the build
979
+ // takes it out before the params are stored, so a param of that name is never given one.
980
+ for (const [key, { is }] of Object.entries(SECTION_KEYS)) {
981
+ if (!Object.hasOwn(params, key)) continue
982
+ console.warn(
983
+ `Warning: ${name} (meta.js): params declares "${key}", a key a section's frontmatter keeps for ` +
984
+ `itself — ${is} — so the component is never given it (@uniweb/schemas/section). Remove it from params:.`
985
+ )
986
+ }
974
987
  }
975
988
  }
976
989
 
@@ -23,6 +23,7 @@
23
23
 
24
24
  import { readFile, readdir, stat } from 'node:fs/promises'
25
25
  import { resolveQueriesConfig, toConfigQueries } from './queries-config.js'
26
+ import { isSectionKey } from '@uniweb/schemas/section'
26
27
  import { parseNumericPrefix, compareByNumericPrefix } from '../utils/numeric-prefix.js'
27
28
  import { isMarkdownFile, isIgnoredFolder } from '../utils/content-files.js'
28
29
  import { readLayoutFolder, DEFAULT_LAYOUT, layoutAreaRoute } from './layout-folder.js'
@@ -1016,9 +1017,14 @@ async function processMarkdownFile(filePath, id, siteRoot, defaultStableId = nul
1016
1017
  console.warn(`[content-collector] ${err.message}`)
1017
1018
  }
1018
1019
 
1019
- // `query`, `fetch` and `data` are never params: `query:` / `fetch:` declare the
1020
- // section's own data, and a leftover `data:` is refused (`declaredFetch`).
1021
- const { type, hidden, preset, input, props, fetch, query, data, id: frontmatterId, ...params } = frontMatter
1020
+ // ⭐ WHAT IS NOT A PARAM is `SECTION_KEYS` (`@uniweb/schemas/section`) — the list the build's
1021
+ // warning on a component's `params:` reads too — and `fetch`, one of the section's settings,
1022
+ // kept on the section as a field of its own. `query:` / `fetch:` declare the section's own
1023
+ // data, and a leftover `data:` is refused (`declaredFetch`).
1024
+ const { type, hidden, preset, input, props, fetch, query, data, id: frontmatterId } = frontMatter
1025
+ const params = Object.fromEntries(
1026
+ Object.entries(frontMatter).filter(([key]) => !isSectionKey(key) && key !== 'fetch')
1027
+ )
1022
1028
  // ⭐ `hidden: true` — a draft section, the page's meaning one level down: left out of a
1023
1029
  // published build, kept by `uniweb dev`, and synced as the section's own `hidden`
1024
1030
  // [Diego, 2026-09-28]. ⛔ Until then it was an ordinary param: the component received it
@@ -39,6 +39,11 @@
39
39
  * Same browser/internal split the runtime's distribution channel draws, for the
40
40
  * same reason: what a visitor fetches and what a renderer needs are different
41
41
  * sets, and only one of them belongs on a public origin.
42
+ *
43
+ * ⛔ The emitted copy is the BROWSER's, and only the browser's. Prerender imports
44
+ * the extension from its own build (`resolveExtensionPath`), because the copy
45
+ * sits under the site package, where the extension's bare imports
46
+ * (`@uniweb/core`, `react`) do not resolve.
42
47
  */
43
48
 
44
49
  import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
@@ -69,26 +74,31 @@ function isBrowserAsset(rel) {
69
74
  }
70
75
 
71
76
  /**
72
- * Locate the built `dist/` behind a site-relative extension URL.
77
+ * Locate the built `dist/` behind a site-relative extension URL:
78
+ * `/effects/entry.js` → `<project>/effects/dist/` or
79
+ * `<project>/extensions/effects/dist/`, with `rest` the path inside it.
73
80
  *
74
- * The same candidates `resolveExtensionPath` walks for prerender, kept in step
75
- * deliberately: if prerender can load an extension from the workspace but the
76
- * build cannot find it to emit, that is exactly the split that produced the
77
- * bug — one lane resolving it and the other not.
81
+ * One lookup for both lanes that need the extension's build: this emitter, which
82
+ * copies it into the site's output, and prerender, which imports it from where it
83
+ * was built (`resolveExtensionPath`). Neither can find an extension the other
84
+ * cannot, by construction rather than by two lists kept in step by hand.
78
85
  *
79
- * @returns {{ distDir: string, urlBase: string }|null}
86
+ * @param {string} url - a site-relative URL, with any deployment base removed
87
+ * @param {string} projectRoot - the directory holding the site and its extensions
88
+ * @returns {{ distDir: string, urlBase: string, rest: string }|null}
80
89
  */
81
- export function resolveExtensionDist(url, siteDir) {
90
+ export function resolveExtensionDist(url, projectRoot) {
82
91
  const parts = url.replace(/^\//, '').split('/')
83
92
  if (parts.length < 2) return null
84
93
  const pkgName = parts[0]
85
- const projectRoot = resolve(siteDir, '..')
86
94
 
87
95
  for (const candidate of [
88
96
  join(projectRoot, pkgName, 'dist'),
89
97
  join(projectRoot, 'extensions', pkgName, 'dist')
90
98
  ]) {
91
- if (existsSync(candidate)) return { distDir: candidate, urlBase: pkgName }
99
+ if (existsSync(candidate)) {
100
+ return { distDir: candidate, urlBase: pkgName, rest: parts.slice(1).join('/') }
101
+ }
92
102
  }
93
103
  return null
94
104
  }
@@ -108,11 +118,12 @@ export function collectExtensionAssets(extensions, siteDir) {
108
118
  const emit = []
109
119
  const unresolved = []
110
120
  if (!Array.isArray(extensions)) return { emit, unresolved }
121
+ const projectRoot = resolve(siteDir, '..')
111
122
 
112
123
  for (const decl of extensions) {
113
124
  if (!isSiteRelative(decl)) continue // absolute URL or a ref — someone else serves it
114
125
  const url = typeof decl === 'string' ? decl : decl.url
115
- const found = resolveExtensionDist(url, siteDir)
126
+ const found = resolveExtensionDist(url, projectRoot)
116
127
  if (!found) {
117
128
  unresolved.push(url)
118
129
  continue