@uniweb/build 0.76.1 → 0.78.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.
- package/package.json +7 -7
- package/src/content/index.js +0 -1
- package/src/dev/api-mount.js +27 -19
- package/src/site/content-collector.js +58 -18
- package/src/site/data-fetcher.js +19 -0
- package/src/site/index.js +1 -2
- package/src/site/plugin.js +18 -5
- package/src/site/queries-config.js +2 -1
- package/src/site/query-processor.js +4 -40
- package/src/uwx/emit-surface.json +3 -15
- package/src/uwx/index.js +13 -0
- package/src/uwx/project-writer.js +6 -2
- package/src/uwx/services-request.js +432 -0
- package/src/uwx/shown-records.js +52 -0
- package/src/uwx/site-diff.js +47 -0
- package/src/uwx/site-project.js +41 -60
- package/src/uwx/site.js +77 -172
- package/src/uwx/sync-package.js +30 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.78.0",
|
|
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-reader": "^1.2.8",
|
|
61
|
-
"@uniweb/content-writer": "^0.3.4",
|
|
62
60
|
"@uniweb/projections": "^0.7.14",
|
|
63
|
-
"@uniweb/
|
|
61
|
+
"@uniweb/content-writer": "^0.3.4",
|
|
64
62
|
"@uniweb/semantic-parser": "^1.5.1",
|
|
63
|
+
"@uniweb/content-reader": "^1.2.8",
|
|
64
|
+
"@uniweb/schemas": "^0.12.0",
|
|
65
65
|
"@uniweb/theming": "^0.2.0"
|
|
66
66
|
},
|
|
67
67
|
"optionalDependencies": {
|
|
68
|
-
"@uniweb/runtime": "^0.29.
|
|
68
|
+
"@uniweb/runtime": "^0.29.2"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
71
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
|
|
@@ -74,8 +74,8 @@
|
|
|
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/
|
|
78
|
-
"@uniweb/
|
|
77
|
+
"@uniweb/core": "^0.37.2",
|
|
78
|
+
"@uniweb/runtime": "^0.29.2"
|
|
79
79
|
},
|
|
80
80
|
"peerDependenciesMeta": {
|
|
81
81
|
"vite": {
|
package/src/content/index.js
CHANGED
|
@@ -19,7 +19,6 @@ export { collectSiteContent } from '../site/content-collector.js'
|
|
|
19
19
|
export {
|
|
20
20
|
processQueries,
|
|
21
21
|
writeQueryFiles,
|
|
22
|
-
getQueryLastModified,
|
|
23
22
|
} from '../site/query-processor.js'
|
|
24
23
|
// Where a site's records live — `paths.records`, else `records/` — so a caller
|
|
25
24
|
// holding its own config (a document's, not a `site.yml`) asks the one resolver.
|
package/src/dev/api-mount.js
CHANGED
|
@@ -4,20 +4,35 @@ import { pathToFileURL } from 'node:url'
|
|
|
4
4
|
import yaml from 'js-yaml'
|
|
5
5
|
import { YAML_OPTIONS } from '../utils/yaml-schema.js'
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Where `uniweb dev` answers the site's `api` service when `$devApi` names a handler.
|
|
9
|
+
* The dev server is the host here, so the address is its own.
|
|
10
|
+
*/
|
|
11
|
+
export const DEV_API_ADDRESS = '/_api'
|
|
12
|
+
|
|
7
13
|
/**
|
|
8
14
|
* Mount a site's own request handler in the dev server.
|
|
9
15
|
*
|
|
10
16
|
* A site that talks to a backend needs one running to be developed against, and
|
|
11
17
|
* making that a live deployment is slow, costs money, and puts a shared database
|
|
12
|
-
* behind a developer's experiments. So a site may name a **local handler
|
|
13
|
-
* dev
|
|
18
|
+
* behind a developer's experiments. So a site may name a **local handler**, and in
|
|
19
|
+
* `uniweb dev` the dev server answers the site's `api` service with it:
|
|
14
20
|
*
|
|
15
21
|
* ```yaml
|
|
16
22
|
* # site.yml
|
|
17
|
-
*
|
|
18
|
-
*
|
|
23
|
+
* services:
|
|
24
|
+
* api: true # ask your host for an app backend (in production)
|
|
25
|
+
* $devApi: ./mock/api.js # what answers it in `uniweb dev`, at /_api
|
|
19
26
|
* ```
|
|
20
27
|
*
|
|
28
|
+
* ⭐ THE DEV SERVER SUPPLIES THE ADDRESS [Diego, 2026-10-06]. In `uniweb dev` it is
|
|
29
|
+
* the host, so where it answers is its own to choose — `DEV_API_ADDRESS` — and the
|
|
30
|
+
* plugin puts that address in the dev payload's `config.api`, the site tier
|
|
31
|
+
* `resolveService` reads when no host speaks. ⛔ *Until then the site wrote it, as a
|
|
32
|
+
* top-level `api: /_api` "the same in development and in production" — an address
|
|
33
|
+
* that, under `services:`, would read as "the site brings its own backend" and ask the
|
|
34
|
+
* host to turn its own off.*
|
|
35
|
+
*
|
|
21
36
|
* ```js
|
|
22
37
|
* // mock/api.js — default-export a fetch handler
|
|
23
38
|
* export default (request) => new Response('{}', { headers: { 'content-type': 'application/json' } })
|
|
@@ -35,10 +50,9 @@ import { YAML_OPTIONS } from '../utils/yaml-schema.js'
|
|
|
35
50
|
*
|
|
36
51
|
* ## ⛔ Development only, and it cannot leak
|
|
37
52
|
*
|
|
38
|
-
*
|
|
39
|
-
* `info` key carries it, and
|
|
40
|
-
*
|
|
41
|
-
* fact about one machine.
|
|
53
|
+
* `$devApi` is read by the dev plugin and by nothing else: no build reads it, no
|
|
54
|
+
* `info` key carries it, and only the dev server's payload names its address. What
|
|
55
|
+
* answers the site's `api` service locally is a fact about one machine.
|
|
42
56
|
*
|
|
43
57
|
* ⚠️ **Same-origin on purpose.** Mounting inside the dev server means cookies and
|
|
44
58
|
* `credentials: 'same-origin'` behave as they do in production, where a site's app
|
|
@@ -59,7 +73,7 @@ import { YAML_OPTIONS } from '../utils/yaml-schema.js'
|
|
|
59
73
|
* @param {import('vite').ViteDevServer} server
|
|
60
74
|
* @param {object} options
|
|
61
75
|
* @param {string} options.root - the site directory
|
|
62
|
-
* @returns {
|
|
76
|
+
* @returns {string|null} the address the handler answers on, or null when none was mounted
|
|
63
77
|
*/
|
|
64
78
|
export function mountDevApi(server, { root }) {
|
|
65
79
|
// ⛔ Read from the RAW site.yml, never from the collected `config`. `$`-prefixed
|
|
@@ -70,18 +84,12 @@ export function mountDevApi(server, { root }) {
|
|
|
70
84
|
try {
|
|
71
85
|
site = yaml.load(readFileSync(join(root, 'site.yml'), 'utf8'), YAML_OPTIONS) || {}
|
|
72
86
|
} catch {
|
|
73
|
-
return
|
|
87
|
+
return null
|
|
74
88
|
}
|
|
75
89
|
|
|
76
90
|
const spec = site.$devApi
|
|
77
|
-
if (!spec) return
|
|
78
|
-
|
|
79
|
-
const declared = site.api
|
|
80
|
-
const mount = typeof declared === 'string' ? declared : declared?.endpoint
|
|
81
|
-
if (!mount) {
|
|
82
|
-
console.error("[dev-api] `$devApi` needs an `api:` address to answer on — add `api: /_api` to site.yml.")
|
|
83
|
-
return false
|
|
84
|
-
}
|
|
91
|
+
if (!spec) return null
|
|
92
|
+
const mount = DEV_API_ADDRESS
|
|
85
93
|
|
|
86
94
|
// Loaded once, lazily, and awaited by the middleware. ⚠️ Loud and specific on
|
|
87
95
|
// failure: a dev API that silently fails to load looks exactly like a backend
|
|
@@ -139,5 +147,5 @@ export function mountDevApi(server, { root }) {
|
|
|
139
147
|
})
|
|
140
148
|
|
|
141
149
|
console.log(`[dev-api] '${spec}' answering ${prefix}/*`)
|
|
142
|
-
return
|
|
150
|
+
return mount
|
|
143
151
|
}
|
|
@@ -41,6 +41,7 @@ import { resolveDefaultLocale, resolvePublishableLocales, validateLanguageConfig
|
|
|
41
41
|
import { authoredRedirectTarget } from '@uniweb/core/resolve-route'
|
|
42
42
|
import { parseFrontmatter } from '../utils/frontmatter.js'
|
|
43
43
|
import { parseGrid } from '@uniweb/schemas/grid'
|
|
44
|
+
import { runtimeServicesConfig, refuseRetiredServiceKeys } from '../uwx/services-request.js'
|
|
44
45
|
|
|
45
46
|
// Try to import content-reader, fall back to simplified parser
|
|
46
47
|
let markdownToProseMirror
|
|
@@ -2437,6 +2438,60 @@ async function collectLayouts(layoutDir, siteRoot) {
|
|
|
2437
2438
|
* @param {boolean} [options.strict=false] - Fail on content problems that would ship broken output rather than warning about them. A production build passes true; dev leaves it false, because a folder an author just created is empty for a moment and that is not an error.
|
|
2438
2439
|
* @returns {Promise<Object>} Site content object with assets manifest
|
|
2439
2440
|
*/
|
|
2441
|
+
/**
|
|
2442
|
+
* `site.yml` as a payload carries it — without what is authoring or publish intent,
|
|
2443
|
+
* and without the backend-scoped `$` keys. One rule for every path that ships a
|
|
2444
|
+
* site's config.
|
|
2445
|
+
*
|
|
2446
|
+
* `publishLanguages` is authoring/publish intent — it has no runtime consumer and
|
|
2447
|
+
* never ships in a payload (the visitor runtime is list-unaware; the sync lane reads
|
|
2448
|
+
* site.yml directly, not this output). The `query:` shorthand ships as
|
|
2449
|
+
* `config.fetch`, desugared by the caller; carried raw it would sit beside
|
|
2450
|
+
* `config.queries`, the declarations, and read as one.
|
|
2451
|
+
*
|
|
2452
|
+
* ⭐ `services` ships as each service's SITE TIER, `config.<name>`
|
|
2453
|
+
* (`runtimeServicesConfig`), and never whole: at `config.services` it would be the HOST
|
|
2454
|
+
* tier, where a present block declines every service it does not name, and its
|
|
2455
|
+
* request half is not an answer. *(Until 2026-10-06 the whole block shipped there, and
|
|
2456
|
+
* doubled as an undocumented way to simulate a host's offer locally; the site tier was
|
|
2457
|
+
* the top-level `search:` / `submit:` / `assistant:` / `tracking:` / `api:` keys,
|
|
2458
|
+
* refused now.)*
|
|
2459
|
+
*
|
|
2460
|
+
* ⛔ `$`-prefixed keys were the project's BACKEND-SCOPED state — `$uuid`, `$org`,
|
|
2461
|
+
* `$backend`, `$services`, `$secrets` — until they moved to `sync.json` (2026-09-20),
|
|
2462
|
+
* and a `site.yml` written before then can still carry them. This payload is a
|
|
2463
|
+
* PUBLISHED artifact that a visitor can fetch. They have no runtime reader (nothing in
|
|
2464
|
+
* core, runtime or kit reads a `config.$*` key), so this removes noise for four of
|
|
2465
|
+
* them and a real disclosure for the fifth: `$secrets` carries no values, but its
|
|
2466
|
+
* entries name every secret the site has, and an inventory of credential names is not
|
|
2467
|
+
* something an `export` should publish.
|
|
2468
|
+
*
|
|
2469
|
+
* @param {object} siteConfig
|
|
2470
|
+
* @returns {object}
|
|
2471
|
+
*/
|
|
2472
|
+
function payloadSiteConfig(siteConfig) {
|
|
2473
|
+
const {
|
|
2474
|
+
publishLanguages: _publishLanguages,
|
|
2475
|
+
query: _query,
|
|
2476
|
+
services,
|
|
2477
|
+
...runtimeSiteConfig
|
|
2478
|
+
} = siteConfig
|
|
2479
|
+
for (const key of Object.keys(runtimeSiteConfig)) {
|
|
2480
|
+
if (key.startsWith('$')) delete runtimeSiteConfig[key]
|
|
2481
|
+
}
|
|
2482
|
+
// ⭐ Each service's SITE TIER, from its entry: `config.<name>` — the switch, an
|
|
2483
|
+
// address the site brings, its options — never the request, and never a credential.
|
|
2484
|
+
const warn = (m) => console.warn(`[site-content] ${m}`)
|
|
2485
|
+
for (const [name, value] of Object.entries(runtimeServicesConfig(services, { warn }))) {
|
|
2486
|
+
if (name in runtimeSiteConfig) {
|
|
2487
|
+
warn(`\`services.${name}\` names a site.yml key that is not a service — ignoring it.`)
|
|
2488
|
+
continue
|
|
2489
|
+
}
|
|
2490
|
+
runtimeSiteConfig[name] = value
|
|
2491
|
+
}
|
|
2492
|
+
return runtimeSiteConfig
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2440
2495
|
export async function collectSiteContent(sitePath, options = {}) {
|
|
2441
2496
|
const { foundationPath, configFile = 'site.yml', profile: profileName, dropUnpublished = false, base = '/', strict = false } = options
|
|
2442
2497
|
|
|
@@ -2447,6 +2502,7 @@ export async function collectSiteContent(sitePath, options = {}) {
|
|
|
2447
2502
|
|
|
2448
2503
|
// Read site config and raw theme config
|
|
2449
2504
|
const siteConfig = await readYamlFile(join(sitePath, configFile))
|
|
2505
|
+
refuseRetiredServiceKeys(siteConfig, configFile)
|
|
2450
2506
|
|
|
2451
2507
|
// Queries are declared in TWO files — `site.yml::queries` and `queries.yml`,
|
|
2452
2508
|
// the latter winning per key — and resolving them is one question with one
|
|
@@ -2547,7 +2603,7 @@ export async function collectSiteContent(sitePath, options = {}) {
|
|
|
2547
2603
|
// Check if pages directory exists
|
|
2548
2604
|
if (!existsSync(pagesPath)) {
|
|
2549
2605
|
return {
|
|
2550
|
-
config: siteConfig,
|
|
2606
|
+
config: payloadSiteConfig(siteConfig),
|
|
2551
2607
|
theme: {
|
|
2552
2608
|
...processedTheme,
|
|
2553
2609
|
css: themeCSS,
|
|
@@ -2725,23 +2781,7 @@ export async function collectSiteContent(sitePath, options = {}) {
|
|
|
2725
2781
|
if (page.slug) delete page.slug
|
|
2726
2782
|
}
|
|
2727
2783
|
|
|
2728
|
-
|
|
2729
|
-
// consumer and never ships in a payload (the visitor runtime is
|
|
2730
|
-
// list-unaware; the sync lane reads site.yml directly, not this output).
|
|
2731
|
-
// The `query:` shorthand ships as `config.fetch`, desugared below; carried raw
|
|
2732
|
-
// it would sit beside `config.queries`, the declarations, and read as one.
|
|
2733
|
-
const { publishLanguages: _publishLanguages, query: _query, ...runtimeSiteConfig } = siteConfig
|
|
2734
|
-
// ⛔ `$`-prefixed keys were the project's BACKEND-SCOPED state — `$uuid`, `$org`,
|
|
2735
|
-
// `$backend`, `$services`, `$secrets` — until they moved to `sync.json`
|
|
2736
|
-
// (2026-09-20), and a `site.yml` written before then can still carry them. This
|
|
2737
|
-
// payload is a PUBLISHED artifact that a visitor can fetch. They have no runtime reader (nothing in core, runtime
|
|
2738
|
-
// or kit reads a `config.$*` key), so this removes noise for four of them and a
|
|
2739
|
-
// real disclosure for the fifth: `$secrets` carries no values, but its entries
|
|
2740
|
-
// name every secret the site has, and an inventory of credential names is not
|
|
2741
|
-
// something an `export` should publish.
|
|
2742
|
-
for (const key of Object.keys(runtimeSiteConfig)) {
|
|
2743
|
-
if (key.startsWith('$')) delete runtimeSiteConfig[key]
|
|
2744
|
-
}
|
|
2784
|
+
const runtimeSiteConfig = payloadSiteConfig(siteConfig)
|
|
2745
2785
|
|
|
2746
2786
|
// ⛔ **A BUILD does not ship a site whose config failed to parse.**
|
|
2747
2787
|
// `strict` is already this codebase's word for it — `build-site-data.js` sets
|
package/src/site/data-fetcher.js
CHANGED
|
@@ -203,6 +203,24 @@ export function refuseUnder(where, context) {
|
|
|
203
203
|
walk(where)
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
/**
|
|
207
|
+
* ⛔ `filter:` IS RETIRED (2026-10-06 [Diego]: "it should not be supported. We don't need to keep
|
|
208
|
+
* legacy concepts around") — refused on a query and on a fetch. It was the string predicate
|
|
209
|
+
* (`tags contains featured`) that `where:` replaced. A fetch has not read it since 2026-09-05; a
|
|
210
|
+
* query carried it into `config.queries` and applied it nowhere, so a query that said `filter:`
|
|
211
|
+
* delivered every record with nothing said (measured 2026-10-06).
|
|
212
|
+
*
|
|
213
|
+
* @param {Object|undefined} declaration - a query declaration or a binding
|
|
214
|
+
* @param {string} context - where it sits, for the message
|
|
215
|
+
*/
|
|
216
|
+
export function refuseFilter(declaration, context) {
|
|
217
|
+
if (!declaration || typeof declaration !== 'object' || declaration.filter === undefined) return
|
|
218
|
+
throw new Error(
|
|
219
|
+
`[uniweb] ${context}: \`filter:\` is retired. Write the predicate as \`where:\`, a where-object — ` +
|
|
220
|
+
`e.g. \`where: { tags: { contains: featured } }\`.`
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
|
|
206
224
|
/**
|
|
207
225
|
* ⛔ A `where` OUTSIDE THE LANGUAGE STOPS THE BUILD — a retired operator (`like`,
|
|
208
226
|
* `nin`), an unknown one, an empty `and` / `or`, a text operator with an empty
|
|
@@ -340,6 +358,7 @@ export function refuseBinding(fetch, context, { level = null } = {}) {
|
|
|
340
358
|
if (typeof fetch !== 'object' || Array.isArray(fetch)) return
|
|
341
359
|
refuseRefineAndMisplacedCurrent(fetch, context, level)
|
|
342
360
|
refuseMerge(fetch, context)
|
|
361
|
+
refuseFilter(fetch, context)
|
|
343
362
|
refuseSourceKeys(fetch, context)
|
|
344
363
|
if (fetch.collection === undefined) refuseQueryName(fetch.query, context, { object: true })
|
|
345
364
|
refuseUnder(fetch.where, context)
|
package/src/site/index.js
CHANGED
|
@@ -41,8 +41,7 @@ export {
|
|
|
41
41
|
} from './advanced-processors.js'
|
|
42
42
|
export {
|
|
43
43
|
processQueries,
|
|
44
|
-
writeQueryFiles
|
|
45
|
-
getQueryLastModified
|
|
44
|
+
writeQueryFiles
|
|
46
45
|
} from './query-processor.js'
|
|
47
46
|
export { collectSchemalessData, collectSchemalessDataAssets, rewriteSchemalessDataAssets } from './schemaless-data.js'
|
|
48
47
|
export {
|
package/src/site/plugin.js
CHANGED
|
@@ -655,6 +655,18 @@ export function siteContentPlugin(options = {}) {
|
|
|
655
655
|
}
|
|
656
656
|
|
|
657
657
|
let siteContent = null
|
|
658
|
+
// Where `uniweb dev` answers the site's `api` service with `$devApi`'s handler — set
|
|
659
|
+
// in `configureServer`, so never in a build.
|
|
660
|
+
let devApiAddress = null
|
|
661
|
+
/**
|
|
662
|
+
* The dev payload names `$devApi`'s address as the site's `api` service: the site
|
|
663
|
+
* tier, which `resolveService` reads when no host speaks. A build never gets here
|
|
664
|
+
* with one.
|
|
665
|
+
*/
|
|
666
|
+
const withDevApi = (content) => {
|
|
667
|
+
if (devApiAddress && content?.config) content.config.api = devApiAddress
|
|
668
|
+
return content
|
|
669
|
+
}
|
|
658
670
|
// What each section type declares its data blocks are — read once per collection of the site.
|
|
659
671
|
let dataModelPromise = null
|
|
660
672
|
const dataModels = () => (dataModelPromise ||= dataBlockModels(resolvedSitePath).catch(() => NO_DATA_MODELS))
|
|
@@ -955,7 +967,7 @@ export function siteContentPlugin(options = {}) {
|
|
|
955
967
|
// pages stay in the graph so in-progress drafts remain previewable.
|
|
956
968
|
// strict on a production build: a mount that contributes no pages is a
|
|
957
969
|
// warning while you author and a broken deploy once you ship.
|
|
958
|
-
siteContent = await collectForBundle(resolvedSitePath, { foundationPath, dropUnpublished: isProduction, base: basePath, strict: isProduction })
|
|
970
|
+
siteContent = withDevApi(await collectForBundle(resolvedSitePath, { foundationPath, dropUnpublished: isProduction, base: basePath, strict: isProduction }))
|
|
959
971
|
dataModelPromise = null
|
|
960
972
|
headHtml = await loadHeadHtml()
|
|
961
973
|
console.log(`[site-content] Collected ${siteContent.pages?.length || 0} pages`)
|
|
@@ -998,13 +1010,14 @@ export function siteContentPlugin(options = {}) {
|
|
|
998
1010
|
server = devServer
|
|
999
1011
|
|
|
1000
1012
|
// A site's own backend, answered locally in development. `site.yml::$devApi`
|
|
1001
|
-
// names a module that default-exports a fetch handler
|
|
1002
|
-
//
|
|
1013
|
+
// names a module that default-exports a fetch handler; the dev server mounts it
|
|
1014
|
+
// at an address of its own and names that address in the payload it serves
|
|
1015
|
+
// (`withDevApi`), so the site's `api` service resolves to it.
|
|
1003
1016
|
// ⚠️ Synchronous on purpose — see mountDevApi. An await here and the
|
|
1004
1017
|
// middleware lands after Vite's SPA fallback, which answers the API with
|
|
1005
1018
|
// index.html and says nothing about why.
|
|
1006
1019
|
try {
|
|
1007
|
-
mountDevApi(devServer, { root: resolvedSitePath })
|
|
1020
|
+
devApiAddress = mountDevApi(devServer, { root: resolvedSitePath })
|
|
1008
1021
|
} catch (err) {
|
|
1009
1022
|
console.error(`[dev-api] ${err.message}`)
|
|
1010
1023
|
}
|
|
@@ -1025,7 +1038,7 @@ export function siteContentPlugin(options = {}) {
|
|
|
1025
1038
|
rebuildQueries = false
|
|
1026
1039
|
console.log('[site-content] Content changed, rebuilding...')
|
|
1027
1040
|
try {
|
|
1028
|
-
siteContent = await collectForBundle(resolvedSitePath, { foundationPath, base: basePath })
|
|
1041
|
+
siteContent = withDevApi(await collectForBundle(resolvedSitePath, { foundationPath, base: basePath }))
|
|
1029
1042
|
dataModelPromise = null
|
|
1030
1043
|
headHtml = await loadHeadHtml()
|
|
1031
1044
|
if (materialize) {
|
|
@@ -37,7 +37,7 @@ import { readRegisteredFoundation } from './registered-foundation.js'
|
|
|
37
37
|
import { buildDataSchemaMap, SCHEMA_NOT_FOUND } from '../resolve-data-schema.js'
|
|
38
38
|
import { resolveFoundationSrcPath } from '../utils/foundation-source-root.js'
|
|
39
39
|
import { buildSchema } from '../schema.js'
|
|
40
|
-
import { refuseQueryRoute, refuseLimit } from './data-fetcher.js'
|
|
40
|
+
import { refuseQueryRoute, refuseLimit, refuseFilter } from './data-fetcher.js'
|
|
41
41
|
import { readFile } from 'node:fs/promises'
|
|
42
42
|
import yaml from 'js-yaml'
|
|
43
43
|
import { YAML_OPTIONS } from '../utils/yaml-schema.js'
|
|
@@ -248,6 +248,7 @@ export function refuseQueryDeclaration(decl) {
|
|
|
248
248
|
}
|
|
249
249
|
refuseQueryRoute(decl, where)
|
|
250
250
|
refuseLimit(decl.limit, where)
|
|
251
|
+
refuseFilter(decl, where)
|
|
251
252
|
// ⛔ `sync:` IS RETIRED (2026-09-21). `sync: false` kept a query's records off a
|
|
252
253
|
// backend while a query owned its records; every record in `records/` is pushed
|
|
253
254
|
// now, whatever reads it, so an ignored `sync: false` would push exactly what the
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
* await writeQueryFiles(siteDir, byQuery)
|
|
52
52
|
*/
|
|
53
53
|
|
|
54
|
-
import { readFile, readdir,
|
|
54
|
+
import { readFile, readdir, writeFile, mkdir, copyFile, rm } from 'node:fs/promises'
|
|
55
55
|
import { join, basename, extname, dirname, relative, resolve, sep, isAbsolute } from 'node:path'
|
|
56
56
|
import { existsSync } from 'node:fs'
|
|
57
57
|
import { createHash } from 'node:crypto'
|
|
@@ -59,7 +59,7 @@ import yaml from 'js-yaml'
|
|
|
59
59
|
import { YAML_OPTIONS } from '../utils/yaml-schema.js'
|
|
60
60
|
import { parseBibtex } from '@citestyle/bibtex'
|
|
61
61
|
import { DATA_DIR, withoutRouteVariables, mapQueryPaths, BRANCH_KEY } from '@uniweb/core'
|
|
62
|
-
import { applyWhere, applySort, refuseUnder, refuseOutsideLanguage, refuseQueryRoute, refuseLimit } from './data-fetcher.js'
|
|
62
|
+
import { applyWhere, applySort, refuseUnder, refuseOutsideLanguage, refuseQueryRoute, refuseLimit, refuseFilter } from './data-fetcher.js'
|
|
63
63
|
import { resolveAssetPath, walkContentAssets, isLocalAssetPath } from './assets.js'
|
|
64
64
|
import { readEntityPool, groupPoolBySchema, poolDirsForSchema } from './entity-pool.js'
|
|
65
65
|
import { readRecordsConfig, resolveFolder, folderTreeOrder } from './records-config.js'
|
|
@@ -126,7 +126,6 @@ function parseQueryConfig(name, config) {
|
|
|
126
126
|
scope: null,
|
|
127
127
|
sort: null,
|
|
128
128
|
where: null,
|
|
129
|
-
filter: null,
|
|
130
129
|
limit: 0,
|
|
131
130
|
excerpt: { maxLength: 160 },
|
|
132
131
|
}
|
|
@@ -136,6 +135,7 @@ function parseQueryConfig(name, config) {
|
|
|
136
135
|
refuseOutsideLanguage(config.where, `queries.${name}`)
|
|
137
136
|
refuseQueryRoute(config, `queries.${name}`)
|
|
138
137
|
refuseLimit(config.limit, `queries.${name}`)
|
|
138
|
+
refuseFilter(config, `queries.${name}`)
|
|
139
139
|
return {
|
|
140
140
|
name,
|
|
141
141
|
// The query's schema selects its records — `records/{schema}/` declares the
|
|
@@ -146,11 +146,8 @@ function parseQueryConfig(name, config) {
|
|
|
146
146
|
// here until 2026-09-11: a named query's `scope` was ignored on this lane.
|
|
147
147
|
scope: typeof config.scope === 'string' ? config.scope : null,
|
|
148
148
|
sort: config.sort || null,
|
|
149
|
-
//
|
|
150
|
-
// it replaced. Both are carried and both are applied below, in the same order
|
|
151
|
-
// `data-fetcher.js::applyPostProcessing` uses — see the note there.
|
|
149
|
+
// The predicate. ⛔ `filter:`, the string DSL it replaced, is refused above.
|
|
152
150
|
where: config.where || null,
|
|
153
|
-
filter: config.filter || null,
|
|
154
151
|
limit: config.limit || 0,
|
|
155
152
|
excerpt: {
|
|
156
153
|
maxLength: config.excerpt?.maxLength || 160,
|
|
@@ -1419,36 +1416,3 @@ export async function writeQueryFiles(siteDir, byQuery, queriesConfig = null) {
|
|
|
1419
1416
|
}
|
|
1420
1417
|
}
|
|
1421
1418
|
}
|
|
1422
|
-
|
|
1423
|
-
/**
|
|
1424
|
-
* Get last modified time for a collection
|
|
1425
|
-
*
|
|
1426
|
-
* @param {string} siteDir - Site root directory
|
|
1427
|
-
* @param {Object} config - Collection config
|
|
1428
|
-
* @returns {Promise<Date|null>} Most recent modification time
|
|
1429
|
-
*/
|
|
1430
|
-
export async function getQueryLastModified(siteDir, config) {
|
|
1431
|
-
const parsed = parseQueryConfig('temp', config)
|
|
1432
|
-
const poolDir = join(siteDir, parsed.path)
|
|
1433
|
-
|
|
1434
|
-
if (!existsSync(poolDir)) {
|
|
1435
|
-
return null
|
|
1436
|
-
}
|
|
1437
|
-
|
|
1438
|
-
const files = await readdir(poolDir)
|
|
1439
|
-
const itemFiles = files.filter(f =>
|
|
1440
|
-
!f.startsWith('_') &&
|
|
1441
|
-
(f.endsWith('.md') || f.endsWith('.yml') || f.endsWith('.yaml') || f.endsWith('.json') || f.endsWith('.bib'))
|
|
1442
|
-
)
|
|
1443
|
-
|
|
1444
|
-
let lastModified = null
|
|
1445
|
-
|
|
1446
|
-
for (const file of itemFiles) {
|
|
1447
|
-
const fileStat = await stat(join(poolDir, file))
|
|
1448
|
-
if (!lastModified || fileStat.mtime > lastModified) {
|
|
1449
|
-
lastModified = fileStat.mtime
|
|
1450
|
-
}
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
return lastModified
|
|
1454
|
-
}
|
|
@@ -45,9 +45,6 @@
|
|
|
45
45
|
"agents": [
|
|
46
46
|
"site.yml::agents"
|
|
47
47
|
],
|
|
48
|
-
"assistant": [
|
|
49
|
-
"site.yml::assistant"
|
|
50
|
-
],
|
|
51
48
|
"base": [
|
|
52
49
|
"site.yml::base"
|
|
53
50
|
],
|
|
@@ -89,28 +86,19 @@
|
|
|
89
86
|
"publish_languages": [
|
|
90
87
|
"site.yml::publishLanguages"
|
|
91
88
|
],
|
|
92
|
-
"search": [
|
|
93
|
-
"site.yml::search"
|
|
94
|
-
],
|
|
95
89
|
"seo": [
|
|
96
90
|
"site.yml::seo"
|
|
97
91
|
],
|
|
98
|
-
"
|
|
99
|
-
"site.yml::submit"
|
|
100
|
-
],
|
|
101
|
-
"theme": null,
|
|
102
|
-
"tracking": [
|
|
103
|
-
"site.yml::tracking"
|
|
104
|
-
]
|
|
92
|
+
"theme": null
|
|
105
93
|
}
|
|
106
94
|
},
|
|
107
95
|
"services": {
|
|
108
96
|
"kind": "passthrough",
|
|
109
|
-
"authoredAs": "site.yml
|
|
97
|
+
"authoredAs": "site.yml::services"
|
|
110
98
|
},
|
|
111
99
|
"secrets": {
|
|
112
100
|
"kind": "passthrough",
|
|
113
|
-
"authoredAs": "
|
|
101
|
+
"authoredAs": "sync.json::backends.<origin>.secrets"
|
|
114
102
|
},
|
|
115
103
|
"pages": {
|
|
116
104
|
"kind": "records"
|
package/src/uwx/index.js
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
|
|
13
13
|
export { mintUuidV7 } from './uuid.js'
|
|
14
14
|
export { createZip, readZip } from './zip.js'
|
|
15
|
+
// The queries whose records a site's pages show live — what `records` is for.
|
|
16
|
+
export { recordQueriesShown } from './shown-records.js'
|
|
15
17
|
export {
|
|
16
18
|
sha256Hex,
|
|
17
19
|
toJsonBuffer,
|
|
@@ -113,6 +115,16 @@ export {
|
|
|
113
115
|
refForAssetId,
|
|
114
116
|
normalizeOrigin as normalizeBackendOrigin,
|
|
115
117
|
} from './sync-store.js'
|
|
118
|
+
// A site's services request — `site.yml::services`, applied over the site's own rows.
|
|
119
|
+
// Spec: kb/framework/reference/site-services-request.md
|
|
120
|
+
export {
|
|
121
|
+
RUNTIME_KEYS,
|
|
122
|
+
readServicesRequest,
|
|
123
|
+
statedServices,
|
|
124
|
+
heldServices,
|
|
125
|
+
servicesFromDocument,
|
|
126
|
+
sameServiceRow,
|
|
127
|
+
} from './services-request.js'
|
|
116
128
|
export {
|
|
117
129
|
diffSiteUnits,
|
|
118
130
|
describeSiteDiff,
|
|
@@ -121,6 +133,7 @@ export {
|
|
|
121
133
|
walkSiteUnits,
|
|
122
134
|
collectUnitUuids,
|
|
123
135
|
collectQueryUuids,
|
|
136
|
+
siteItemsByKey,
|
|
124
137
|
stampUnitUuids,
|
|
125
138
|
} from './site-diff.js'
|
|
126
139
|
export {
|
|
@@ -268,13 +268,17 @@ const isPlainObject = (v) => Boolean(v) && typeof v === 'object' && !Array.isArr
|
|
|
268
268
|
* ⛔ The declaration keys (`query` / `fetch` / `data`) are written WHOLE, never
|
|
269
269
|
* merged: a declaration is one value, and merging an incoming `fetch:` into the
|
|
270
270
|
* local one kept whatever key the remote no longer has — a stale `limit` survived
|
|
271
|
-
* every pull.
|
|
271
|
+
* every pull. So is `services`: it is the site's settled request, and a service
|
|
272
|
+
* merged in from the old map would be an ask nobody made.
|
|
272
273
|
* @returns {'updated'|'unchanged'}
|
|
273
274
|
*/
|
|
274
275
|
export function writeSiteConfig(siteRoot, config) {
|
|
275
|
-
return mergeYamlConfig(join(siteRoot, 'site.yml'), config, { replace:
|
|
276
|
+
return mergeYamlConfig(join(siteRoot, 'site.yml'), config, { replace: SITE_CONFIG_WHOLE })
|
|
276
277
|
}
|
|
277
278
|
|
|
279
|
+
/** `site.yml` keys a writer replaces whole rather than merging into. */
|
|
280
|
+
const SITE_CONFIG_WHOLE = Object.freeze([...DECLARATION_KEYS, 'services'])
|
|
281
|
+
|
|
278
282
|
/**
|
|
279
283
|
* Write a YAML object to a file (full dump, idempotent) — for machine-owned
|
|
280
284
|
* config the projector authors wholesale, e.g. a projected `page.yml`/`folder.yml`.
|