@uniweb/build 0.44.0 → 0.44.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 +11 -9
- package/src/foundation/derive-supports.js +34 -2
- package/src/i18n/index.js +3 -2
- package/src/site/plugin.js +4 -3
- package/src/site/search-declared.js +34 -0
- package/src/uwx/emit-surface.json +119 -0
- package/src/uwx/site.js +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.44.
|
|
3
|
+
"version": "0.44.2",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -20,7 +20,8 @@
|
|
|
20
20
|
"./hosts": "./src/hosts/index.js",
|
|
21
21
|
"./i18n": "./src/i18n/index.js",
|
|
22
22
|
"./uwx": "./src/uwx/index.js",
|
|
23
|
-
"./import-map-plugin": "./src/import-map-plugin.js"
|
|
23
|
+
"./import-map-plugin": "./src/import-map-plugin.js",
|
|
24
|
+
"./emit-surface": "./src/uwx/emit-surface.json"
|
|
24
25
|
},
|
|
25
26
|
"bin": {},
|
|
26
27
|
"files": [
|
|
@@ -56,15 +57,15 @@
|
|
|
56
57
|
"js-yaml": "^4.1.0",
|
|
57
58
|
"sharp": "^0.35.3",
|
|
58
59
|
"yaml": "^2.5.0",
|
|
59
|
-
"@uniweb/
|
|
60
|
-
"@uniweb/schemas": "^0.2.13",
|
|
60
|
+
"@uniweb/projections": "^0.6.0",
|
|
61
61
|
"@uniweb/semantic-parser": "^1.4.0",
|
|
62
|
-
"@uniweb/content-writer": "^0.3.4",
|
|
63
62
|
"@uniweb/theming": "^0.1.15",
|
|
64
|
-
"@uniweb/
|
|
63
|
+
"@uniweb/content-writer": "^0.3.4",
|
|
64
|
+
"@uniweb/schemas": "^0.2.13",
|
|
65
|
+
"@uniweb/content-reader": "^1.2.4"
|
|
65
66
|
},
|
|
66
67
|
"optionalDependencies": {
|
|
67
|
-
"@uniweb/runtime": "^0.19.
|
|
68
|
+
"@uniweb/runtime": "^0.19.4"
|
|
68
69
|
},
|
|
69
70
|
"peerDependencies": {
|
|
70
71
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
|
|
@@ -73,7 +74,7 @@
|
|
|
73
74
|
"@tailwindcss/vite": "^4.0.0",
|
|
74
75
|
"@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
|
|
75
76
|
"vite-plugin-svgr": "^4.0.0",
|
|
76
|
-
"@uniweb/core": "^0.24.
|
|
77
|
+
"@uniweb/core": "^0.24.3"
|
|
77
78
|
},
|
|
78
79
|
"peerDependenciesMeta": {
|
|
79
80
|
"vite": {
|
|
@@ -99,6 +100,7 @@
|
|
|
99
100
|
}
|
|
100
101
|
},
|
|
101
102
|
"scripts": {
|
|
102
|
-
"test": "vitest run"
|
|
103
|
+
"test": "vitest run",
|
|
104
|
+
"emit-surface": "node scripts/gen-emit-surface.mjs"
|
|
103
105
|
}
|
|
104
106
|
}
|
|
@@ -133,6 +133,30 @@ function isResolveServiceCall(node) {
|
|
|
133
133
|
return false
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
/**
|
|
137
|
+
* A `.isServiceEnabled(name)` call on anything.
|
|
138
|
+
*
|
|
139
|
+
* ⛔ **Its name argument is at index 0, not 1.** `resolveService(website, name)`
|
|
140
|
+
* passes the website first; the method form has already bound it as the
|
|
141
|
+
* receiver. Reading index 1 here would find `undefined`, fall to the blind
|
|
142
|
+
* branch, and under-report `uniweb.supports` with a warning nobody is watching
|
|
143
|
+
* for — which is why `derive-supports.test.js` asserts a foundation calling
|
|
144
|
+
* `isSubmitEnabled()` derives `submit`.
|
|
145
|
+
*
|
|
146
|
+
* ⭐ This is how every `@uniweb/kit` service predicate is seen: they compile to
|
|
147
|
+
* `website.isServiceEnabled('<name>')`, and kit is bundled into the foundation,
|
|
148
|
+
* so the literal survives the shake. One matcher covers all of them — the name
|
|
149
|
+
* is an argument, so the family does not grow a rule per service.
|
|
150
|
+
*/
|
|
151
|
+
function isServiceEnabledCall(node) {
|
|
152
|
+
return (
|
|
153
|
+
node.type === 'CallExpression' &&
|
|
154
|
+
node.callee?.type === 'MemberExpression' &&
|
|
155
|
+
!node.callee.computed &&
|
|
156
|
+
node.callee.property?.name === 'isServiceEnabled'
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
136
160
|
/** A `.isSearchEnabled()` call on anything. */
|
|
137
161
|
function isSearchEnabledCall(node) {
|
|
138
162
|
return (
|
|
@@ -164,6 +188,11 @@ function collectSurvivors(bundle) {
|
|
|
164
188
|
/**
|
|
165
189
|
* Derive the services this foundation reaches for.
|
|
166
190
|
*
|
|
191
|
+
* Three call shapes are read, and two of them carry the name as an argument so
|
|
192
|
+
* they cover the open registry without a rule per service:
|
|
193
|
+
* `resolveService(website, name)`, `website.isServiceEnabled(name)` (what every
|
|
194
|
+
* `@uniweb/kit` predicate compiles to), and the two hand-written gates below.
|
|
195
|
+
*
|
|
167
196
|
* @param {object} bundle - Rollup's bundle, as handed to `writeBundle`
|
|
168
197
|
* @param {object} ctx - the Rollup plugin context (`this` in the hook)
|
|
169
198
|
* @returns {{services: string[], blind: boolean, blindAt: string[]}}
|
|
@@ -191,9 +220,12 @@ export function deriveSupports(bundle, ctx) {
|
|
|
191
220
|
services.add('search')
|
|
192
221
|
return
|
|
193
222
|
}
|
|
194
|
-
|
|
223
|
+
const isMethod = isServiceEnabledCall(node)
|
|
224
|
+
if (!isMethod && !isResolveServiceCall(node)) return
|
|
195
225
|
|
|
196
|
-
|
|
226
|
+
// The name is argument 0 on the method (the website is the receiver) and
|
|
227
|
+
// argument 1 on the free function. Getting this wrong under-reports.
|
|
228
|
+
const arg = node.arguments?.[isMethod ? 0 : 1]
|
|
197
229
|
if (typeof arg?.value === 'string') {
|
|
198
230
|
services.add(arg.value)
|
|
199
231
|
} else if (arg?.type === 'Identifier' && consts.has(arg.name)) {
|
package/src/i18n/index.js
CHANGED
|
@@ -23,7 +23,8 @@ import {
|
|
|
23
23
|
translateRecordData,
|
|
24
24
|
RECORDS_DIR
|
|
25
25
|
} from './records.js'
|
|
26
|
-
import { generateSearchIndex
|
|
26
|
+
import { generateSearchIndex } from '@uniweb/projections'
|
|
27
|
+
import { searchDeclaredOn } from '../site/search-declared.js'
|
|
27
28
|
|
|
28
29
|
// Free-form translation support
|
|
29
30
|
import {
|
|
@@ -400,7 +401,7 @@ export async function buildLocalizedContent(siteRoot, options = {}) {
|
|
|
400
401
|
outputs[locale] = { content: contentOutputPath }
|
|
401
402
|
|
|
402
403
|
// Generate search index for this locale if search is enabled
|
|
403
|
-
if (generateSearchIndexes &&
|
|
404
|
+
if (generateSearchIndexes && searchDeclaredOn(translated)) {
|
|
404
405
|
const searchConfig = translated.config?.search || {}
|
|
405
406
|
const searchIndex = generateSearchIndex(translated, {
|
|
406
407
|
locale,
|
package/src/site/plugin.js
CHANGED
|
@@ -209,8 +209,9 @@ async function processDevSectionFetches(sections, fetchOptions) {
|
|
|
209
209
|
}
|
|
210
210
|
}
|
|
211
211
|
}
|
|
212
|
-
import { generateSearchIndex,
|
|
212
|
+
import { generateSearchIndex, getSearchIndexFilename } from '@uniweb/projections'
|
|
213
213
|
import { mergeTranslations } from '../i18n/merge.js'
|
|
214
|
+
import { searchDeclaredOn } from './search-declared.js'
|
|
214
215
|
import { mountDevApi } from '../dev/api-mount.js'
|
|
215
216
|
|
|
216
217
|
/*
|
|
@@ -1145,7 +1146,7 @@ export function siteContentPlugin(options = {}) {
|
|
|
1145
1146
|
// Serve search-index.json in dev mode (supports locale prefixes)
|
|
1146
1147
|
const searchIndexMatch = req.url.match(new RegExp(`^(?:\\/(${LOCALE_RE}))?\\/search-index\\.json$`))
|
|
1147
1148
|
if (searchIndexMatch && siteContent) {
|
|
1148
|
-
const searchEnabled = searchPluginConfig.enabled !== false &&
|
|
1149
|
+
const searchEnabled = searchPluginConfig.enabled !== false && searchDeclaredOn(siteContent)
|
|
1149
1150
|
if (searchEnabled) {
|
|
1150
1151
|
const searchConfig = siteContent.config?.search || {}
|
|
1151
1152
|
const defaultLocale = resolveDefaultLocale(siteContent.config)
|
|
@@ -1511,7 +1512,7 @@ export function siteContentPlugin(options = {}) {
|
|
|
1511
1512
|
}
|
|
1512
1513
|
|
|
1513
1514
|
// Generate search index if enabled
|
|
1514
|
-
const searchEnabled = searchPluginConfig.enabled !== false &&
|
|
1515
|
+
const searchEnabled = searchPluginConfig.enabled !== false && searchDeclaredOn(finalContent)
|
|
1515
1516
|
if (searchEnabled) {
|
|
1516
1517
|
const searchConfig = finalContent.config?.search || {}
|
|
1517
1518
|
const defaultLocale = resolveDefaultLocale(finalContent.config)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Is search on for this site, as the AUTHOR declared it?
|
|
3
|
+
*
|
|
4
|
+
* ⭐ **The build's own decision about its own output.** Deliberately not
|
|
5
|
+
* `@uniweb/projections`': that package is a helper library — it derives
|
|
6
|
+
* artifacts from content and must not gate on a site setting. Deciding whether
|
|
7
|
+
* to generate is the caller's job *[Diego, 2026-09-10]*.
|
|
8
|
+
*
|
|
9
|
+
* ⛔ **The boolean form is why this is not a one-liner.**
|
|
10
|
+
* `config?.search?.enabled !== false` reads `false.enabled` when an author
|
|
11
|
+
* writes `search: false`, because optional chaining short-circuits on
|
|
12
|
+
* `null`/`undefined` only — so `undefined !== false` is `true` and the natural
|
|
13
|
+
* spelling of "off" still shipped a `search-index.json` carrying every page's
|
|
14
|
+
* text. On a static export that file is world-readable. `Website.isSearchEnabled`
|
|
15
|
+
* fixed the same expression in `@uniweb/core`; the copy in projections never got
|
|
16
|
+
* it and now needs none. Pinned by `tests/search-declared.test.js`.
|
|
17
|
+
*
|
|
18
|
+
* ⚖️ **Author tier only, and that is correct here.** A host's `services` block
|
|
19
|
+
* decides where queries are *answered* at render; it has no bearing on whether
|
|
20
|
+
* this build emits an index. The render-time question is
|
|
21
|
+
* `Website.isServiceEnabled('search')`.
|
|
22
|
+
*
|
|
23
|
+
* ⛔ **A leaf on purpose.** It lived in `site/plugin.js` for one commit, which
|
|
24
|
+
* made `i18n/index.js` import a 26-module Vite plugin to reach four lines — and
|
|
25
|
+
* put one future import in the other direction away from a cycle.
|
|
26
|
+
*
|
|
27
|
+
* @param {object} siteContent - parsed site content, or anything with `.config`
|
|
28
|
+
* @returns {boolean}
|
|
29
|
+
*/
|
|
30
|
+
export function searchDeclaredOn(siteContent) {
|
|
31
|
+
const search = siteContent?.config?.search
|
|
32
|
+
if (typeof search === 'boolean') return search
|
|
33
|
+
return search?.enabled !== false
|
|
34
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"model": "@uniweb/site-content",
|
|
4
|
+
"producer": "@uniweb/build/src/uwx/site.js",
|
|
5
|
+
"kinds": {
|
|
6
|
+
"closed": "A fixed key set. Every key is listed; a consumer may assert it.",
|
|
7
|
+
"passthrough": "Author keys copied verbatim, with no allowlist. ⛔ No closed key set exists — a consumer must NOT assert one.",
|
|
8
|
+
"records": "Content records with their own field shapes, not site.yml config keys."
|
|
9
|
+
},
|
|
10
|
+
"systemKeys": [
|
|
11
|
+
"$uuid",
|
|
12
|
+
"$id",
|
|
13
|
+
"$model"
|
|
14
|
+
],
|
|
15
|
+
"sections": {
|
|
16
|
+
"info": {
|
|
17
|
+
"kind": "closed",
|
|
18
|
+
"keys": {
|
|
19
|
+
"description": [
|
|
20
|
+
"site.yml::description"
|
|
21
|
+
],
|
|
22
|
+
"favicon": [
|
|
23
|
+
"site.yml::favicon"
|
|
24
|
+
],
|
|
25
|
+
"foundation": [
|
|
26
|
+
"site.yml::foundation"
|
|
27
|
+
],
|
|
28
|
+
"name": [
|
|
29
|
+
"site.yml::name"
|
|
30
|
+
],
|
|
31
|
+
"tags": [
|
|
32
|
+
"site.yml::tags"
|
|
33
|
+
],
|
|
34
|
+
"template": [
|
|
35
|
+
"site.yml::template"
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"settings": {
|
|
40
|
+
"kind": "closed",
|
|
41
|
+
"keys": {
|
|
42
|
+
"agents": [
|
|
43
|
+
"site.yml::agents"
|
|
44
|
+
],
|
|
45
|
+
"assistant": [
|
|
46
|
+
"site.yml::assistant"
|
|
47
|
+
],
|
|
48
|
+
"base": [
|
|
49
|
+
"site.yml::base"
|
|
50
|
+
],
|
|
51
|
+
"build": [
|
|
52
|
+
"site.yml::build"
|
|
53
|
+
],
|
|
54
|
+
"default_language": [
|
|
55
|
+
"site.yml::defaultLanguage"
|
|
56
|
+
],
|
|
57
|
+
"fetch": [
|
|
58
|
+
"site.yml::fetch",
|
|
59
|
+
"site.yml::data"
|
|
60
|
+
],
|
|
61
|
+
"fetcher": [
|
|
62
|
+
"site.yml::fetcher"
|
|
63
|
+
],
|
|
64
|
+
"head_html": null,
|
|
65
|
+
"keywords": [
|
|
66
|
+
"site.yml::keywords"
|
|
67
|
+
],
|
|
68
|
+
"languages": [
|
|
69
|
+
"site.yml::languages"
|
|
70
|
+
],
|
|
71
|
+
"layout": [
|
|
72
|
+
"site.yml::layout"
|
|
73
|
+
],
|
|
74
|
+
"paths": [
|
|
75
|
+
"site.yml::paths"
|
|
76
|
+
],
|
|
77
|
+
"placeholders": [
|
|
78
|
+
"site.yml::placeholders"
|
|
79
|
+
],
|
|
80
|
+
"publish_languages": [
|
|
81
|
+
"site.yml::publishLanguages"
|
|
82
|
+
],
|
|
83
|
+
"search": [
|
|
84
|
+
"site.yml::search"
|
|
85
|
+
],
|
|
86
|
+
"seo": [
|
|
87
|
+
"site.yml::seo"
|
|
88
|
+
],
|
|
89
|
+
"submit": [
|
|
90
|
+
"site.yml::submit"
|
|
91
|
+
],
|
|
92
|
+
"theme": null,
|
|
93
|
+
"tracking": [
|
|
94
|
+
"site.yml::tracking"
|
|
95
|
+
]
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
"services": {
|
|
99
|
+
"kind": "passthrough",
|
|
100
|
+
"authoredAs": "site.yml::$services"
|
|
101
|
+
},
|
|
102
|
+
"secrets": {
|
|
103
|
+
"kind": "passthrough",
|
|
104
|
+
"authoredAs": "site.yml::$secrets"
|
|
105
|
+
},
|
|
106
|
+
"pages": {
|
|
107
|
+
"kind": "records"
|
|
108
|
+
},
|
|
109
|
+
"layout_sections": {
|
|
110
|
+
"kind": "records"
|
|
111
|
+
},
|
|
112
|
+
"extensions": {
|
|
113
|
+
"kind": "records"
|
|
114
|
+
},
|
|
115
|
+
"queries": {
|
|
116
|
+
"kind": "records"
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
package/src/uwx/site.js
CHANGED
|
@@ -852,7 +852,9 @@ function queriesNested(declarations, uuids = null) {
|
|
|
852
852
|
// config is bound where the service is provisioned, and arrives here by `pull`.
|
|
853
853
|
//
|
|
854
854
|
// ⚖️ WHAT THESE ARE NOT. A site's OWN service declarations — `search:`, `submit:`,
|
|
855
|
-
// `assistant:`, `tracking:` —
|
|
855
|
+
// `assistant:`, `tracking:` — ride the `settings` Section and are untouched here.
|
|
856
|
+
// (They were top-level `info.*` keys until 2026-09-09; `settingsNested` below is
|
|
857
|
+
// where they are now written.) Those
|
|
856
858
|
// are authored, they resolve at the SITE tier (`config.<name>`, first choice in
|
|
857
859
|
// `@uniweb/core`'s `resolveService`), and moving them here would flip them to the
|
|
858
860
|
// host tier, where a block's mere presence declines every service it does not name.
|