uniweb 0.61.0 → 0.63.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/partials/agents.md +17 -15
- package/src/backend/client.js +231 -20
- package/src/backend/foundation-bring-along.js +71 -65
- package/src/backend/site-sync.js +255 -127
- package/src/commands/add.js +62 -4
- package/src/commands/clone.js +36 -6
- package/src/commands/publish.js +33 -20
- package/src/commands/pull.js +68 -15
- package/src/commands/push.js +23 -17
- package/src/commands/register.js +234 -37
- package/src/commands/rename.js +12 -9
- package/src/commands/status.js +32 -7
- package/src/framework-index.json +8 -8
- package/src/index.js +43 -10
- package/src/utils/args.js +6 -14
- package/src/utils/flag-guard.js +17 -9
- package/src/utils/foundation-name.js +179 -0
- package/src/utils/git.js +4 -1
- package/src/utils/registry-orgs.js +3 -2
- package/src/utils/scaffold.js +7 -5
- package/src/utils/site-data-upload.js +13 -2
- package/starter/foundation/main.js +2 -1
- package/templates/foundation/main.js.hbs +9 -0
- package/templates/site/site.yml.hbs +3 -2
package/src/utils/args.js
CHANGED
|
@@ -21,25 +21,17 @@
|
|
|
21
21
|
* @returns {string | null | undefined}
|
|
22
22
|
*/
|
|
23
23
|
/**
|
|
24
|
-
* The
|
|
25
|
-
* is
|
|
24
|
+
* The workspace a site command names — `--org @acme` — and, on a site's first push or
|
|
25
|
+
* publish, the org it is created under.
|
|
26
26
|
*
|
|
27
|
-
* `--as-org`
|
|
28
|
-
*
|
|
29
|
-
* it is also not the name anyone reaches for: asked in one day, the backend's docs
|
|
30
|
-
* said `--as-unit` and a second reader said `--org`; nobody produced `--as-org`.
|
|
31
|
-
* Since the flag's main job is answering *who owns this site*, `--org` is the name
|
|
32
|
-
* that matches the question, and the alias costs one `||` (the same shape
|
|
33
|
-
* `--backend` already uses).
|
|
34
|
-
*
|
|
35
|
-
* `||`, not `??`, on purpose: a valueless `--org` falls through to `--as-org`
|
|
36
|
-
* rather than shadowing it.
|
|
27
|
+
* ⛔ `--as-org` is RETIRED (2026-09-23). It mirrored the wire's `?as_org=`, which the
|
|
28
|
+
* `x-uniweb-workspace` header replaced; `flag-guard.js` refuses it and names `--org`.
|
|
37
29
|
*
|
|
38
30
|
* @param {string[]} args
|
|
39
31
|
* @returns {string|null|undefined}
|
|
40
32
|
*/
|
|
41
33
|
export function readOrgFlag(args) {
|
|
42
|
-
return readFlagValue(args, '--org')
|
|
34
|
+
return readFlagValue(args, '--org')
|
|
43
35
|
}
|
|
44
36
|
|
|
45
37
|
/**
|
|
@@ -97,7 +89,7 @@ function editDistance(a, b) {
|
|
|
97
89
|
|
|
98
90
|
/**
|
|
99
91
|
* The closest known flag to `flag`, or null when nothing is close enough.
|
|
100
|
-
* The threshold scales with length so `--
|
|
92
|
+
* The threshold scales with length so `--forcee` → `--force` is offered while two
|
|
101
93
|
* unrelated short flags are not.
|
|
102
94
|
* @param {string} flag
|
|
103
95
|
* @param {string[]} known
|
package/src/utils/flag-guard.js
CHANGED
|
@@ -50,12 +50,11 @@ const VIA_DEPLOY = ['--target', '--host', '--no-save']
|
|
|
50
50
|
/**
|
|
51
51
|
* Per-verb flag sets. Derived by scanning each command for dash-literals AND the
|
|
52
52
|
* helpers it calls — not from the help text, which has drifted from the parser in
|
|
53
|
-
* both directions (`--
|
|
54
|
-
* documented on `publish` and consumed two files away).
|
|
53
|
+
* both directions (`--yes` is documented on `publish` and consumed two files away).
|
|
55
54
|
*/
|
|
56
55
|
const VERBS = {
|
|
57
56
|
push: [
|
|
58
|
-
'--all', '--
|
|
57
|
+
'--all', '--org', '--dry-run', '--force',
|
|
59
58
|
'--foundation', '--output', '-o', '--personal',
|
|
60
59
|
// read in utils/conformance.js and backend/site-sync.js respectively —
|
|
61
60
|
// neither appears in push.js
|
|
@@ -69,7 +68,7 @@ const VERBS = {
|
|
|
69
68
|
'--no-release', ...VIA_DEPLOY
|
|
70
69
|
],
|
|
71
70
|
publish: [
|
|
72
|
-
'--
|
|
71
|
+
'--org', '--dry-run', '--force', '--foundation',
|
|
73
72
|
'--personal',
|
|
74
73
|
// read in utils/conformance.js, backend/site-sync.js, and
|
|
75
74
|
// backend/foundation-bring-along.js — none appear in publish.js
|
|
@@ -80,15 +79,15 @@ const VERBS = {
|
|
|
80
79
|
'--no-assets',
|
|
81
80
|
'--no-records', '--no-delete', '--no-prune',
|
|
82
81
|
// via backend/site-sync.js (the owner resolver) and utils/conformance.js
|
|
83
|
-
'--yes', '--org', '--
|
|
82
|
+
'--yes', '--org', '--no-validate', ...VIA_DEPLOY
|
|
84
83
|
],
|
|
85
84
|
clone: [
|
|
86
85
|
'--content-only', '--no-assets', '--no-records', '--path',
|
|
87
|
-
'--project', '--org'
|
|
86
|
+
'--project', '--org'
|
|
88
87
|
],
|
|
89
88
|
register: [
|
|
90
89
|
'--dry-run', '--json', '--output', '-o',
|
|
91
|
-
'--schema-only', '--scope', '--org'
|
|
90
|
+
'--schema-only', '--scope', '--org'
|
|
92
91
|
],
|
|
93
92
|
/**
|
|
94
93
|
* `forget` = remove one backend's records (`--backend <url>`), or everything a
|
|
@@ -99,7 +98,7 @@ const VERBS = {
|
|
|
99
98
|
forget: ['--backend', '--all'],
|
|
100
99
|
status: [
|
|
101
100
|
'--json', '--remote', '--dry-run',
|
|
102
|
-
'--force', '--no-verify', '--no-validate', '--yes', '--org',
|
|
101
|
+
'--force', '--no-verify', '--no-validate', '--yes', '--org',
|
|
103
102
|
// inert here, reachable through the bring-along module status imports for
|
|
104
103
|
// `resolveLocalFoundation` — listed per the over-approximation note above
|
|
105
104
|
'--no-release', ...VIA_DEPLOY
|
|
@@ -119,7 +118,7 @@ const VERBS = {
|
|
|
119
118
|
*/
|
|
120
119
|
refresh: [
|
|
121
120
|
'--no-backend', '--no-git',
|
|
122
|
-
'--
|
|
121
|
+
'--org', '--dry-run', '--no-validate', '--yes', ...VIA_DEPLOY
|
|
123
122
|
]
|
|
124
123
|
}
|
|
125
124
|
|
|
@@ -213,6 +212,15 @@ export function checkFlags(verb, args = []) {
|
|
|
213
212
|
].join('\n')
|
|
214
213
|
}
|
|
215
214
|
}
|
|
215
|
+
// ⭐ `--as-org` is RETIRED (2026-09-23) with the `?as_org=` it mirrored — the request
|
|
216
|
+
// names its workspace in a header now, and `--org` is the one spelling.
|
|
217
|
+
if (flag === '--as-org' && all.includes('--org')) {
|
|
218
|
+
return {
|
|
219
|
+
flag,
|
|
220
|
+
suggestion: '--org',
|
|
221
|
+
message: `\`--as-org\` is retired — use \`--org @org\` (e.g. \`uniweb ${verb} --org @acme\`).`
|
|
222
|
+
}
|
|
223
|
+
}
|
|
216
224
|
const suggestion = didYouMean(flag, all)
|
|
217
225
|
const lines = [`Unknown flag \`${flag}\` for \`uniweb ${verb}\`.`]
|
|
218
226
|
if (suggestion) lines.push(` Did you mean \`${suggestion}\`?`)
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Giving a foundation its name — `name` in `main.js`'s default export.
|
|
3
|
+
*
|
|
4
|
+
* The rule itself (main.js `name`, else package.json `name`; `src` and `foundation`
|
|
5
|
+
* refused) is `@uniweb/build`'s `foundation-name.js`, shared by the build, `register`
|
|
6
|
+
* and `push`. This module only WRITES the name: the scaffold at `create` / `add`, and
|
|
7
|
+
* `register` when it asks for one.
|
|
8
|
+
*
|
|
9
|
+
* ⛔ NO `@uniweb/build` IMPORT, AND NO IMPORT OF main.js. `uniweb create` runs before
|
|
10
|
+
* any project exists: `@uniweb/build` is an optional peer that may not be installed,
|
|
11
|
+
* and main.js's own imports may not be either. Hence text scans rather than a load,
|
|
12
|
+
* and the one duplicated fact below, pinned to the build's list by
|
|
13
|
+
* `test/foundation-name.test.js`. What a foundation IS named is always read by the
|
|
14
|
+
* build (`readFoundationName`); these only decide where to write.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
18
|
+
import { join, resolve, basename, dirname } from 'node:path'
|
|
19
|
+
|
|
20
|
+
// The folders a foundation's code lives in — never a foundation's name
|
|
21
|
+
// (`@uniweb/build`'s FORBIDDEN_FOUNDATION_NAMES; the test keeps them equal).
|
|
22
|
+
export const SCAFFOLD_FOLDER_NAMES = new Set(['src', 'foundation'])
|
|
23
|
+
|
|
24
|
+
// Written above the name wherever the CLI adds one — the scaffold's main.js
|
|
25
|
+
// template says the same.
|
|
26
|
+
const NAME_COMMENT = '// What this foundation registers as — @<org>/<name> — and what sites pin.'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A name made from free text — a project's name, a folder's — in the form a
|
|
30
|
+
* foundation name takes: lowercase letters, digits and inner hyphens.
|
|
31
|
+
* `My Site` → `my-site`. Null when nothing usable is left, or when what is left
|
|
32
|
+
* names a folder rather than a foundation.
|
|
33
|
+
*
|
|
34
|
+
* @param {unknown} raw
|
|
35
|
+
* @returns {string|null}
|
|
36
|
+
*/
|
|
37
|
+
export function normalizeFoundationName(raw) {
|
|
38
|
+
const name = String(raw ?? '')
|
|
39
|
+
.trim()
|
|
40
|
+
.toLowerCase()
|
|
41
|
+
.replace(/^@[^/]*\//, '')
|
|
42
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
43
|
+
.replace(/^-+|-+$/g, '')
|
|
44
|
+
return name && !SCAFFOLD_FOLDER_NAMES.has(name) ? name : null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* A name to offer a foundation that has none of its own: its folder's — or, for
|
|
49
|
+
* the `src/` and `foundation/` a scaffold makes, its project's folder. Null when
|
|
50
|
+
* neither makes one.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} foundationDir
|
|
53
|
+
* @returns {string|null}
|
|
54
|
+
*/
|
|
55
|
+
export function suggestFoundationName(foundationDir) {
|
|
56
|
+
const dir = resolve(foundationDir)
|
|
57
|
+
return (
|
|
58
|
+
normalizeFoundationName(basename(dir)) ||
|
|
59
|
+
normalizeFoundationName(basename(dirname(dir)))
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Whether `main.js`'s default export names the foundation — a `name` key at the
|
|
65
|
+
* export's own level, not one nested in `props` or `vars`.
|
|
66
|
+
*
|
|
67
|
+
* A scan, not a parse (see the module note): it skips strings and comments and
|
|
68
|
+
* counts brackets, which is all an object literal of declarations needs. A name
|
|
69
|
+
* that arrives by a spread is invisible to it — the build's read is the truth.
|
|
70
|
+
*
|
|
71
|
+
* @param {string} text - main.js source
|
|
72
|
+
* @returns {boolean}
|
|
73
|
+
*/
|
|
74
|
+
export function mainNamesFoundation(text) {
|
|
75
|
+
const opening = /export\s+default\s*\{/.exec(text)
|
|
76
|
+
if (!opening) return false
|
|
77
|
+
let depth = 1
|
|
78
|
+
let atKey = true // just after the opening brace, or a comma at the top level
|
|
79
|
+
for (let i = opening.index + opening[0].length; i < text.length && depth > 0; i++) {
|
|
80
|
+
const c = text[i]
|
|
81
|
+
if (c === '/' && text[i + 1] === '/') {
|
|
82
|
+
i = text.indexOf('\n', i)
|
|
83
|
+
if (i < 0) break
|
|
84
|
+
} else if (c === '/' && text[i + 1] === '*') {
|
|
85
|
+
i = text.indexOf('*/', i + 2) + 1
|
|
86
|
+
if (i <= 0) break
|
|
87
|
+
} else if (c === "'" || c === '"' || c === '`') {
|
|
88
|
+
let j = i + 1
|
|
89
|
+
while (j < text.length && text[j] !== c) j += text[j] === '\\' ? 2 : 1
|
|
90
|
+
if (depth === 1 && atKey && text.slice(i + 1, j) === 'name' && /^\s*:/.test(text.slice(j + 1))) {
|
|
91
|
+
return true
|
|
92
|
+
}
|
|
93
|
+
i = j
|
|
94
|
+
atKey = false
|
|
95
|
+
} else if ('{[('.includes(c)) {
|
|
96
|
+
depth++
|
|
97
|
+
atKey = false
|
|
98
|
+
} else if ('}])'.includes(c)) {
|
|
99
|
+
depth--
|
|
100
|
+
} else if (depth === 1 && c === ',') {
|
|
101
|
+
atKey = true
|
|
102
|
+
} else if (depth === 1 && atKey && /[A-Za-z_$]/.test(c)) {
|
|
103
|
+
const word = /^[A-Za-z_$][\w$]*/.exec(text.slice(i))[0]
|
|
104
|
+
// `name: 'x'`, or the shorthand `{ name }`
|
|
105
|
+
if (word === 'name' && /^\s*[:,}]/.test(text.slice(i + word.length))) return true
|
|
106
|
+
i += word.length - 1
|
|
107
|
+
atKey = false
|
|
108
|
+
} else if (!/\s/.test(c)) {
|
|
109
|
+
atKey = false
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return false
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Write `name` into a foundation's `main.js`.
|
|
117
|
+
*
|
|
118
|
+
* With `replace`, the one `name: '<replace>'` literal is rewritten — the case where
|
|
119
|
+
* main.js already names the foundation, with a name that cannot register. Without
|
|
120
|
+
* it, `name` becomes the default export's first key. A missing main.js is created.
|
|
121
|
+
*
|
|
122
|
+
* ⚠️ A text edit, not a parse: it handles `export default { … }` — every scaffold
|
|
123
|
+
* and every template — and says so otherwise rather than guessing.
|
|
124
|
+
*
|
|
125
|
+
* @param {string} mainFile
|
|
126
|
+
* @param {string} name - already a valid foundation name
|
|
127
|
+
* @param {{ replace?: string|null }} [opts]
|
|
128
|
+
* @returns {{ ok: true } | { ok: false, reason: string }}
|
|
129
|
+
*/
|
|
130
|
+
export function writeFoundationName(mainFile, name, { replace = null } = {}) {
|
|
131
|
+
const line = `${NAME_COMMENT}\n name: '${name}',`
|
|
132
|
+
if (!existsSync(mainFile)) {
|
|
133
|
+
writeFileSync(mainFile, `export default {\n ${line}\n}\n`)
|
|
134
|
+
return { ok: true }
|
|
135
|
+
}
|
|
136
|
+
const text = readFileSync(mainFile, 'utf8')
|
|
137
|
+
|
|
138
|
+
if (replace !== null) {
|
|
139
|
+
const literal = new RegExp(
|
|
140
|
+
`(\\bname\\s*:\\s*)(['"\`])${replace.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\2`,
|
|
141
|
+
'g'
|
|
142
|
+
)
|
|
143
|
+
const hits = text.match(literal) || []
|
|
144
|
+
if (hits.length !== 1) {
|
|
145
|
+
return {
|
|
146
|
+
ok: false,
|
|
147
|
+
reason: `main.js does not name the foundation "${replace}" in exactly one place`
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
writeFileSync(mainFile, text.replace(literal, `$1'${name}'`))
|
|
151
|
+
return { ok: true }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const opening = /export\s+default\s*\{/.exec(text)
|
|
155
|
+
if (!opening) {
|
|
156
|
+
return { ok: false, reason: 'main.js has no `export default { … }` to add a name to' }
|
|
157
|
+
}
|
|
158
|
+
// The name leads the export — on lines of its own, so a multi-line export keeps
|
|
159
|
+
// its first line and gains a blank one between.
|
|
160
|
+
const at = opening.index + opening[0].length
|
|
161
|
+
writeFileSync(mainFile, `${text.slice(0, at)}\n ${line}\n${text.slice(at)}`)
|
|
162
|
+
return { ok: true }
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Give a freshly scaffolded foundation its name, unless its main.js already has one.
|
|
167
|
+
*
|
|
168
|
+
* Called after template content is applied, because a template's own main.js
|
|
169
|
+
* replaces the scaffolded one — and with it the name the scaffold wrote.
|
|
170
|
+
*
|
|
171
|
+
* @param {string} foundationDir - the scaffolded package (flat layout: main.js at its root)
|
|
172
|
+
* @param {string|null} name
|
|
173
|
+
*/
|
|
174
|
+
export function ensureFoundationName(foundationDir, name) {
|
|
175
|
+
if (!name) return
|
|
176
|
+
const mainFile = join(foundationDir, 'main.js')
|
|
177
|
+
if (existsSync(mainFile) && mainNamesFoundation(readFileSync(mainFile, 'utf8'))) return
|
|
178
|
+
writeFoundationName(mainFile, name)
|
|
179
|
+
}
|
package/src/utils/git.js
CHANGED
|
@@ -37,12 +37,15 @@ export function siteContentRoots(siteDir) {
|
|
|
37
37
|
// root added below already covered it and the bare `'collections.yml'` entry
|
|
38
38
|
// that used to sit in this list resolved to a path no site ever had. A
|
|
39
39
|
// root-level file has no directory entry standing in for it.
|
|
40
|
+
//
|
|
41
|
+
// The records folder's `folder.yml` is the reverse case: it lives in the records
|
|
42
|
+
// directory, which is a root below, so it needs no entry. (It was `records.yml` at
|
|
43
|
+
// the site root until 2026-09-21, and was listed here then.)
|
|
40
44
|
const roots = new Set([
|
|
41
45
|
'site.yml',
|
|
42
46
|
'theme.yml',
|
|
43
47
|
'head.html',
|
|
44
48
|
'queries.yml',
|
|
45
|
-
'records.yml',
|
|
46
49
|
'locales'
|
|
47
50
|
])
|
|
48
51
|
let paths = {}
|
|
@@ -61,8 +61,9 @@ export function bareHandle(scope) {
|
|
|
61
61
|
|
|
62
62
|
/**
|
|
63
63
|
* A publish scope in the one form the registry names things by — `@acme` — from any
|
|
64
|
-
* spelling `--scope` or `package.json::uniweb.scope` may carry
|
|
65
|
-
* `@acme/…`). Null when there is no handle in it.
|
|
64
|
+
* spelling `--scope` or a schemas-only package's `package.json::uniweb.scope` may carry
|
|
65
|
+
* it in (`@acme`, `acme`, `@acme/…`). Null when there is no handle in it. (A
|
|
66
|
+
* foundation's scope is part of its name, `@acme/marketing`, and needs no reading.)
|
|
66
67
|
*
|
|
67
68
|
* ⛔ Every name built from a scope goes through this. The `.uwx` assembly
|
|
68
69
|
* (`@uniweb/build`'s `buildRegistryPackage`) has always accepted `acme` as well as
|
package/src/utils/scaffold.js
CHANGED
|
@@ -76,14 +76,16 @@ export async function getWorkspaceTemplateOutputs({ blank = false } = {}) {
|
|
|
76
76
|
}
|
|
77
77
|
|
|
78
78
|
/**
|
|
79
|
-
* Scaffold a foundation from the foundation package template
|
|
79
|
+
* Scaffold a foundation from the foundation package template.
|
|
80
80
|
*
|
|
81
|
-
* @param {string} targetDir
|
|
81
|
+
* @param {string} targetDir
|
|
82
82
|
* @param {Object} context - Template context
|
|
83
|
-
* @param {string} context.name - Package name
|
|
83
|
+
* @param {string} context.name - Package name — workspace plumbing (`src`, `docs-src`)
|
|
84
|
+
* @param {string} [context.registryName] - The foundation's own name, written to
|
|
85
|
+
* main.js's `name`: what it registers as (`@org/<name>`). Omitted when none can be
|
|
86
|
+
* made, and `uniweb register` asks for one
|
|
84
87
|
* @param {string} context.projectName - Workspace name
|
|
85
|
-
* @param {boolean} [context.isExtension]
|
|
86
|
-
* @param {Object} [options] - Processing options
|
|
88
|
+
* @param {boolean} [context.isExtension]
|
|
87
89
|
*/
|
|
88
90
|
export async function scaffoldFoundation(targetDir, context, options = {}) {
|
|
89
91
|
registerVersions(getVersionsForTemplates())
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
* demonstrated.**
|
|
52
52
|
*/
|
|
53
53
|
|
|
54
|
+
import { WORKSPACE_HEADER } from '../backend/client.js'
|
|
54
55
|
import { createHash } from 'node:crypto'
|
|
55
56
|
import { humanBytes } from './bytes.js'
|
|
56
57
|
import { fetchWithRetry, isTransientStatus } from './fetch-retry.js'
|
|
@@ -62,6 +63,9 @@ import { fetchWithRetry, isTransientStatus } from './fetch-retry.js'
|
|
|
62
63
|
* @param {string} opts.apiBase - backend origin
|
|
63
64
|
* @param {string} opts.token - bearer, used on the direct arm only
|
|
64
65
|
* @param {string} opts.siteUuid - the site these files belong to (path segment)
|
|
66
|
+
* @param {string|null} [opts.workspace] - the workspace the requests name, `@handle`
|
|
67
|
+
* (`x-uniweb-workspace`) — on the plan and on every DIRECT PUT, never on a presigned
|
|
68
|
+
* one, which is self-authorizing and must carry nothing it was not signed with
|
|
65
69
|
* @param {{ data: Record<string, unknown> }|null} opts.ball - source of the set:
|
|
66
70
|
* `{ "<relpath under dist/data>": <json> }`, media refs already rewritten
|
|
67
71
|
* @param {(m: string) => void} [opts.onProgress]
|
|
@@ -72,9 +76,13 @@ export async function uploadSiteData({
|
|
|
72
76
|
apiBase,
|
|
73
77
|
token,
|
|
74
78
|
siteUuid,
|
|
79
|
+
workspace = null,
|
|
75
80
|
ball,
|
|
76
81
|
onProgress = () => {}
|
|
77
82
|
}) {
|
|
83
|
+
// A plan made with the header mints BARE PUT urls: the PUT names its workspace with
|
|
84
|
+
// the header like every other request (backend, 2026-09-23).
|
|
85
|
+
const workspaceHeader = workspace ? { [WORKSPACE_HEADER]: workspace } : {}
|
|
78
86
|
// ⛔ An EMPTY set still posts a plan, and that is the whole point of this lane
|
|
79
87
|
// being a manifest rather than a stream of files.
|
|
80
88
|
//
|
|
@@ -131,7 +139,8 @@ export async function uploadSiteData({
|
|
|
131
139
|
method: 'POST',
|
|
132
140
|
headers: {
|
|
133
141
|
'Content-Type': 'application/json',
|
|
134
|
-
Authorization: `Bearer ${token}
|
|
142
|
+
Authorization: `Bearer ${token}`,
|
|
143
|
+
...workspaceHeader
|
|
135
144
|
},
|
|
136
145
|
body: JSON.stringify({
|
|
137
146
|
files: files.map(({ path, content_type, size, sha256 }) => ({
|
|
@@ -172,7 +181,9 @@ export async function uploadSiteData({
|
|
|
172
181
|
// PUT is a bearer-authed backend route; a presigned URL is self-authorizing
|
|
173
182
|
// and must NOT carry a foreign bearer, which can break signature validation.
|
|
174
183
|
const authHeaders =
|
|
175
|
-
plan.mode === 'presigned'
|
|
184
|
+
plan.mode === 'presigned'
|
|
185
|
+
? {}
|
|
186
|
+
: { Authorization: `Bearer ${token}`, ...workspaceHeader }
|
|
176
187
|
|
|
177
188
|
const uploaded = []
|
|
178
189
|
const failed = []
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* - defaultLayout: Name of the default layout from src/layouts/ (optional)
|
|
7
7
|
* - props: Foundation-wide props accessible via website.foundationProps
|
|
8
8
|
*
|
|
9
|
-
*
|
|
9
|
+
* Name: `name` in the default export is what this foundation registers as
|
|
10
|
+
* (@org/<name>) — `uniweb create` adds it. The version comes from package.json.
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
// Create a layout at src/layouts/MyLayout/index.jsx
|
|
@@ -14,10 +14,19 @@
|
|
|
14
14
|
|
|
15
15
|
{{#if isExtension}}
|
|
16
16
|
export default {
|
|
17
|
+
{{#if registryName}}
|
|
18
|
+
name: '{{registryName}}',
|
|
19
|
+
{{/if}}
|
|
17
20
|
extension: true,
|
|
18
21
|
}
|
|
19
22
|
{{else}}
|
|
20
23
|
export default {
|
|
24
|
+
{{#if registryName}}
|
|
25
|
+
// ─── Name ───────────────────────────────────────────────────────────────────
|
|
26
|
+
// What this foundation registers as — @<org>/<name> — and what sites pin.
|
|
27
|
+
name: '{{registryName}}',
|
|
28
|
+
|
|
29
|
+
{{/if}}
|
|
21
30
|
// ─── Layout ─────────────────────────────────────────────────────────────────
|
|
22
31
|
// Create layouts in layouts/MyLayout/index.jsx (auto-discovered).
|
|
23
32
|
// defaultLayout: 'MyLayout',
|
|
@@ -46,8 +46,9 @@ index: home
|
|
|
46
46
|
# records/article/ → @/article (your foundation's)
|
|
47
47
|
# records/std/person/ → @std/person
|
|
48
48
|
# A file whose name starts with `_` is not a record.
|
|
49
|
-
# records.yml
|
|
50
|
-
# with `scope:`.
|
|
49
|
+
# records/folder.yml OPTIONAL — sorts records into folders a query can slice
|
|
50
|
+
# with `scope:`. The one file in records/ that is not a
|
|
51
|
+
# record. Most sites have none:
|
|
51
52
|
# - folder: archive
|
|
52
53
|
# records:
|
|
53
54
|
# - article/2025-*.md
|