@uniweb/build 0.57.0 → 0.59.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 +9 -9
- package/src/foundation-name.js +89 -0
- package/src/i18n/records.js +1 -1
- package/src/index.js +9 -0
- package/src/prerender.js +1 -1
- package/src/runtime-schema.js +4 -2
- package/src/schema.js +95 -10
- package/src/site/content-collector.js +1 -1
- package/src/site/entity-pool.js +58 -10
- package/src/site/foundation-ref.js +15 -2
- package/src/site/plugin.js +9 -7
- package/src/site/queries-config.js +3 -3
- package/src/site/query-processor.js +8 -7
- package/src/site/records-config.js +73 -32
- package/src/utils/numeric-prefix.js +1 -1
- package/src/uwx/backfill.js +22 -2
- package/src/uwx/entity-source.js +1 -1
- package/src/uwx/folder.js +3 -3
- package/src/uwx/index.js +7 -4
- package/src/uwx/project-writer.js +13 -11
- package/src/uwx/records-project.js +56 -61
- package/src/uwx/records.js +55 -49
- package/src/uwx/registry-package.js +40 -6
- package/src/uwx/self-scope.js +110 -23
- package/src/uwx/site-project.js +5 -2
- package/src/uwx/site.js +42 -70
- package/src/uwx/sync-package.js +20 -9
package/src/uwx/self-scope.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
// `@/x` — a Model ref in the site's own
|
|
1
|
+
// `@/x` — a Model ref in the site's foundation's own scope — and its qualified form.
|
|
2
2
|
//
|
|
3
3
|
// `@/member` names a data schema in the foundation's own `schemas/`. It is
|
|
4
4
|
// AUTHORING shorthand: a backend resolves Models by name and never mints one, so a
|
|
5
5
|
// ref that leaves the CLI must be the name the Model was stored under,
|
|
6
|
-
// `@<
|
|
6
|
+
// `@<scope>/member`. Everything that reads a ref off the wire matches it exactly —
|
|
7
7
|
// the entity store at restore, and a records service answering a hosted page's
|
|
8
8
|
// query.
|
|
9
9
|
//
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// - a query's `schema` `site.js::queriesNested` (the `queries` Section)
|
|
14
14
|
// - the author's spelling, on pull `records-project.js` (placement and declarations)
|
|
15
15
|
//
|
|
16
|
-
// ⛔ They must agree on one
|
|
16
|
+
// ⛔ They must agree on one scope. When the query path shipped `@/member` verbatim
|
|
17
17
|
// while the records beside it were qualified, the query named a Model its own
|
|
18
18
|
// records were not stored under: a hosted page's question named a Model that does
|
|
19
19
|
// not exist, the records service refused that key, and the section rendered
|
|
@@ -21,39 +21,52 @@
|
|
|
21
21
|
// `block.dataError`, the console clean. Nothing on either side was malformed; the
|
|
22
22
|
// two paths disagreed.
|
|
23
23
|
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
// (
|
|
24
|
+
// ⭐ THE SCOPE IS THE FOUNDATION'S — the one in its name (`@acme/marketing`), which is
|
|
25
|
+
// the scope `register` stored the foundation's own data schemas under
|
|
26
|
+
// (`registry-package.js`). ⛔ Until 2026-09-22 every path above qualified with the org
|
|
27
|
+
// that owns the SITE instead — a stand-in that holds only when one org owns both, and
|
|
28
|
+
// a foundation may be registered under any org its author belongs to [Diego]. A site
|
|
29
|
+
// owned by `@client` on a foundation registered as `@acme/fnd` shipped its records as
|
|
30
|
+
// `@client/member`, a Model `register` never stored; a personal site shipped
|
|
31
|
+
// `@/member` unresolved (both measured). `siteSelfScope` is where the scope comes from.
|
|
32
|
+
|
|
33
|
+
import { readFileSync } from 'node:fs'
|
|
34
|
+
import { join, resolve } from 'node:path'
|
|
35
|
+
import yaml from 'js-yaml'
|
|
36
|
+
import { YAML_OPTIONS } from '../utils/yaml-schema.js'
|
|
37
|
+
import { detectFoundationType, parseCatalogRef } from '../site/foundation-ref.js'
|
|
38
|
+
import { readFoundationName } from '../schema.js'
|
|
39
|
+
import { splitFoundationName } from '../foundation-name.js'
|
|
27
40
|
|
|
28
41
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
42
|
+
* A scope as it may be written — `@acme`, `acme`, or `@acme/…` — reduced to the bare
|
|
43
|
+
* handle, or `''`.
|
|
31
44
|
*
|
|
32
|
-
* @param {unknown}
|
|
45
|
+
* @param {unknown} scope
|
|
33
46
|
* @returns {string}
|
|
34
47
|
*/
|
|
35
|
-
export function bareOrg(
|
|
36
|
-
return typeof
|
|
48
|
+
export function bareOrg(scope) {
|
|
49
|
+
return typeof scope === 'string' ? scope.replace(/^@/, '').replace(/\/.*$/, '') : ''
|
|
37
50
|
}
|
|
38
51
|
|
|
39
52
|
/**
|
|
40
|
-
* `@/x` → `@<
|
|
41
|
-
* does `@/x` when no
|
|
53
|
+
* `@/x` → `@<scope>/x`. Any other ref (`@std/x`, `@acme/x`) passes through, and so
|
|
54
|
+
* does `@/x` when no scope is known — callers that ship an unresolved alias say so
|
|
42
55
|
* themselves (`buildRecordEntities` warns per query).
|
|
43
56
|
*
|
|
44
57
|
* @param {unknown} ref
|
|
45
|
-
* @param {unknown}
|
|
58
|
+
* @param {unknown} scope - the site's foundation's scope (`siteSelfScope`)
|
|
46
59
|
* @returns {unknown}
|
|
47
60
|
*/
|
|
48
|
-
export function resolveSelfScope(ref,
|
|
49
|
-
const handle = bareOrg(
|
|
61
|
+
export function resolveSelfScope(ref, scope) {
|
|
62
|
+
const handle = bareOrg(scope)
|
|
50
63
|
return typeof ref === 'string' && ref.startsWith('@/') && handle
|
|
51
64
|
? `@${handle}/${ref.slice(2)}`
|
|
52
65
|
: ref
|
|
53
66
|
}
|
|
54
67
|
|
|
55
68
|
/**
|
|
56
|
-
* The inverse, for a pull: `@<
|
|
69
|
+
* The inverse, for a pull: `@<scope>/x` → `@/x` for the site's foundation's scope.
|
|
57
70
|
*
|
|
58
71
|
* ⛔ WITHOUT THIS THE ROUND TRIP IS NOT A FIXED POINT, and the failure is silent
|
|
59
72
|
* on both ends. A record authored under `records/article/` comes back as
|
|
@@ -63,16 +76,90 @@ export function resolveSelfScope(ref, org) {
|
|
|
63
76
|
* relied on the query-name default comes back with an explicit schema it never
|
|
64
77
|
* had.
|
|
65
78
|
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
* genuinely is that org's, and `@/` would be a lie.
|
|
79
|
+
* A model scoped to ANY OTHER scope is left alone: it genuinely is that org's, and
|
|
80
|
+
* `@/` would be a lie.
|
|
69
81
|
*
|
|
70
82
|
* @param {unknown} ref
|
|
71
|
-
* @param {unknown}
|
|
83
|
+
* @param {unknown} scope - the scope the pulled records were qualified with
|
|
72
84
|
* @returns {unknown}
|
|
73
85
|
*/
|
|
74
|
-
export function unresolveSelfScope(ref,
|
|
75
|
-
const handle = bareOrg(
|
|
86
|
+
export function unresolveSelfScope(ref, scope) {
|
|
87
|
+
const handle = bareOrg(scope)
|
|
76
88
|
if (typeof ref !== 'string' || !handle) return ref
|
|
77
89
|
return ref.startsWith(`@${handle}/`) ? `@/${ref.slice(handle.length + 2)}` : ref
|
|
78
90
|
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The scope a site's `@/x` refs resolve into: its foundation's, `@org` — or null
|
|
94
|
+
* when it has none yet.
|
|
95
|
+
*
|
|
96
|
+
* - `foundation` given (a pull passes the pinned ref the pushed site carries) — a
|
|
97
|
+
* catalog ref's scope, or a local foundation's, resolved from that value.
|
|
98
|
+
* - else `foundationDir` — that local foundation's.
|
|
99
|
+
* - else `site.yml::foundation`, the same way.
|
|
100
|
+
*
|
|
101
|
+
* A local foundation's scope is the one in its name, read by the one rule
|
|
102
|
+
* (`readFoundationName`, the reader `register` and `push` use), so what a push pins
|
|
103
|
+
* and what it qualifies with cannot differ. A bare name has no scope until `register`
|
|
104
|
+
* gives it one; a URL foundation has none.
|
|
105
|
+
*
|
|
106
|
+
* ⚠️ A foundation whose name cannot be read (a leftover `uniweb.scope`, a `main.js`
|
|
107
|
+
* that fails to load) answers null here. `register` — which a push and a publish run
|
|
108
|
+
* first — says why; this only qualifies.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} siteRoot
|
|
111
|
+
* @param {{ foundation?: unknown, foundationDir?: string }} [opts]
|
|
112
|
+
* @returns {Promise<string|null>}
|
|
113
|
+
*/
|
|
114
|
+
export async function siteSelfScope(siteRoot, { foundation, foundationDir } = {}) {
|
|
115
|
+
if (foundation === undefined && foundationDir) return localFoundationScope(resolve(foundationDir))
|
|
116
|
+
const declared = foundation !== undefined ? foundation : readDeclaredFoundation(siteRoot)
|
|
117
|
+
const value = declared && typeof declared === 'object' ? declared.name : declared
|
|
118
|
+
if (typeof value !== 'string' || !value) return null
|
|
119
|
+
const ref = parseCatalogRef(value)
|
|
120
|
+
if (ref) return ref.scope
|
|
121
|
+
let info = null
|
|
122
|
+
try {
|
|
123
|
+
info = detectFoundationType(value, siteRoot)
|
|
124
|
+
} catch {
|
|
125
|
+
return null
|
|
126
|
+
}
|
|
127
|
+
return info?.type === 'local' && info.path ? localFoundationScope(info.path) : null
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function localFoundationScope(dir) {
|
|
131
|
+
try {
|
|
132
|
+
const { name } = await readFoundationName(dir)
|
|
133
|
+
return splitFoundationName(name).scope
|
|
134
|
+
} catch {
|
|
135
|
+
return null
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function readDeclaredFoundation(siteRoot) {
|
|
140
|
+
try {
|
|
141
|
+
const siteYml = yaml.load(readFileSync(join(siteRoot, 'site.yml'), 'utf8'), YAML_OPTIONS) || {}
|
|
142
|
+
return siteYml.foundation ?? null
|
|
143
|
+
} catch {
|
|
144
|
+
return null
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Refuse the retired `org` option.
|
|
150
|
+
*
|
|
151
|
+
* ⛔ It carried the org that owns the SITE, and qualified a site's `@/x` refs with it
|
|
152
|
+
* (until 2026-09-22). Ignoring it would be silent, so a caller still passing it is told
|
|
153
|
+
* what replaced it: the foundation's scope, derived — or `scope`, to state it.
|
|
154
|
+
*
|
|
155
|
+
* @param {object|undefined} opts
|
|
156
|
+
* @param {string} where - the function, for the message
|
|
157
|
+
*/
|
|
158
|
+
export function refuseOrgOption(opts, where) {
|
|
159
|
+
if (opts && opts.org !== undefined) {
|
|
160
|
+
throw new Error(
|
|
161
|
+
`${where}: \`org\` is no longer read — a site's \`@/x\` refs resolve into its ` +
|
|
162
|
+
`foundation's scope (the one in the foundation's name). Pass \`scope\` to state it.`
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
}
|
package/src/uwx/site-project.js
CHANGED
|
@@ -868,9 +868,12 @@ function projectLayout(layoutSections, layoutBaseDir, report, prune, ctx) {
|
|
|
868
868
|
* files that have no corresponding incoming item (git-pull-like). Off by
|
|
869
869
|
* default; `uniweb pull` opts in. Guarded against wiping a level on an
|
|
870
870
|
* empty set.
|
|
871
|
+
* @param {string|null} [params.scope] - the scope the push qualified a query's
|
|
872
|
+
* `@/x` schema with, so it is written back as `@/x` — see
|
|
873
|
+
* `declarationsToQueriesYml`, whose default it overrides.
|
|
871
874
|
* @returns {{ config: object, collections: object, locales: object, pages: string[], sections: string[], layout: string[], deleted: string[], renamed: object[] }}
|
|
872
875
|
*/
|
|
873
|
-
export function siteContentDocumentToProject({ document, siteRoot, backend = null, sourceLocale = LOCALIZED_FIELD_ASSUMPTION.defaultSourceLocale, prune = false, keepAuthoredFoundation = false }) {
|
|
876
|
+
export function siteContentDocumentToProject({ document, siteRoot, backend = null, sourceLocale = LOCALIZED_FIELD_ASSUMPTION.defaultSourceLocale, prune = false, keepAuthoredFoundation = false, scope }) {
|
|
874
877
|
const report = { config: null, collections: null, locales: null, assets: null, pages: [], sections: [], layout: [], deleted: [], renamed: [] }
|
|
875
878
|
|
|
876
879
|
// Collects target-locale translations of localized scalars as they're projected;
|
|
@@ -893,7 +896,7 @@ export function siteContentDocumentToProject({ document, siteRoot, backend = nul
|
|
|
893
896
|
)
|
|
894
897
|
|
|
895
898
|
report.config = siteInfoToConfig({ document, siteRoot, backend, sourceLocale, collector, keepAuthoredFoundation })
|
|
896
|
-
report.queries = declarationsToQueriesYml({ document, siteRoot,
|
|
899
|
+
report.queries = declarationsToQueriesYml({ document, siteRoot, scope })
|
|
897
900
|
|
|
898
901
|
// The uuid identity index (gitignored `.uniweb/`): read the prior map to anchor
|
|
899
902
|
// rename detection, build a fresh one as we project, then persist it. Items not
|
package/src/uwx/site.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Map a file site project to one `@uniweb/site-content` entity, as the
|
|
2
|
-
// section-keyed
|
|
2
|
+
// section-keyed `$`-document (docs/reference/entity-content.md), then to a
|
|
3
3
|
// `subtype: entity` .uwx on the SYNC lane.
|
|
4
4
|
//
|
|
5
5
|
// SOURCE LAYER: the content-collector's return is flattened and lossy for
|
|
@@ -8,9 +8,13 @@
|
|
|
8
8
|
// processMarkdownFile, ordering, mode detection) so those semantics stay
|
|
9
9
|
// identical to a normal build — only the directory/mode/order *walk* is ours.
|
|
10
10
|
//
|
|
11
|
-
// The document mirrors the @uniweb/site-content Model: `info` (brief) · `
|
|
12
|
-
// (self-nesting; each page carries its `page_sections` as an inline
|
|
13
|
-
// `layout_sections` · `extensions` · `queries
|
|
11
|
+
// The document mirrors the @uniweb/site-content Model: `info` (brief) · `settings`
|
|
12
|
+
// · `pages` (self-nesting; each page carries its `page_sections` as an inline
|
|
13
|
+
// field) · `layout_sections` · `extensions` · `queries` · `services` · `secrets`.
|
|
14
|
+
// `settings` is emitted only when non-empty; `services` and `secrets` only when the
|
|
15
|
+
// backend being pushed to has that key in `sync.json` (absent is not empty — see
|
|
16
|
+
// below). `emit-surface.json` states which `site.yml` key lands in `info` and
|
|
17
|
+
// `settings`. `info.foundation`
|
|
14
18
|
// carries the verbatim `site.yml::foundation` string (the round-trip source of
|
|
15
19
|
// truth).
|
|
16
20
|
//
|
|
@@ -73,7 +77,7 @@ import { loadFreeformTranslation } from '../i18n/freeform.js'
|
|
|
73
77
|
import { updateBackendState, readBackendState } from './sync-store.js'
|
|
74
78
|
import { upsertYamlScalar } from './yaml-upsert.js'
|
|
75
79
|
import { resolveQueriesConfig } from './queries-config.js'
|
|
76
|
-
import { resolveSelfScope } from './self-scope.js'
|
|
80
|
+
import { resolveSelfScope, siteSelfScope, refuseOrgOption } from './self-scope.js'
|
|
77
81
|
import { siteContentDirs } from './site-dirs.js'
|
|
78
82
|
|
|
79
83
|
const SITE_ENTITY_KEY = 'site-content' // one content entity per site project
|
|
@@ -915,7 +919,7 @@ function externalSource(d) {
|
|
|
915
919
|
return source
|
|
916
920
|
}
|
|
917
921
|
|
|
918
|
-
function queriesNested(declarations, uuids = null,
|
|
922
|
+
function queriesNested(declarations, uuids = null, scope = null) {
|
|
919
923
|
const out = []
|
|
920
924
|
for (const [name, d] of Object.entries(declarations)) {
|
|
921
925
|
refuseUnder(d.where, `queries.${name}`)
|
|
@@ -926,13 +930,13 @@ function queriesNested(declarations, uuids = null, org = null) {
|
|
|
926
930
|
// the runtime fetches it from (never the records service).
|
|
927
931
|
const source = d.path ? { path: d.path } : d.url !== undefined ? externalSource(d) : d.source
|
|
928
932
|
setIf(data, 'source', source)
|
|
929
|
-
// ⛔ QUALIFIED, WITH THE SAME RULE AND THE SAME
|
|
930
|
-
// (`records.js::buildRecordEntities`)
|
|
931
|
-
// this name against the Models its records were
|
|
932
|
-
// `@/member` beside records stored as `@
|
|
933
|
-
// resolves no Model and the page that binds it renders
|
|
934
|
-
// author's `@/` back (`records-project.js::declarationsToQueriesYml`).
|
|
935
|
-
setIf(data, 'schema', resolveSelfScope(d.schema,
|
|
933
|
+
// ⛔ QUALIFIED, WITH THE SAME RULE AND THE SAME SCOPE AS THE RECORDS' `$model`
|
|
934
|
+
// (`records.js::buildRecordEntities`) — the site's foundation's. A consumer
|
|
935
|
+
// answers a query by matching this name against the Models its records were
|
|
936
|
+
// stored under, so a verbatim `@/member` beside records stored as `@acme/member`
|
|
937
|
+
// names nothing: the query resolves no Model and the page that binds it renders
|
|
938
|
+
// empty. The pull puts the author's `@/` back (`records-project.js::declarationsToQueriesYml`).
|
|
939
|
+
setIf(data, 'schema', resolveSelfScope(d.schema, scope))
|
|
936
940
|
setIf(data, 'sort', d.sort)
|
|
937
941
|
// Legacy `filter:` is not synced — it is translated to `where` upstream
|
|
938
942
|
// (the canonical predicate). No legacy fields on the wire.
|
|
@@ -1009,16 +1013,19 @@ function queriesNested(declarations, uuids = null, org = null) {
|
|
|
1009
1013
|
// ⛔ ABSENT IS NOT EMPTY, and the difference is destructive. The Section is
|
|
1010
1014
|
// REPLACED by what we send, so `[]` means "drop every stored config row" while a
|
|
1011
1015
|
// missing key means "I am not telling you about this". A project that has never
|
|
1012
|
-
// pulled has no
|
|
1013
|
-
// wipe a service the operator configured in the app. So: emit the
|
|
1014
|
-
//
|
|
1016
|
+
// pulled has no `services` for that backend, and its ordinary push must not read as
|
|
1017
|
+
// a request to wipe a service the operator configured in the app. So: emit the
|
|
1018
|
+
// Section only when that backend's entry in `sync.json` carries the key. Clearing is
|
|
1019
|
+
// available and explicit — `services: []` there. *(Both said `site.yml::$services`
|
|
1020
|
+
// until the move on 2026-09-20.)*
|
|
1015
1021
|
//
|
|
1016
1022
|
// ⚠️ The push gate is NOT what makes this safe, though it usually catches it: its
|
|
1017
1023
|
// tokens live in a gitignored per-clone cache, so a fresh clone pushes
|
|
1018
1024
|
// unconditionally. Correctness has to sit here.
|
|
1019
1025
|
|
|
1020
1026
|
/**
|
|
1021
|
-
*
|
|
1027
|
+
* A backend's provisioned `services` / `secrets` (its `sync.json` entry) → Section
|
|
1028
|
+
* records, or undefined when the key is absent.
|
|
1022
1029
|
*
|
|
1023
1030
|
* ⭐ PASSTHROUGH, NOT AN ALLOWLIST — the same rule and the same reason as
|
|
1024
1031
|
* `queriesNested` above. The field set belongs to the backend's Model, a service's
|
|
@@ -1027,7 +1034,8 @@ function queriesNested(declarations, uuids = null, org = null) {
|
|
|
1027
1034
|
* would DESTROY whatever is stored under it on every push. Framework can enumerate
|
|
1028
1035
|
* its own vocabulary and cannot enumerate theirs; withhold ours, forward the rest.
|
|
1029
1036
|
*
|
|
1030
|
-
* @param {*} declared - the raw
|
|
1037
|
+
* @param {*} declared - the raw `services` / `secrets` value from that backend's
|
|
1038
|
+
* `sync.json` entry
|
|
1031
1039
|
* @param {(entry: object) => string|null} identify - the record's stable `$id`
|
|
1032
1040
|
* @param {string} label - the key name, for the one warning below
|
|
1033
1041
|
* @returns {object[]|undefined}
|
|
@@ -1229,13 +1237,16 @@ function settingsNested(siteYml, { headHtml, themeYml, sourceLocale, translation
|
|
|
1229
1237
|
* @param {string} [opts.sourceLocale] - localized-field wrap locale. Defaults to
|
|
1230
1238
|
* the site's effective default locale (`defaultLanguage || languages[0] ||
|
|
1231
1239
|
* 'en'` — the shared `resolveDefaultLocale` rule), NOT a bare 'en'.
|
|
1232
|
-
* @param {string} [opts.
|
|
1233
|
-
*
|
|
1234
|
-
* records are emitted with
|
|
1240
|
+
* @param {string|null} [opts.scope] - the scope a query's foundation-relative `schema`
|
|
1241
|
+
* resolves into (`@/x` → `@scope/x`): the site's foundation's. Defaults to
|
|
1242
|
+
* `siteSelfScope`; pass the scope the records are emitted with. With none
|
|
1243
|
+
* known, `@/x` ships as written.
|
|
1235
1244
|
* @returns {Promise<object>} the section-keyed `$`-document:
|
|
1236
|
-
* `{ $uuid?, $id, $model, info, pages, layout_sections, extensions,
|
|
1245
|
+
* `{ $uuid?, $id, $model, info, settings?, pages, layout_sections, extensions,
|
|
1246
|
+
* queries, services?, secrets? }`
|
|
1237
1247
|
*/
|
|
1238
1248
|
export async function siteProjectToDocument(siteRoot, opts = {}) {
|
|
1249
|
+
refuseOrgOption(opts, 'uwx/site')
|
|
1239
1250
|
const siteYml = await readYamlFile(join(siteRoot, 'site.yml'))
|
|
1240
1251
|
const sourceLocale = opts.sourceLocale || resolveDefaultLocale(siteYml)
|
|
1241
1252
|
if (!siteYml.name) {
|
|
@@ -1533,7 +1544,8 @@ export async function siteProjectToDocument(siteRoot, opts = {}) {
|
|
|
1533
1544
|
//
|
|
1534
1545
|
// ⚠️ `queriesNested` keeps its name. §2's rule: rename what an author or a
|
|
1535
1546
|
// consumer sees, leave the identifier alone.
|
|
1536
|
-
|
|
1547
|
+
const scope = opts.scope !== undefined ? opts.scope : await siteSelfScope(siteRoot)
|
|
1548
|
+
doc.queries = queriesNested(colConfig.declarations, opts.queryUuids, scope)
|
|
1537
1549
|
// Emitted ONLY when the file declares the key — see the header above
|
|
1538
1550
|
// `serviceRecords`: on a replaced Section, absent and empty are different
|
|
1539
1551
|
// requests and one of them is destructive.
|
|
@@ -1618,53 +1630,13 @@ export function writeSiteEntityUuid(siteRoot, backend, uuid) {
|
|
|
1618
1630
|
return updateBackendState(siteRoot, backend, { site: { uuid } })
|
|
1619
1631
|
}
|
|
1620
1632
|
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
* this site's storage charged to?"*, which `$uuid` alone cannot answer and which
|
|
1629
|
-
* otherwise costs a backend round-trip (or is simply unknowable from the repo).
|
|
1630
|
-
*
|
|
1631
|
-
* Stored as the BARE handle, never `@handle` — the canonical form everywhere else
|
|
1632
|
-
* (`deriveScope` returns it, `createOrg` echoes it as `org.handle`,
|
|
1633
|
-
* `validateHandle` validates it); the `@` is display sugar the reader re-adds. In
|
|
1634
|
-
* `site.yml` it was also the only form that parsed: `@` is a reserved YAML
|
|
1635
|
-
* indicator, so `$org: @acme` is an error.
|
|
1636
|
-
*
|
|
1637
|
-
* ⛔ NOT `deploy.yml`, though that file already holds the bound `backend` and the
|
|
1638
|
-
* two look like the same class of fact. Four reasons, and the first is the one that
|
|
1639
|
-
* settles it:
|
|
1640
|
-
*
|
|
1641
|
-
* 1. CARDINALITY. `deploy.yml` is multi-TARGET and `backend` sits *under* a target,
|
|
1642
|
-
* so its shape says "this may vary per target." An org may not: one site has one
|
|
1643
|
-
* owning org, fixed at create and preserved on replace. The org is a property of
|
|
1644
|
-
* the site's uuid, and sits beside it in the same backend's section — filing it
|
|
1645
|
-
* under a target would encode a freedom that does not exist.
|
|
1646
|
-
* 2. WHO WRITES. Three paths mint a site (`ensureSiteExists`, the media-less push's
|
|
1647
|
-
* content-lane create, and `clone` seeding an existing one) and **none of them
|
|
1648
|
-
* write `deploy.yml`** — only `deploy` and `publish` call `recordLastDeploy`. The
|
|
1649
|
-
* record would exist or not depending on which verb the developer reached for.
|
|
1650
|
-
* 3. SUPPRESSIBLE. `recordLastDeploy` is a no-op under `autoSave: off` / `--no-save`.
|
|
1651
|
-
* Turning off deploy *receipts* would silently drop an *ownership* record.
|
|
1652
|
-
* 4. SEMANTICS. `deploy.yml` describes the act of shipping (`lastDeploy` is a
|
|
1653
|
-
* receipt) and is optional entirely. But `push` creates a site and never
|
|
1654
|
-
* publishes — a site can exist, be owned, and accrue storage charges without
|
|
1655
|
-
* ever being deployed. Ownership does not belong in a record of a deploy that
|
|
1656
|
-
* may not have happened.
|
|
1657
|
-
*
|
|
1658
|
-
* `backend` answers *where this ships*; `site.org` answers *whose this is*.
|
|
1659
|
-
*
|
|
1660
|
-
* @param {string} siteRoot
|
|
1661
|
-
* @param {string} backend - the origin of the backend the site was created on
|
|
1662
|
-
* @param {string} handle - the bare org handle (no leading `@`)
|
|
1663
|
-
* @returns {boolean} true if sync.json changed
|
|
1664
|
-
*/
|
|
1665
|
-
export function writeSiteOrg(siteRoot, backend, handle) {
|
|
1666
|
-
return updateBackendState(siteRoot, backend, { site: { org: handle } })
|
|
1667
|
-
}
|
|
1633
|
+
// ⛔ `writeSiteOrg` is GONE (2026-09-23). The workspace a project's requests name is two
|
|
1634
|
+
// keys of `site` now — `org` (a handle, bare) and `unit` (the uuid of a workspace with no
|
|
1635
|
+
// handle), at most one set — and every site request names it, so it is no longer a note
|
|
1636
|
+
// written once at the create. A writer of one key would leave the other stale; write
|
|
1637
|
+
// both through `updateBackendState`. Why it lives here rather than in `deploy.yml`: a
|
|
1638
|
+
// site has one workspace per backend, fixed by the create, while `deploy.yml` is per
|
|
1639
|
+
// target and records the act of shipping, which `push` never does.
|
|
1668
1640
|
|
|
1669
1641
|
// ⛔ `writeSiteBackend` and `site.yml::$backend` are GONE (2026-09-20). The scope is
|
|
1670
1642
|
// no longer a value recorded beside the uuid — it is the KEY the identity is stored
|
package/src/uwx/sync-package.js
CHANGED
|
@@ -23,6 +23,7 @@ import { buildRecordEntities, entityContentHash } from './records.js'
|
|
|
23
23
|
import { ASSET_SLOTS } from '@uniweb/semantic-parser'
|
|
24
24
|
import { buildFolderEntity } from './folder.js'
|
|
25
25
|
import { siteProjectToDocument } from './site.js'
|
|
26
|
+
import { siteSelfScope, refuseOrgOption } from './self-scope.js'
|
|
26
27
|
import { stampUnitUuids, collectUnitUuids } from './site-diff.js'
|
|
27
28
|
import { emitEntitySyncPackage } from './entity-document.js'
|
|
28
29
|
import { isLocalAssetPath } from '../site/assets.js'
|
|
@@ -205,6 +206,9 @@ function rewriteEntityAssets(node, map, ids, noStamp = null) {
|
|
|
205
206
|
* @param {string} siteRoot - directory containing site.yml
|
|
206
207
|
* @param {object} [opts]
|
|
207
208
|
* @param {string} [opts.foundationDir] - local foundation root (collection Models)
|
|
209
|
+
* @param {string|null} [opts.scope] - the scope the site's `@/x` refs resolve
|
|
210
|
+
* into. Defaults to its foundation's (`siteSelfScope`) — the one in the
|
|
211
|
+
* foundation's name. `org` is refused: it carried the site owner's.
|
|
208
212
|
* @param {Function} [opts.resolveModel] - async non-local Model resolver
|
|
209
213
|
* @param {string} [opts.sourceLocale] - localized-field wrap locale
|
|
210
214
|
* @param {Object<string,string>} [opts.priorHashes] - sync-cache (send-only-changed)
|
|
@@ -271,6 +275,13 @@ export async function emitSyncPackages(siteRoot, opts = {}) {
|
|
|
271
275
|
const stamp = withBaseVersion(opts.baseVersions || {}, opts.itemBaseVersions || {})
|
|
272
276
|
const exporter = opts.exporter
|
|
273
277
|
const exportedAt = opts.exportedAt
|
|
278
|
+
refuseOrgOption(opts, 'emitSyncPackages')
|
|
279
|
+
// ⭐ ONE SCOPE FOR THE WHOLE EMIT — the site's foundation's, the one `register`
|
|
280
|
+
// stored its Models under — so a record's `$model` and a query's `schema` are
|
|
281
|
+
// qualified alike. Read once here and handed to both. ⛔ It was the org that owns
|
|
282
|
+
// the SITE until 2026-09-22 (`self-scope.js`).
|
|
283
|
+
const scope =
|
|
284
|
+
opts.scope !== undefined ? opts.scope : await siteSelfScope(siteRoot, { foundationDir: opts.foundationDir })
|
|
274
285
|
|
|
275
286
|
const col = await buildRecordEntities(siteRoot, {
|
|
276
287
|
// Which backend's record uuids go on the wire (sync.json) — see records.js.
|
|
@@ -278,10 +289,10 @@ export async function emitSyncPackages(siteRoot, opts = {}) {
|
|
|
278
289
|
...(opts.foundationDir ? { foundationDir: opts.foundationDir } : {}),
|
|
279
290
|
...(opts.resolveModel ? { resolveModel: opts.resolveModel } : {}),
|
|
280
291
|
...(sourceLocale ? { sourceLocale } : {}),
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
|
|
292
|
+
// Resolves a foundation-relative `@/x` model ref into `@scope/x` before it ships.
|
|
293
|
+
// Null for a foundation with no scope yet, which is why buildRecordEntities
|
|
294
|
+
// warns rather than throws.
|
|
295
|
+
scope,
|
|
285
296
|
})
|
|
286
297
|
const warnings = [...col.warnings, ...(col.folder?.warnings ?? [])]
|
|
287
298
|
|
|
@@ -291,9 +302,9 @@ export async function emitSyncPackages(siteRoot, opts = {}) {
|
|
|
291
302
|
recordEntities: col.entities,
|
|
292
303
|
...(sourceLocale ? { sourceLocale } : {}),
|
|
293
304
|
// ⭐ Every record in the directory, at the top of the folder or in the
|
|
294
|
-
// sub-folder `
|
|
305
|
+
// sub-folder `folder.yml` places it in — and NOTHING when there is no folder to
|
|
295
306
|
// state. ⛔ Gated here as well as by `declared`, because the builder sends any
|
|
296
|
-
// non-empty tree: a `
|
|
307
|
+
// non-empty tree: a `folder.yml` whose folders hold only records that resolved
|
|
297
308
|
// no schema is a tree of empty branches, and sending it would replace the
|
|
298
309
|
// backend's folder with them (measured on the `dynamic` template, 2026-09-21).
|
|
299
310
|
folderNodes: col.sendFolder === true ? (col.folder?.nodes ?? []) : [],
|
|
@@ -325,10 +336,10 @@ export async function emitSyncPackages(siteRoot, opts = {}) {
|
|
|
325
336
|
// Whose $uuid belongs on this wire document — see siteProjectToDocument.
|
|
326
337
|
...(opts.backend ? { backend: opts.backend } : {}),
|
|
327
338
|
...(opts.queryUuids ? { queryUuids: opts.queryUuids } : {}),
|
|
328
|
-
// ⛔ THE SAME
|
|
339
|
+
// ⛔ THE SAME SCOPE `buildRecordEntities` WAS GIVEN ABOVE. A query's `schema`
|
|
329
340
|
// must name the Model its records are stored under, and both are qualified
|
|
330
|
-
// from one `@/x` by one rule (`./self-scope.js`) — so they take one
|
|
331
|
-
|
|
341
|
+
// from one `@/x` by one rule (`./self-scope.js`) — so they take one scope.
|
|
342
|
+
scope,
|
|
332
343
|
// Withhold the `$services`/`$secrets` Sections when the caller has
|
|
333
344
|
// determined the file is not asking for anything new by them. Passed
|
|
334
345
|
// through rather than decided here: the last-agreed state is project
|