@uniweb/build 0.55.0 → 0.56.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/build",
3
- "version": "0.55.0",
3
+ "version": "0.56.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.5",
61
- "@uniweb/content-writer": "^0.3.4",
60
+ "@uniweb/schemas": "^0.3.3",
62
61
  "@uniweb/projections": "^0.7.3",
63
- "@uniweb/theming": "^0.1.15",
62
+ "@uniweb/content-writer": "^0.3.4",
63
+ "@uniweb/content-reader": "^1.2.5",
64
64
  "@uniweb/semantic-parser": "^1.4.1",
65
- "@uniweb/schemas": "^0.3.3"
65
+ "@uniweb/theming": "^0.1.15"
66
66
  },
67
67
  "optionalDependencies": {
68
- "@uniweb/runtime": "^0.27.0"
68
+ "@uniweb/runtime": "^0.27.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/core": "^0.29.6",
78
- "@uniweb/runtime": "^0.27.0"
77
+ "@uniweb/runtime": "^0.27.2",
78
+ "@uniweb/core": "^0.29.6"
79
79
  },
80
80
  "peerDependenciesMeta": {
81
81
  "vite": {
@@ -1,30 +1,36 @@
1
1
  /**
2
2
  * deploy.yml writer
3
3
  *
4
- * Updates the lastDeploy.<target> block of a site's deploy.yml without
4
+ * Updates the deploys.<target> block of a site's deploy.yml without
5
5
  * reformatting the rest of the file. Uses the eemeli/yaml Document API
6
6
  * because js-yaml does not preserve comments on round-trip — the writer
7
- * must not destroy a developer's comments on the targets:/autoSave:
7
+ * must not destroy a developer's comments on the targets:/saveDeploys:
8
8
  * regions of the file.
9
9
  *
10
10
  * This is the only place in @uniweb/build that depends on `yaml`. The
11
11
  * loader (deploy-config.js) stays on js-yaml for read-only ingestion.
12
12
  *
13
- * Auto-save semantics:
14
- * - 'off' : no-op (CI / `--no-save`)
15
- * - 'lastDeploy' : touch ONLY lastDeploy.<targetName>
16
- * - 'full' : reserved; behaves as 'lastDeploy' for now
13
+ * `saveDeploys: false` (or `--no-save`) makes this a no-op; otherwise it
14
+ * touches ONLY deploys.<targetName> — plus targets.<targetName> when the file has no
15
+ * such target yet (a publish to a backend no target names). ⚠️ It was `autoSave: off|lastDeploy|full`
16
+ * until 2026-09-20, and `full` was reserved and behaved as `lastDeploy` — a
17
+ * boolean wearing a tri-state.
17
18
  *
18
19
  * First-deploy path: when deploy.yml does not exist, writes a fresh
19
20
  * file scaffolded with `default:`, a single entry under `targets:`, and
20
- * `autoSave: lastDeploy`. This is the only code path that writes the
21
+ * `saveDeploys: true`. This is the only code path that writes the
21
22
  * config region.
23
+ *
24
+ * And two removals, for `uniweb forget`: `forgetDeploys` drops the records one
25
+ * backend wrote and keeps every target; `forgetDeployYml` deletes the file, for a
26
+ * copy that is to become a new project.
22
27
  */
23
28
 
24
- import { readFile, writeFile } from 'node:fs/promises'
29
+ import { readFile, writeFile, unlink } from 'node:fs/promises'
25
30
  import { existsSync } from 'node:fs'
26
31
  import { join } from 'node:path'
27
32
  import { Document, parseDocument, isMap } from 'yaml'
33
+ import { normalizeOrigin } from '../uwx/sync-store.js'
28
34
 
29
35
  // The header every generated deploy.yml carries. It has to answer the question
30
36
  // a reader has while looking AT the file — who wrote this, and may I edit it —
@@ -39,7 +45,7 @@ const SCAFFOLD_HEADER = [
39
45
  ' deploy.yml — written by `uniweb deploy` / `uniweb publish`.',
40
46
  '',
41
47
  ' You do not create this file. The first successful deploy does, recording the',
42
- ' target you picked and what happened. Later deploys rewrite only `lastDeploy:`',
48
+ ' target you picked and what happened. Later deploys rewrite only `deploys:`',
43
49
  ' and leave the rest — including your comments — alone.',
44
50
  '',
45
51
  ' Safe to commit; no credentials live here. Host credentials come from the',
@@ -48,29 +54,34 @@ const SCAFFOLD_HEADER = [
48
54
  ' default: which target is used when none is named',
49
55
  ' targets: where this site ships. Edit to change the destination, or add',
50
56
  ' a target and pick it with `--target <name>`',
51
- ' autoSave: `lastDeploy` to keep the record below, `off` to stop writing it',
52
- ' lastDeploy: what the last deploy did. A record, not a setting — nothing',
53
- ' reads it back, so a stale one is safe to delete',
57
+ ' deploys: what each target\'s last deploy did. A record, not a setting.',
58
+ ' `publish` reads one field back — a fingerprint of the service',
59
+ ' request it last sent — so deleting it only means the next',
60
+ ' publish re-sends that request',
61
+ ' saveDeploys: false to stop recording deploys',
62
+ '',
63
+ ' Which site this is on each backend lives in sync.json, not here.',
54
64
  ].join('\n')
55
65
 
56
66
  /**
57
- * Update or create deploy.yml with a fresh lastDeploy.<target> entry.
67
+ * Update or create deploy.yml with a fresh deploys.<target> entry.
58
68
  *
59
69
  * @param {string} siteDir
60
70
  * @param {object} opts
61
71
  * @param {string} opts.targetName e.g. 'production'
62
- * @param {object} [opts.targetConfig] { host, ...host-specific }; only
63
- * used on first-deploy scaffold.
72
+ * @param {object} [opts.targetConfig] { host, ...host-specific }; the
73
+ * first-deploy scaffold's target, and a
74
+ * target to ADD when the file lacks it
64
75
  * @param {object} opts.lastDeploy { at, url, foundation, runtime,
65
76
  * artifactSha, ... }
66
- * @param {'off'|'lastDeploy'|'full'} opts.autoSave
77
+ * @param {boolean} opts.saveDeploys - false makes this a no-op
67
78
  * @returns {Promise<{ created: boolean, path: string } | null>}
68
- * null when autoSave is 'off' (no-op).
79
+ * null when saveDeploys is false (no-op).
69
80
  */
70
81
  export async function recordLastDeploy(siteDir, opts) {
71
- const { targetName, targetConfig, lastDeploy, autoSave } = opts
82
+ const { targetName, targetConfig, lastDeploy, saveDeploys } = opts
72
83
 
73
- if (autoSave === 'off') return null
84
+ if (saveDeploys === false) return null
74
85
 
75
86
  const path = join(siteDir, 'deploy.yml')
76
87
 
@@ -89,13 +100,29 @@ export async function recordLastDeploy(siteDir, opts) {
89
100
 
90
101
  const text = await readFile(path, 'utf8')
91
102
  const doc = parseDocument(text)
103
+ // Never rewrite a file we could not read — the caller reports it instead.
104
+ if (doc.errors.length) {
105
+ throw new Error(`deploy.yml did not parse: ${doc.errors[0].message.split('\n')[0]}`)
106
+ }
107
+
108
+ // A target the file does not have yet — a publish to a backend no target names
109
+ // (resolvePublishTarget) — is added beside the others. An existing target is never
110
+ // modified, and `default` / `saveDeploys` are never touched.
111
+ if (targetConfig?.host) {
112
+ const targetsNode = doc.get('targets', true)
113
+ if (!isMap(targetsNode)) {
114
+ if (targetsNode === undefined) doc.set('targets', { [targetName]: targetConfig })
115
+ } else if (!targetsNode.has(targetName)) {
116
+ targetsNode.set(targetName, targetConfig)
117
+ }
118
+ }
92
119
 
93
- // Touch ONLY lastDeploy.<targetName>. Never reach into targets/default/autoSave.
94
- let lastDeployNode = doc.get('lastDeploy', true)
95
- if (!isMap(lastDeployNode)) {
96
- doc.set('lastDeploy', { [targetName]: lastDeploy })
120
+ // Otherwise touch ONLY deploys.<targetName>.
121
+ let deploysNode = doc.get('deploys', true)
122
+ if (!isMap(deploysNode)) {
123
+ doc.set('deploys', { [targetName]: lastDeploy })
97
124
  } else {
98
- lastDeployNode.set(targetName, lastDeploy)
125
+ deploysNode.set(targetName, lastDeploy)
99
126
  }
100
127
 
101
128
  await writeFile(path, doc.toString(), 'utf8')
@@ -106,8 +133,8 @@ function scaffold({ targetName, targetConfig, lastDeploy }) {
106
133
  const doc = new Document({
107
134
  default: targetName,
108
135
  targets: { [targetName]: targetConfig },
109
- autoSave: 'lastDeploy',
110
- lastDeploy: { [targetName]: lastDeploy },
136
+ deploys: { [targetName]: lastDeploy },
137
+ saveDeploys: true,
111
138
  })
112
139
  doc.commentBefore = SCAFFOLD_HEADER
113
140
  return doc
@@ -131,11 +158,11 @@ function scaffold({ targetName, targetConfig, lastDeploy }) {
131
158
  *
132
159
  * Behavior:
133
160
  * - File missing: scaffold a fresh file with this target, set as
134
- * default, autoSave: lastDeploy. No lastDeploy block (no deploy
161
+ * default, saveDeploys: true. No deploys block (no deploy
135
162
  * has happened yet).
136
163
  * - File exists: merge targetConfig into targets.<targetName>
137
164
  * (overlapping keys overwritten, other keys preserved). Never
138
- * touches `default`, `autoSave`, `lastDeploy`, or other targets,
165
+ * touches `default`, `saveDeploys`, `deploys`, or other targets,
139
166
  * so adding a CI workflow to a project that already deploys
140
167
  * elsewhere doesn't change its deploy semantics.
141
168
  */
@@ -155,7 +182,7 @@ export async function recordTarget(siteDir, opts) {
155
182
  const doc = new Document({
156
183
  default: targetName,
157
184
  targets: { [targetName]: targetConfig },
158
- autoSave: 'lastDeploy',
185
+ saveDeploys: true,
159
186
  })
160
187
  doc.commentBefore = SCAFFOLD_HEADER
161
188
  await writeFile(path, doc.toString(), 'utf8')
@@ -187,3 +214,84 @@ export async function recordTarget(siteDir, opts) {
187
214
  await writeFile(path, doc.toString(), 'utf8')
188
215
  return { created: false, path, action: 'merge' }
189
216
  }
217
+
218
+ /**
219
+ * Remove the deploy records one backend wrote — `uniweb forget --backend <url>`.
220
+ *
221
+ * A record belongs to the backend it names: every publish writes
222
+ * `deploys.<target>.backend`. A record naming none falls back to its target's
223
+ * `backend:`, which only a `host: uniweb` target has — so no other host's history
224
+ * can match, and a record that names no backend anywhere is left alone rather than
225
+ * guessed at.
226
+ *
227
+ * ⛔ **`targets:` stays.** A target is a CHOICE of where to ship, not something a
228
+ * backend told us — the spec's own line between this file and `sync.json`. After
229
+ * the forget, the next publish there simply creates a new site.
230
+ *
231
+ * Why the records go at all: each names the forgotten site (`siteUuid`, `url`), and
232
+ * its request fingerprint describes what was sent to THAT site. Kept, they would
233
+ * sit beside a target that now publishes somewhere else entirely.
234
+ *
235
+ * @param {string} siteDir
236
+ * @param {string} origin
237
+ * @returns {Promise<string[]>} the target names whose record was removed, sorted
238
+ * @throws when deploy.yml exists and does not parse — the caller decides whether
239
+ * that blocks anything; this does not rewrite a file it could not read
240
+ */
241
+ export async function forgetDeploys(siteDir, origin) {
242
+ const key = normalizeOrigin(origin)
243
+ const path = join(siteDir, 'deploy.yml')
244
+ if (!key || !existsSync(path)) return []
245
+
246
+ const doc = parseDocument(await readFile(path, 'utf8'))
247
+ if (doc.errors.length) {
248
+ throw new Error(`deploy.yml did not parse: ${doc.errors[0].message.split('\n')[0]}`)
249
+ }
250
+ const deploysNode = doc.get('deploys', true)
251
+ if (!isMap(deploysNode)) return []
252
+
253
+ const js = doc.toJS() || {}
254
+ const removed = []
255
+ for (const [name, record] of Object.entries(js.deploys || {})) {
256
+ const target = js.targets?.[name]
257
+ const named =
258
+ record?.backend ?? (target?.host === 'uniweb' ? target?.backend : undefined)
259
+ if (normalizeOrigin(named) === key) removed.push(name)
260
+ }
261
+ if (!removed.length) return []
262
+
263
+ for (const name of removed) deploysNode.delete(name)
264
+ // An empty `deploys: {}` says nothing and would be the only thing left of the
265
+ // backend in this file.
266
+ if (!deploysNode.items.length) doc.delete('deploys')
267
+ await writeFile(path, doc.toString(), 'utf8')
268
+ return removed.sort()
269
+ }
270
+
271
+ /**
272
+ * Delete deploy.yml outright — `uniweb forget --all`, for a copy that is to become a
273
+ * new project.
274
+ *
275
+ * ⭐ **The targets go too, not just the records** *[Diego, 2026-09-21: "if it's for
276
+ * duplicating a site project and making a new site from it, … the whole deploy.yml
277
+ * file has to be deleted"]*. They name the ORIGINAL's destinations — its Pages
278
+ * project, its bucket, its domain — so a deploy from the copy would overwrite the
279
+ * original there. The copy's first deploy writes a fresh file.
280
+ *
281
+ * @param {string} siteDir
282
+ * @returns {Promise<string[]|null>} the target names it held, or null when there
283
+ * was no file. A file that does not parse is removed all the same and reports `[]`.
284
+ */
285
+ export async function forgetDeployYml(siteDir) {
286
+ const path = join(siteDir, 'deploy.yml')
287
+ if (!existsSync(path)) return null
288
+ let names = []
289
+ try {
290
+ const doc = parseDocument(await readFile(path, 'utf8'))
291
+ if (!doc.errors.length) names = Object.keys(doc.toJS()?.targets || {}).sort()
292
+ } catch {
293
+ /* unreadable — still the original's, still removed */
294
+ }
295
+ await unlink(path)
296
+ return names
297
+ }
@@ -5,14 +5,21 @@
5
5
  * by name. Read-only — never writes. See deploy-config-writer.js for
6
6
  * the write path.
7
7
  *
8
- * On-disk shape:
8
+ * On-disk shape (spec: kb/framework/reference/deploy-yml.md):
9
9
  * default: production
10
10
  * targets:
11
- * production: { host, ...host-specific config }
11
+ * production: { host, backend?, ...host-specific config }
12
12
  * preview: { host, ...host-specific config }
13
- * autoSave: lastDeploy | off | full
14
- * lastDeploy:
15
- * production: { at, url, foundation, runtime, artifactSha, ... }
13
+ * deploys:
14
+ * production: { at, host, url?, git?, servicesRequest? }
15
+ * saveDeploys: true
16
+ *
17
+ * ⚠️ `deploys` was `lastDeploy` and `saveDeploys` was `autoSave: off|lastDeploy|full`
18
+ * until 2026-09-20. `full` was "reserved; behaves as lastDeploy" — so the tri-state
19
+ * was already a boolean in practice, and the rename says so. `autoSave` also
20
+ * SOUNDED like it covered the whole file, which it never did.
21
+ *
22
+ * ⚠️ Nothing a backend minted lives here any more. Identity is `sync.json`'s.
16
23
  *
17
24
  * The CLI and the build pipeline both call resolveTarget() to turn a
18
25
  * loaded document + a (possibly null) --target flag into a concrete
@@ -26,9 +33,9 @@ import { existsSync } from 'node:fs'
26
33
  import { join } from 'node:path'
27
34
  import yaml from 'js-yaml'
28
35
  import { YAML_OPTIONS } from '../utils/yaml-schema.js'
36
+ import { normalizeOrigin } from '../uwx/sync-store.js'
29
37
 
30
38
  const DEFAULT_TARGET_NAME = 'production'
31
- const VALID_AUTOSAVE = new Set(['off', 'lastDeploy', 'full'])
32
39
 
33
40
  /**
34
41
  * Load and validate deploy.yml from `siteDir`. Returns null when the
@@ -57,7 +64,7 @@ export async function loadDeployYml(siteDir) {
57
64
  * targetName: string,
58
65
  * host: string,
59
66
  * config: object,
60
- * autoSave: 'off'|'lastDeploy'|'full',
67
+ * saveDeploys: boolean,
61
68
  * fromFile: boolean,
62
69
  * }}
63
70
  */
@@ -73,7 +80,7 @@ export function resolveTarget(deployYml, requestedTarget) {
73
80
  targetName: DEFAULT_TARGET_NAME,
74
81
  host: 'uniweb',
75
82
  config: {},
76
- autoSave: 'lastDeploy',
83
+ saveDeploys: true,
77
84
  fromFile: false,
78
85
  }
79
86
  }
@@ -99,11 +106,73 @@ export function resolveTarget(deployYml, requestedTarget) {
99
106
  targetName: name,
100
107
  host,
101
108
  config,
102
- autoSave: deployYml.autoSave || 'lastDeploy',
109
+ saveDeploys: deployYml.saveDeploys !== false,
103
110
  fromFile: true,
104
111
  }
105
112
  }
106
113
 
114
+ /**
115
+ * The target a publish to `origin` records its deploy under.
116
+ *
117
+ * ⭐ **The target follows the backend, never the reverse.** A publish goes to ONE backend,
118
+ * decided before this runs — `--backend`, or the backend the user is logged in to
119
+ * *[Diego, 2026-09-21: "publish should publish to the backend the user logged in to"]*.
120
+ * The default target is only a tie-break, winning when it names that backend too.
121
+ *
122
+ * 1. no deploy.yml → the scaffold: `production`, naming `origin`
123
+ * 2. `host: uniweb` targets naming `origin` → the default one if it is among them,
124
+ * else the first by name. A target with no `backend:` names `defaultBackend` —
125
+ * its documented meaning
126
+ * 3. none → a NEW target, named after the backend's host (`uniweb.app`,
127
+ * `localhost:8080`), `-2`, `-3`… if taken. The caller adds it when it persists.
128
+ *
129
+ * ⛔ Until 2026-09-21 publish always resolved the DEFAULT target, whatever backend it
130
+ * published to: a publish to a second backend recorded over the default target's deploy,
131
+ * and compared its service request against that target's fingerprint.
132
+ *
133
+ * @param {object|null} deployYml - from loadDeployYml
134
+ * @param {string} origin - the backend being published to
135
+ * @param {{ defaultBackend?: string }} [opts]
136
+ * @returns {{ targetName: string, host: 'uniweb', config: object, saveDeploys: boolean,
137
+ * fromFile: boolean }} `fromFile: false` means the file has no such target yet
138
+ */
139
+ export function resolvePublishTarget(deployYml, origin, { defaultBackend } = {}) {
140
+ const key = normalizeOrigin(origin)
141
+ if (!deployYml) {
142
+ return {
143
+ targetName: DEFAULT_TARGET_NAME,
144
+ host: 'uniweb',
145
+ config: { backend: key },
146
+ saveDeploys: true,
147
+ fromFile: false,
148
+ }
149
+ }
150
+ const saveDeploys = deployYml.saveDeploys !== false
151
+ const targets = deployYml.targets || {}
152
+ const naming = Object.keys(targets)
153
+ .filter((name) => {
154
+ const t = targets[name]
155
+ return t && t.host === 'uniweb' && normalizeOrigin(t.backend || defaultBackend) === key
156
+ })
157
+ .sort()
158
+ if (naming.length) {
159
+ const targetName = naming.includes(deployYml.default) ? deployYml.default : naming[0]
160
+ const { host, ...config } = targets[targetName]
161
+ return { targetName, host, config, saveDeploys, fromFile: true }
162
+ }
163
+ let base = 'uniweb'
164
+ try {
165
+ base = new URL(key).host || base
166
+ } catch {
167
+ /* keep the fallback */
168
+ }
169
+ let targetName = base
170
+ for (let i = 2; Object.prototype.hasOwnProperty.call(targets, targetName); i++) {
171
+ targetName = `${base}-${i}`
172
+ }
173
+ return { targetName, host: 'uniweb', config: { backend: key }, saveDeploys, fromFile: false }
174
+ }
175
+
107
176
  function validate(doc, path) {
108
177
  if (typeof doc !== 'object' || doc === null || Array.isArray(doc)) {
109
178
  throw new Error(`${path}: top level must be a map.`)
@@ -111,39 +180,19 @@ function validate(doc, path) {
111
180
  if (doc.targets !== undefined && (typeof doc.targets !== 'object' || Array.isArray(doc.targets))) {
112
181
  throw new Error(`${path}: \`targets\` must be a map.`)
113
182
  }
114
- // A site has exactly ONE Uniweb identity — `site.yml::$uuid`, a single scalar,
115
- // set by the create and never moved. So two targets naming `host: uniweb` cannot
116
- // both be coherent: whichever one `default:` selects is the site, and the other
117
- // silently describes a site that does not exist. The per-target `backend` key
118
- // makes that look expressible, which is exactly why it is worth rejecting rather
119
- // than leaving to be discovered.
120
- //
121
- // *(Diego, 2026-08-12: multiple deploys are not expected in general, and are not
122
- // allowed for Uniweb Cloud specifically — the only host that reads `$uuid` at
123
- // all. Every other host is stateless from the CLI's side, so any number of those
124
- // targets is fine.)*
125
- if (doc.targets && typeof doc.targets === 'object' && !Array.isArray(doc.targets)) {
126
- const uniwebTargets = Object.entries(doc.targets)
127
- .filter(([, t]) => t && typeof t === 'object' && t.host === 'uniweb')
128
- .map(([name]) => name)
129
- if (uniwebTargets.length > 1) {
130
- throw new Error(
131
- `${path}: only one target may use \`host: uniweb\` — found ${uniwebTargets.length} (${uniwebTargets.join(', ')}). ` +
132
- 'A site has a single Uniweb identity (site.yml::$uuid), so a second Uniweb target cannot describe a different site. ' +
133
- 'Keep one, or point the others at a third-party host.'
134
- )
135
- }
136
- }
137
- if (doc.autoSave !== undefined && !VALID_AUTOSAVE.has(doc.autoSave)) {
138
- throw new Error(
139
- `${path}: \`autoSave\` must be one of: ${[...VALID_AUTOSAVE].join(', ')}.`
140
- )
183
+ // ⭐ SEVERAL `host: uniweb` TARGETS ARE LEGAL (2026-09-20). This rejected a second one
184
+ // because a site had a single identity, `site.yml::$uuid`, so two Uniweb targets
185
+ // could not both be coherent. Identity is keyed by backend origin in `sync.json`
186
+ // now: two targets naming DIFFERENT backends are two sites, and two naming the SAME
187
+ // backend are one site shipped twice. The reason is gone, so the rejection is too.
188
+ if (doc.saveDeploys !== undefined && typeof doc.saveDeploys !== 'boolean') {
189
+ throw new Error(`${path}: \`saveDeploys\` must be true or false.`)
141
190
  }
142
191
  if (doc.default !== undefined && typeof doc.default !== 'string') {
143
192
  throw new Error(`${path}: \`default\` must be a string.`)
144
193
  }
145
- if (doc.lastDeploy !== undefined && (typeof doc.lastDeploy !== 'object' || Array.isArray(doc.lastDeploy))) {
146
- throw new Error(`${path}: \`lastDeploy\` must be a map.`)
194
+ if (doc.deploys !== undefined && (typeof doc.deploys !== 'object' || Array.isArray(doc.deploys))) {
195
+ throw new Error(`${path}: \`deploys\` must be a map.`)
147
196
  }
148
197
  return doc
149
198
  }
package/src/site/index.js CHANGED
@@ -50,5 +50,5 @@ export {
50
50
  applyPostProcessing,
51
51
  mergeDataIntoContent
52
52
  } from './data-fetcher.js'
53
- export { loadDeployYml, resolveTarget } from './deploy-config.js'
54
- export { recordLastDeploy, recordTarget } from './deploy-config-writer.js'
53
+ export { loadDeployYml, resolveTarget, resolvePublishTarget } from './deploy-config.js'
54
+ export { recordLastDeploy, recordTarget, forgetDeploys, forgetDeployYml } from './deploy-config-writer.js'
@@ -26,7 +26,7 @@
26
26
  //
27
27
  // When a query declares no schema, the query-name convention fills it
28
28
  // (`articles` → `@/articles`). Absent the file entirely, a site simply has no
29
- // queries — and therefore delivers no collection data.
29
+ // queries — and therefore delivers no fetched data.
30
30
 
31
31
  import { join } from 'node:path'
32
32
  import { existsSync, readFileSync } from 'node:fs'