uniweb 0.13.17 → 0.14.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/README.md +1 -1
- package/package.json +7 -7
- package/partials/agents.md +51 -0
- package/src/backend/client.js +173 -40
- package/src/backend/data-bundle.js +15 -3
- package/src/backend/foundation-bring-along.js +76 -21
- package/src/backend/payment-handoff.js +28 -9
- package/src/backend/site-media.js +147 -19
- package/src/backend/site-sync.js +362 -40
- package/src/commands/add.js +575 -273
- package/src/commands/build.js +160 -57
- package/src/commands/clone.js +79 -26
- package/src/commands/content.js +11 -7
- package/src/commands/deploy.js +80 -32
- package/src/commands/dev.js +39 -17
- package/src/commands/docs.js +44 -16
- package/src/commands/doctor.js +338 -82
- package/src/commands/export.js +15 -7
- package/src/commands/handoff.js +6 -2
- package/src/commands/i18n.js +248 -99
- package/src/commands/inspect.js +48 -19
- package/src/commands/invite.js +9 -3
- package/src/commands/org.js +19 -5
- package/src/commands/publish.js +229 -60
- package/src/commands/pull.js +157 -48
- package/src/commands/push.js +144 -42
- package/src/commands/refresh.js +37 -10
- package/src/commands/register.js +235 -64
- package/src/commands/rename.js +126 -46
- package/src/commands/runtime.js +74 -24
- package/src/commands/status.js +48 -15
- package/src/commands/sync.js +7 -2
- package/src/commands/template.js +12 -4
- package/src/commands/update.js +263 -87
- package/src/commands/validate.js +115 -32
- package/src/framework-index.json +15 -13
- package/src/index.js +298 -145
- package/src/templates/fetchers/github.js +7 -7
- package/src/templates/fetchers/npm.js +3 -3
- package/src/templates/fetchers/release.js +21 -18
- package/src/templates/index.js +34 -12
- package/src/templates/processor.js +44 -12
- package/src/templates/resolver.js +42 -15
- package/src/templates/validator.js +14 -7
- package/src/utils/asset-upload.js +90 -22
- package/src/utils/code-upload.js +62 -37
- package/src/utils/config.js +31 -10
- package/src/utils/dep-survey.js +33 -7
- package/src/utils/destination-prompt.js +27 -17
- package/src/utils/git.js +66 -22
- package/src/utils/host-prompt.js +4 -3
- package/src/utils/install-integrity.js +8 -4
- package/src/utils/interactive.js +3 -3
- package/src/utils/json-file.js +6 -2
- package/src/utils/names.js +15 -6
- package/src/utils/placement.js +16 -8
- package/src/utils/registry-auth.js +185 -57
- package/src/utils/registry-orgs.js +142 -53
- package/src/utils/runtime-upload.js +52 -14
- package/src/utils/scaffold.js +55 -14
- package/src/utils/site-content-refs.js +3 -1
- package/src/utils/update-check.js +43 -17
- package/src/utils/workspace.js +13 -7
- package/src/versions.js +18 -7
- package/templates/site/_gitignore +5 -0
package/src/commands/doctor.js
CHANGED
|
@@ -2,10 +2,21 @@
|
|
|
2
2
|
* uniweb doctor - Diagnose project configuration issues
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
existsSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
readdirSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
rmSync
|
|
12
|
+
} from 'node:fs'
|
|
6
13
|
import { join, resolve, basename, dirname, relative } from 'node:path'
|
|
7
14
|
import yaml from 'js-yaml'
|
|
8
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
resolveFoundationSrcPath,
|
|
17
|
+
classifyPackage,
|
|
18
|
+
isExtensionPackage as buildIsExtensionPackage
|
|
19
|
+
} from '@uniweb/build'
|
|
9
20
|
import { loadDeployYml } from '@uniweb/build/site'
|
|
10
21
|
import { listAdapters } from '@uniweb/build/hosts'
|
|
11
22
|
import { getCliVersion } from '../versions.js'
|
|
@@ -15,6 +26,7 @@ import { surveyWorkspaceDeps } from '../utils/dep-survey.js'
|
|
|
15
26
|
import { discoverFoundations, discoverSites } from '../utils/discover.js'
|
|
16
27
|
import { checkSiteInstall } from '../utils/install-integrity.js'
|
|
17
28
|
import { findWorkspaceRoot } from '../utils/workspace.js'
|
|
29
|
+
import { DATA_DIR } from '@uniweb/core/data-paths'
|
|
18
30
|
|
|
19
31
|
/**
|
|
20
32
|
* Parse the `--fix [<issue-id>]` flag.
|
|
@@ -125,7 +137,11 @@ function loadPackageJson(dir) {
|
|
|
125
137
|
function loadSiteYml(dir) {
|
|
126
138
|
const ymlPath = join(dir, 'site.yml')
|
|
127
139
|
const yamlPath = join(dir, 'site.yaml')
|
|
128
|
-
const configPath = existsSync(ymlPath)
|
|
140
|
+
const configPath = existsSync(ymlPath)
|
|
141
|
+
? ymlPath
|
|
142
|
+
: existsSync(yamlPath)
|
|
143
|
+
? yamlPath
|
|
144
|
+
: null
|
|
129
145
|
if (!configPath) return null
|
|
130
146
|
try {
|
|
131
147
|
return yaml.load(readFileSync(configPath, 'utf8'))
|
|
@@ -134,6 +150,103 @@ function loadSiteYml(dir) {
|
|
|
134
150
|
}
|
|
135
151
|
}
|
|
136
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Diagnose the compiled-collection output directory.
|
|
155
|
+
*
|
|
156
|
+
* `public/<DATA_DIR>/` holds what the build compiles from `collections/`, and
|
|
157
|
+
* nothing else — `collections/` is the only supported way to provide
|
|
158
|
+
* structured data. Two consequences, both checked here:
|
|
159
|
+
*
|
|
160
|
+
* 1. **The mapping is a bijection.** Every entry should be backed by a
|
|
161
|
+
* declared collection. An entry that isn't is stale — most often a
|
|
162
|
+
* collection removed from `site.yml`, whose compiled records stay on disk
|
|
163
|
+
* and keep being deployed. The build reconciles *within* a collection it
|
|
164
|
+
* still knows about; it cannot reconcile one it has never heard of, which
|
|
165
|
+
* is exactly the case that needs a declaration to compare against.
|
|
166
|
+
*
|
|
167
|
+
* 2. **It should not be committed.** It is generated into the source tree
|
|
168
|
+
* rather than `dist/`, so without an ignore rule it looks permanent, gets
|
|
169
|
+
* committed, and then looks like something you may edit. That is how
|
|
170
|
+
* hand-authored files ended up there in the first place.
|
|
171
|
+
*/
|
|
172
|
+
function checkGeneratedDataDir({ sitePath, siteName, siteYml, issues, shouldFix, fixed }) {
|
|
173
|
+
const dataDir = join(sitePath, 'public', DATA_DIR)
|
|
174
|
+
const declared = new Set(Object.keys(siteYml.collections || {}))
|
|
175
|
+
|
|
176
|
+
if (existsSync(dataDir)) {
|
|
177
|
+
// A collection `x` owns `x.json` (the cascade) and `x/` (per-record files
|
|
178
|
+
// when it declares `deferred:`). Anything else is unaccounted for.
|
|
179
|
+
const orphans = readdirSync(dataDir, { withFileTypes: true })
|
|
180
|
+
.filter((entry) => {
|
|
181
|
+
const name = entry.isDirectory()
|
|
182
|
+
? entry.name
|
|
183
|
+
: entry.name.replace(/\.json$/i, '')
|
|
184
|
+
return !declared.has(name)
|
|
185
|
+
})
|
|
186
|
+
.map((entry) => (entry.isDirectory() ? `${entry.name}/` : entry.name))
|
|
187
|
+
|
|
188
|
+
if (orphans.length > 0) {
|
|
189
|
+
const id = 'orphaned-collection-output'
|
|
190
|
+
issues.push({
|
|
191
|
+
id,
|
|
192
|
+
type: 'warning',
|
|
193
|
+
site: siteName,
|
|
194
|
+
message: `${orphans.length} entr${orphans.length === 1 ? 'y' : 'ies'} in public/${DATA_DIR}/ with no declared collection`
|
|
195
|
+
})
|
|
196
|
+
warn(`[${id}] Stale output in public/${DATA_DIR}/: ${orphans.join(', ')}`)
|
|
197
|
+
log(
|
|
198
|
+
` No collection in site.yml produces ${orphans.length === 1 ? 'it' : 'these'}. ` +
|
|
199
|
+
`${orphans.length === 1 ? 'It is' : 'They are'} still served and deployed.`
|
|
200
|
+
)
|
|
201
|
+
if (shouldFix(id)) {
|
|
202
|
+
for (const entry of orphans) {
|
|
203
|
+
rmSync(join(dataDir, entry.replace(/\/$/, '')), { recursive: true, force: true })
|
|
204
|
+
}
|
|
205
|
+
fixed(`removed ${orphans.length} stale entr${orphans.length === 1 ? 'y' : 'ies'} from public/${DATA_DIR}/`)
|
|
206
|
+
} else {
|
|
207
|
+
log(` ${colors.dim}Run \`uniweb doctor --fix ${id}\` to remove ${orphans.length === 1 ? 'it' : 'them'}.${colors.reset}`)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// The ignore rule. Checked whether or not the directory exists yet — the
|
|
213
|
+
// point is to have it in place *before* the first build writes there.
|
|
214
|
+
const gitignorePath = join(sitePath, '.gitignore')
|
|
215
|
+
const rule = `public/${DATA_DIR}/`
|
|
216
|
+
const existing = existsSync(gitignorePath) ? readFileSync(gitignorePath, 'utf8') : null
|
|
217
|
+
const covered =
|
|
218
|
+
existing !== null &&
|
|
219
|
+
existing
|
|
220
|
+
.split('\n')
|
|
221
|
+
.map((line) => line.trim().replace(/\/$/, ''))
|
|
222
|
+
.includes(rule.replace(/\/$/, ''))
|
|
223
|
+
|
|
224
|
+
if (!covered) {
|
|
225
|
+
const id = 'generated-data-not-ignored'
|
|
226
|
+
issues.push({
|
|
227
|
+
id,
|
|
228
|
+
type: 'warning',
|
|
229
|
+
site: siteName,
|
|
230
|
+
message: `public/${DATA_DIR}/ is generated but not gitignored`
|
|
231
|
+
})
|
|
232
|
+
warn(`[${id}] public/${DATA_DIR}/ is build output but not in .gitignore`)
|
|
233
|
+
log(` Committing it means a diff on every build, and it invites editing what the build overwrites.`)
|
|
234
|
+
if (shouldFix(id)) {
|
|
235
|
+
const body = existing === null ? '' : existing.replace(/\n*$/, '\n')
|
|
236
|
+
writeFileSync(
|
|
237
|
+
gitignorePath,
|
|
238
|
+
`${body}\n# Compiled collections — generated from collections/\n${rule}\n`
|
|
239
|
+
)
|
|
240
|
+
fixed(`added ${rule} to ${gitignorePath}`)
|
|
241
|
+
if (existsSync(dataDir)) {
|
|
242
|
+
log(` ${colors.dim}Already-committed files need \`git rm -r --cached public/${DATA_DIR}\`.${colors.reset}`)
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
log(` ${colors.dim}Run \`uniweb doctor --fix ${id}\` to add it.${colors.reset}`)
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
137
250
|
/**
|
|
138
251
|
* Main doctor command
|
|
139
252
|
*/
|
|
@@ -147,7 +260,8 @@ export async function doctor(args = []) {
|
|
|
147
260
|
// each diagnostic below so the rewrite happens with full context.
|
|
148
261
|
const fixFlag = parseFixFlag(args)
|
|
149
262
|
const shouldFix = (id) => fixFlag === 'all' || fixFlag === id
|
|
150
|
-
const fixed = (msg) =>
|
|
263
|
+
const fixed = (msg) =>
|
|
264
|
+
console.log(` ${colors.green}↳ Fixed:${colors.reset} ${msg}`)
|
|
151
265
|
|
|
152
266
|
const projectDir = resolve(process.cwd())
|
|
153
267
|
|
|
@@ -157,7 +271,9 @@ export async function doctor(args = []) {
|
|
|
157
271
|
|
|
158
272
|
if (!workspaceDir) {
|
|
159
273
|
error('Not in a Uniweb workspace')
|
|
160
|
-
log(
|
|
274
|
+
log(
|
|
275
|
+
`${colors.dim}Run this command from your project root or a site/foundation directory.${colors.reset}`
|
|
276
|
+
)
|
|
161
277
|
process.exit(1)
|
|
162
278
|
}
|
|
163
279
|
|
|
@@ -181,31 +297,42 @@ export async function doctor(args = []) {
|
|
|
181
297
|
issues.push({
|
|
182
298
|
id: 'workspace-yaml-malformed',
|
|
183
299
|
type: 'error',
|
|
184
|
-
message: 'pnpm-workspace.yaml is malformed and could not be parsed.'
|
|
300
|
+
message: 'pnpm-workspace.yaml is malformed and could not be parsed.'
|
|
185
301
|
})
|
|
186
302
|
error('pnpm-workspace.yaml is malformed and could not be parsed.')
|
|
187
303
|
}
|
|
188
304
|
const rootPkg = loadPackageJson(workspaceDir)
|
|
189
|
-
const pkgWorkspaces = Array.isArray(rootPkg?.workspaces)
|
|
305
|
+
const pkgWorkspaces = Array.isArray(rootPkg?.workspaces)
|
|
306
|
+
? rootPkg.workspaces
|
|
307
|
+
: []
|
|
190
308
|
const ymlSet = new Set(ymlPackages)
|
|
191
309
|
const pkgSet = new Set(pkgWorkspaces)
|
|
192
|
-
const onlyInYml = [...ymlSet].filter(g => !pkgSet.has(g))
|
|
193
|
-
const onlyInPkg = [...pkgSet].filter(g => !ymlSet.has(g))
|
|
310
|
+
const onlyInYml = [...ymlSet].filter((g) => !pkgSet.has(g))
|
|
311
|
+
const onlyInPkg = [...pkgSet].filter((g) => !ymlSet.has(g))
|
|
194
312
|
if (onlyInYml.length || onlyInPkg.length) {
|
|
195
313
|
const issue = {
|
|
196
314
|
id: 'workspace-manifests-out-of-sync',
|
|
197
315
|
type: 'warn',
|
|
198
316
|
message: `pnpm-workspace.yaml and package.json::workspaces declare different package globs.`,
|
|
199
|
-
details: { onlyInYml, onlyInPkg }
|
|
317
|
+
details: { onlyInYml, onlyInPkg }
|
|
200
318
|
}
|
|
201
319
|
issues.push(issue)
|
|
202
|
-
warn(
|
|
203
|
-
|
|
204
|
-
|
|
320
|
+
warn(
|
|
321
|
+
`[workspace-manifests-out-of-sync] pnpm-workspace.yaml and package.json::workspaces are out of sync:`
|
|
322
|
+
)
|
|
323
|
+
if (onlyInYml.length)
|
|
324
|
+
log(` only in pnpm-workspace.yaml: ${onlyInYml.join(', ')}`)
|
|
325
|
+
if (onlyInPkg.length)
|
|
326
|
+
log(` only in package.json::workspaces: ${onlyInPkg.join(', ')}`)
|
|
205
327
|
if (shouldFix('workspace-manifests-out-of-sync')) {
|
|
206
328
|
// Canonical resolution: write the union to both manifests.
|
|
207
|
-
const union = Array.from(
|
|
208
|
-
|
|
329
|
+
const union = Array.from(
|
|
330
|
+
new Set([...ymlPackages, ...pkgWorkspaces])
|
|
331
|
+
).sort()
|
|
332
|
+
writeFileSync(
|
|
333
|
+
ymlPath,
|
|
334
|
+
yaml.dump({ packages: union }, { flowLevel: -1, quotingType: '"' })
|
|
335
|
+
)
|
|
209
336
|
const rootPkgPath = join(workspaceDir, 'package.json')
|
|
210
337
|
const rootPkgSrc = readFileSync(rootPkgPath, 'utf-8')
|
|
211
338
|
const rootPkg = JSON.parse(rootPkgSrc)
|
|
@@ -214,7 +341,9 @@ export async function doctor(args = []) {
|
|
|
214
341
|
issue.fixed = true
|
|
215
342
|
fixed(`wrote union [${union.join(', ')}] to both manifests`)
|
|
216
343
|
} else {
|
|
217
|
-
log(
|
|
344
|
+
log(
|
|
345
|
+
` ${colors.dim}Pick one set of globs and copy it to the other manifest. The two should always match — pnpm reads pnpm-workspace.yaml, npm/yarn read package.json::workspaces.${colors.reset}`
|
|
346
|
+
)
|
|
218
347
|
}
|
|
219
348
|
}
|
|
220
349
|
}
|
|
@@ -243,7 +372,10 @@ export async function doctor(args = []) {
|
|
|
243
372
|
} else {
|
|
244
373
|
success(`Found ${foundations.length} foundation(s):`)
|
|
245
374
|
for (const f of foundations) {
|
|
246
|
-
const nameMismatch =
|
|
375
|
+
const nameMismatch =
|
|
376
|
+
f.name !== f.folderName
|
|
377
|
+
? ` ${colors.dim}(folder: ${f.folderName}/)${colors.reset}`
|
|
378
|
+
: ''
|
|
247
379
|
log(` • ${f.name}${nameMismatch}`)
|
|
248
380
|
}
|
|
249
381
|
}
|
|
@@ -252,16 +384,19 @@ export async function doctor(args = []) {
|
|
|
252
384
|
log('')
|
|
253
385
|
success(`Found ${extensions.length} extension(s):`)
|
|
254
386
|
for (const e of extensions) {
|
|
255
|
-
const nameMismatch =
|
|
387
|
+
const nameMismatch =
|
|
388
|
+
e.name !== e.folderName
|
|
389
|
+
? ` ${colors.dim}(folder: ${e.folderName}/)${colors.reset}`
|
|
390
|
+
: ''
|
|
256
391
|
log(` • ${e.name}${nameMismatch}`)
|
|
257
392
|
}
|
|
258
393
|
}
|
|
259
394
|
|
|
260
395
|
// Discover sites via the canonical workspace globs (same rationale as
|
|
261
396
|
// foundations above: respects whatever layout the user chose).
|
|
262
|
-
const sites = (await discoverSites(workspaceDir)).map(s => ({
|
|
397
|
+
const sites = (await discoverSites(workspaceDir)).map((s) => ({
|
|
263
398
|
path: join(workspaceDir, s.path),
|
|
264
|
-
name: s.name
|
|
399
|
+
name: s.name
|
|
265
400
|
}))
|
|
266
401
|
|
|
267
402
|
if (sites.length === 0) {
|
|
@@ -286,13 +421,21 @@ export async function doctor(args = []) {
|
|
|
286
421
|
info(`Checking site: ${siteName}`)
|
|
287
422
|
|
|
288
423
|
if (!siteYml) {
|
|
289
|
-
issues.push({
|
|
424
|
+
issues.push({
|
|
425
|
+
type: 'error',
|
|
426
|
+
site: siteName,
|
|
427
|
+
message: 'Missing site.yml'
|
|
428
|
+
})
|
|
290
429
|
error('Missing site.yml')
|
|
291
430
|
continue
|
|
292
431
|
}
|
|
293
432
|
|
|
294
433
|
if (!sitePkg) {
|
|
295
|
-
issues.push({
|
|
434
|
+
issues.push({
|
|
435
|
+
type: 'error',
|
|
436
|
+
site: siteName,
|
|
437
|
+
message: 'Missing package.json'
|
|
438
|
+
})
|
|
296
439
|
error('Missing package.json')
|
|
297
440
|
continue
|
|
298
441
|
}
|
|
@@ -309,14 +452,37 @@ export async function doctor(args = []) {
|
|
|
309
452
|
id: 'agents-index-relative-links',
|
|
310
453
|
type: 'warning',
|
|
311
454
|
site: siteName,
|
|
312
|
-
message:
|
|
455
|
+
message:
|
|
456
|
+
'llms.txt will use root-relative links because seo.baseUrl is unset'
|
|
313
457
|
}
|
|
314
458
|
issues.push(issue)
|
|
315
459
|
warn(`[agents-index-relative-links] llms.txt links will be root-relative`)
|
|
316
|
-
log(
|
|
317
|
-
|
|
460
|
+
log(
|
|
461
|
+
` Set ${colors.green}seo.baseUrl${colors.reset} in site.yml so agents get absolute URLs`
|
|
462
|
+
)
|
|
463
|
+
log(
|
|
464
|
+
` (this also turns on sitemap.xml and robots.txt, which are skipped without it)`
|
|
465
|
+
)
|
|
318
466
|
}
|
|
319
467
|
|
|
468
|
+
// `public/<DATA_DIR>/` is the build's output directory and nothing else —
|
|
469
|
+
// `collections/` is the only supported way to provide structured data. That
|
|
470
|
+
// makes the mapping a bijection: every entry there should be backed by a
|
|
471
|
+
// declared collection, so anything else is stale, and identifiable.
|
|
472
|
+
//
|
|
473
|
+
// It matters because the directory is written into the source tree rather
|
|
474
|
+
// than dist/, so what lands there persists and gets deployed. A collection
|
|
475
|
+
// removed from site.yml leaves its compiled records behind — visible to
|
|
476
|
+
// anyone who knows the URL, listed by nothing.
|
|
477
|
+
checkGeneratedDataDir({
|
|
478
|
+
sitePath,
|
|
479
|
+
siteName,
|
|
480
|
+
siteYml,
|
|
481
|
+
issues,
|
|
482
|
+
shouldFix,
|
|
483
|
+
fixed
|
|
484
|
+
})
|
|
485
|
+
|
|
320
486
|
const foundationName = siteYml.foundation
|
|
321
487
|
if (!foundationName) {
|
|
322
488
|
warn('No foundation specified in site.yml (using runtime loading?)')
|
|
@@ -324,28 +490,41 @@ export async function doctor(args = []) {
|
|
|
324
490
|
}
|
|
325
491
|
|
|
326
492
|
// Check if foundation name matches a known foundation
|
|
327
|
-
const matchingFoundation = foundations.find(
|
|
493
|
+
const matchingFoundation = foundations.find(
|
|
494
|
+
(f) => f.name === foundationName
|
|
495
|
+
)
|
|
328
496
|
|
|
329
497
|
if (!matchingFoundation) {
|
|
330
498
|
// Check if it might match a folder name instead
|
|
331
|
-
const folderMatch = foundations.find(
|
|
499
|
+
const folderMatch = foundations.find(
|
|
500
|
+
(f) => f.folderName === foundationName
|
|
501
|
+
)
|
|
332
502
|
if (folderMatch) {
|
|
333
503
|
const issue = {
|
|
334
504
|
id: 'foundation-name-mismatch',
|
|
335
505
|
type: 'error',
|
|
336
506
|
site: siteName,
|
|
337
|
-
message: `Foundation mismatch: site.yml uses folder name "${foundationName}" instead of package name "${folderMatch.name}"
|
|
507
|
+
message: `Foundation mismatch: site.yml uses folder name "${foundationName}" instead of package name "${folderMatch.name}"`
|
|
338
508
|
}
|
|
339
509
|
issues.push(issue)
|
|
340
510
|
error(`[foundation-name-mismatch] Foundation mismatch:`)
|
|
341
|
-
log(
|
|
342
|
-
|
|
511
|
+
log(
|
|
512
|
+
` site.yml says: ${colors.yellow}foundation: ${foundationName}${colors.reset}`
|
|
513
|
+
)
|
|
514
|
+
log(
|
|
515
|
+
` This matches the folder name, but the package name is: ${colors.green}${folderMatch.name}${colors.reset}`
|
|
516
|
+
)
|
|
343
517
|
if (shouldFix('foundation-name-mismatch')) {
|
|
344
518
|
const siteYmlPath = join(sitePath, 'site.yml')
|
|
345
519
|
const updated = { ...siteYml, foundation: folderMatch.name }
|
|
346
|
-
writeFileSync(
|
|
520
|
+
writeFileSync(
|
|
521
|
+
siteYmlPath,
|
|
522
|
+
yaml.dump(updated, { flowLevel: -1, quotingType: "'" })
|
|
523
|
+
)
|
|
347
524
|
issue.fixed = true
|
|
348
|
-
fixed(
|
|
525
|
+
fixed(
|
|
526
|
+
`${relative(workspaceDir, siteYmlPath)} now references "${folderMatch.name}"`
|
|
527
|
+
)
|
|
349
528
|
} else {
|
|
350
529
|
log('')
|
|
351
530
|
log(` ${colors.dim}To fix, update site.yml:${colors.reset}`)
|
|
@@ -379,21 +558,30 @@ export async function doctor(args = []) {
|
|
|
379
558
|
id: 'missing-foundation-dep',
|
|
380
559
|
type: 'error',
|
|
381
560
|
site: siteName,
|
|
382
|
-
message: `Missing dependency "${foundationName}" in package.json
|
|
561
|
+
message: `Missing dependency "${foundationName}" in package.json`
|
|
383
562
|
}
|
|
384
563
|
issues.push(issue)
|
|
385
|
-
error(
|
|
564
|
+
error(
|
|
565
|
+
`[missing-foundation-dep] Missing dependency "${foundationName}" in package.json`
|
|
566
|
+
)
|
|
386
567
|
const expectedPath = `file:${relative(sitePath, matchingFoundation.path)}`
|
|
387
568
|
if (shouldFix('missing-foundation-dep')) {
|
|
388
569
|
const sitePkgPath = join(sitePath, 'package.json')
|
|
389
570
|
const updatedPkg = { ...sitePkg }
|
|
390
|
-
updatedPkg.dependencies = {
|
|
571
|
+
updatedPkg.dependencies = {
|
|
572
|
+
...(updatedPkg.dependencies || {}),
|
|
573
|
+
[foundationName]: expectedPath
|
|
574
|
+
}
|
|
391
575
|
writeJsonPreservingStyle(sitePkgPath, updatedPkg)
|
|
392
576
|
issue.fixed = true
|
|
393
|
-
fixed(
|
|
577
|
+
fixed(
|
|
578
|
+
`added "${foundationName}": "${expectedPath}" to ${relative(workspaceDir, sitePkgPath)}`
|
|
579
|
+
)
|
|
394
580
|
} else {
|
|
395
581
|
log('')
|
|
396
|
-
log(
|
|
582
|
+
log(
|
|
583
|
+
` ${colors.dim}Add to site's package.json dependencies:${colors.reset}`
|
|
584
|
+
)
|
|
397
585
|
log(` "${foundationName}": "${expectedPath}"`)
|
|
398
586
|
}
|
|
399
587
|
continue
|
|
@@ -406,7 +594,7 @@ export async function doctor(args = []) {
|
|
|
406
594
|
id: 'stale-file-path',
|
|
407
595
|
type: 'error',
|
|
408
596
|
site: siteName,
|
|
409
|
-
message: `Dependency path doesn't exist: ${depValue}
|
|
597
|
+
message: `Dependency path doesn't exist: ${depValue}`
|
|
410
598
|
}
|
|
411
599
|
issues.push(issue)
|
|
412
600
|
error(`[stale-file-path] Dependency path doesn't exist: ${depValue}`)
|
|
@@ -414,10 +602,15 @@ export async function doctor(args = []) {
|
|
|
414
602
|
if (shouldFix('stale-file-path')) {
|
|
415
603
|
const sitePkgPath = join(sitePath, 'package.json')
|
|
416
604
|
const updatedPkg = { ...sitePkg }
|
|
417
|
-
updatedPkg.dependencies = {
|
|
605
|
+
updatedPkg.dependencies = {
|
|
606
|
+
...(updatedPkg.dependencies || {}),
|
|
607
|
+
[foundationName]: expectedPath
|
|
608
|
+
}
|
|
418
609
|
writeJsonPreservingStyle(sitePkgPath, updatedPkg)
|
|
419
610
|
issue.fixed = true
|
|
420
|
-
fixed(
|
|
611
|
+
fixed(
|
|
612
|
+
`updated "${foundationName}" to "${expectedPath}" in ${relative(workspaceDir, sitePkgPath)}`
|
|
613
|
+
)
|
|
421
614
|
} else {
|
|
422
615
|
log('')
|
|
423
616
|
log(` ${colors.dim}Update site's package.json:${colors.reset}`)
|
|
@@ -439,7 +632,12 @@ export async function doctor(args = []) {
|
|
|
439
632
|
matchingFoundation,
|
|
440
633
|
workspaceDir
|
|
441
634
|
)) {
|
|
442
|
-
issues.push({
|
|
635
|
+
issues.push({
|
|
636
|
+
id: finding.id,
|
|
637
|
+
type: finding.severity,
|
|
638
|
+
site: siteName,
|
|
639
|
+
message: finding.message
|
|
640
|
+
})
|
|
443
641
|
error(`[${finding.id}] ${finding.message}`)
|
|
444
642
|
log(`${colors.dim}${finding.detail}${colors.reset}`)
|
|
445
643
|
log('')
|
|
@@ -516,7 +714,7 @@ export async function doctor(args = []) {
|
|
|
516
714
|
// Check if any foundation with extension: true is wired as a primary foundation
|
|
517
715
|
for (const f of foundations) {
|
|
518
716
|
if (isExtensionPackage(f.path)) {
|
|
519
|
-
const sitesUsingAsPrimary = sites.filter(s => {
|
|
717
|
+
const sitesUsingAsPrimary = sites.filter((s) => {
|
|
520
718
|
const siteYml = loadSiteYml(s.path)
|
|
521
719
|
return siteYml?.foundation === f.name
|
|
522
720
|
})
|
|
@@ -526,8 +724,12 @@ export async function doctor(args = []) {
|
|
|
526
724
|
site: site.name,
|
|
527
725
|
message: `Foundation "${f.name}" declares extension: true but is wired as the primary foundation. It should be in extensions: instead.`
|
|
528
726
|
})
|
|
529
|
-
warn(
|
|
530
|
-
|
|
727
|
+
warn(
|
|
728
|
+
`"${f.name}" is an extension but used as primary foundation in site "${site.name}"`
|
|
729
|
+
)
|
|
730
|
+
log(
|
|
731
|
+
` ${colors.dim}Move it to extensions: in site.yml instead of foundation:${colors.reset}`
|
|
732
|
+
)
|
|
531
733
|
}
|
|
532
734
|
}
|
|
533
735
|
}
|
|
@@ -541,16 +743,23 @@ export async function doctor(args = []) {
|
|
|
541
743
|
if (typeof extUrl !== 'string') continue
|
|
542
744
|
|
|
543
745
|
// Skip remote URLs — can't validate those
|
|
544
|
-
if (extUrl.startsWith('http://') || extUrl.startsWith('https://'))
|
|
746
|
+
if (extUrl.startsWith('http://') || extUrl.startsWith('https://'))
|
|
747
|
+
continue
|
|
545
748
|
|
|
546
749
|
// Local extension URL: check if it maps to a known extension
|
|
547
750
|
// URLs like /effects/foundation.js or /extensions/effects/foundation.js
|
|
548
751
|
const urlParts = extUrl.replace(/^\//, '').split('/')
|
|
549
|
-
const extName =
|
|
752
|
+
const extName =
|
|
753
|
+
urlParts.length >= 2 ? urlParts[urlParts.length - 2] : urlParts[0]
|
|
550
754
|
|
|
551
755
|
// Check if a matching extension exists and is built
|
|
552
|
-
const matchingExt = extensions.find(
|
|
553
|
-
|
|
756
|
+
const matchingExt = extensions.find(
|
|
757
|
+
(e) => e.folderName === extName || e.name === extName
|
|
758
|
+
)
|
|
759
|
+
if (
|
|
760
|
+
matchingExt &&
|
|
761
|
+
!existsSync(join(matchingExt.path, 'dist', 'entry.js'))
|
|
762
|
+
) {
|
|
554
763
|
issues.push({
|
|
555
764
|
type: 'warn',
|
|
556
765
|
site: site.name,
|
|
@@ -582,9 +791,11 @@ export async function doctor(args = []) {
|
|
|
582
791
|
id: 'deploy-yml-malformed',
|
|
583
792
|
type: 'error',
|
|
584
793
|
site: siteName,
|
|
585
|
-
message: `deploy.yml is malformed: ${err.message}
|
|
794
|
+
message: `deploy.yml is malformed: ${err.message}`
|
|
586
795
|
})
|
|
587
|
-
error(
|
|
796
|
+
error(
|
|
797
|
+
`[deploy-yml-malformed] ${relative(workspaceDir, deployYmlPath)}: ${err.message}`
|
|
798
|
+
)
|
|
588
799
|
continue
|
|
589
800
|
}
|
|
590
801
|
if (!deployYml) continue
|
|
@@ -598,10 +809,14 @@ export async function doctor(args = []) {
|
|
|
598
809
|
id: 'deploy-yml-default-unknown-target',
|
|
599
810
|
type: 'error',
|
|
600
811
|
site: siteName,
|
|
601
|
-
message: `deploy.yml: default '${deployYml.default}' is not in targets (known: ${targetNames.join(', ') || '(none)'})
|
|
812
|
+
message: `deploy.yml: default '${deployYml.default}' is not in targets (known: ${targetNames.join(', ') || '(none)'})`
|
|
602
813
|
})
|
|
603
|
-
error(
|
|
604
|
-
|
|
814
|
+
error(
|
|
815
|
+
`[deploy-yml-default-unknown-target] ${siteName}: default '${deployYml.default}' is not declared under \`targets:\`.`
|
|
816
|
+
)
|
|
817
|
+
log(
|
|
818
|
+
` ${colors.dim}Add it to targets: or change default: to one of: ${targetNames.join(', ') || '(none)'}.${colors.reset}`
|
|
819
|
+
)
|
|
605
820
|
}
|
|
606
821
|
|
|
607
822
|
// Each target must have a host, and host should be a known adapter.
|
|
@@ -612,9 +827,11 @@ export async function doctor(args = []) {
|
|
|
612
827
|
id: 'deploy-yml-target-missing-host',
|
|
613
828
|
type: 'error',
|
|
614
829
|
site: siteName,
|
|
615
|
-
message: `deploy.yml: targets.${name} is missing \`host
|
|
830
|
+
message: `deploy.yml: targets.${name} is missing \`host\``
|
|
616
831
|
})
|
|
617
|
-
error(
|
|
832
|
+
error(
|
|
833
|
+
`[deploy-yml-target-missing-host] ${siteName}: targets.${name} has no \`host\` field.`
|
|
834
|
+
)
|
|
618
835
|
continue
|
|
619
836
|
}
|
|
620
837
|
// 'uniweb' is the platform default and isn't in the static-host
|
|
@@ -624,10 +841,14 @@ export async function doctor(args = []) {
|
|
|
624
841
|
id: 'deploy-yml-unknown-host',
|
|
625
842
|
type: 'warn',
|
|
626
843
|
site: siteName,
|
|
627
|
-
message: `deploy.yml: targets.${name}.host '${cfg.host}' is not a known adapter
|
|
844
|
+
message: `deploy.yml: targets.${name}.host '${cfg.host}' is not a known adapter`
|
|
628
845
|
})
|
|
629
|
-
warn(
|
|
630
|
-
|
|
846
|
+
warn(
|
|
847
|
+
`[deploy-yml-unknown-host] ${siteName}: targets.${name}.host: '${cfg.host}' is not a known built-in adapter.`
|
|
848
|
+
)
|
|
849
|
+
log(
|
|
850
|
+
` ${colors.dim}Known: ${[...knownAdapters].sort().join(', ')}, plus 'uniweb'.${colors.reset}`
|
|
851
|
+
)
|
|
631
852
|
}
|
|
632
853
|
}
|
|
633
854
|
|
|
@@ -637,7 +858,9 @@ export async function doctor(args = []) {
|
|
|
637
858
|
// configured. A drift here means a deploy will silently lose the
|
|
638
859
|
// custom domain.
|
|
639
860
|
const cnamePath = join(sitePath, 'public', 'CNAME')
|
|
640
|
-
const ghTarget = Object.entries(targets).find(
|
|
861
|
+
const ghTarget = Object.entries(targets).find(
|
|
862
|
+
([, c]) => c?.host === 'github-pages'
|
|
863
|
+
)
|
|
641
864
|
if (ghTarget) {
|
|
642
865
|
const [ghName, ghCfg] = ghTarget
|
|
643
866
|
if (ghCfg.domain) {
|
|
@@ -655,9 +878,10 @@ export async function doctor(args = []) {
|
|
|
655
878
|
id: 'github-pages-cname-mismatch',
|
|
656
879
|
type: 'warn',
|
|
657
880
|
site: siteName,
|
|
658
|
-
message:
|
|
659
|
-
|
|
660
|
-
|
|
881
|
+
message:
|
|
882
|
+
actual === null
|
|
883
|
+
? `deploy.yml: targets.${ghName}.domain is '${expected}' but ${relative(workspaceDir, cnamePath)} is missing`
|
|
884
|
+
: `deploy.yml: targets.${ghName}.domain is '${expected}' but ${relative(workspaceDir, cnamePath)} contains '${actual}'`
|
|
661
885
|
}
|
|
662
886
|
issues.push(issue)
|
|
663
887
|
warn(`[github-pages-cname-mismatch] ${siteName}: ${issue.message}`)
|
|
@@ -668,7 +892,9 @@ export async function doctor(args = []) {
|
|
|
668
892
|
issue.fixed = true
|
|
669
893
|
fixed(`wrote ${relative(workspaceDir, cnamePath)} = ${expected}`)
|
|
670
894
|
} else {
|
|
671
|
-
log(
|
|
895
|
+
log(
|
|
896
|
+
` ${colors.dim}Re-run \`uniweb add ci --domain ${expected} --force\` (or pass --fix to write CNAME from deploy.yml).${colors.reset}`
|
|
897
|
+
)
|
|
672
898
|
}
|
|
673
899
|
}
|
|
674
900
|
} else if (existsSync(cnamePath)) {
|
|
@@ -685,10 +911,14 @@ export async function doctor(args = []) {
|
|
|
685
911
|
id: 'github-pages-cname-orphan',
|
|
686
912
|
type: 'warn',
|
|
687
913
|
site: siteName,
|
|
688
|
-
message: `${relative(workspaceDir, cnamePath)} exists ('${actual}') but deploy.yml's github-pages target has no \`domain
|
|
914
|
+
message: `${relative(workspaceDir, cnamePath)} exists ('${actual}') but deploy.yml's github-pages target has no \`domain\``
|
|
689
915
|
})
|
|
690
|
-
warn(
|
|
691
|
-
|
|
916
|
+
warn(
|
|
917
|
+
`[github-pages-cname-orphan] ${siteName}: ${relative(workspaceDir, cnamePath)} exists ('${actual}') but the github-pages target has no \`domain\` set.`
|
|
918
|
+
)
|
|
919
|
+
log(
|
|
920
|
+
` ${colors.dim}Either add \`domain: ${actual}\` to targets.${ghName}, or delete the CNAME if you no longer want a custom domain.${colors.reset}`
|
|
921
|
+
)
|
|
692
922
|
}
|
|
693
923
|
}
|
|
694
924
|
|
|
@@ -698,17 +928,26 @@ export async function doctor(args = []) {
|
|
|
698
928
|
// Not an error — site.yml's value is just dormant for that target —
|
|
699
929
|
// but worth flagging so the user knows the value isn't taking effect.
|
|
700
930
|
const siteYml = loadSiteYml(sitePath)
|
|
701
|
-
const workflowPath = join(
|
|
931
|
+
const workflowPath = join(
|
|
932
|
+
workspaceDir,
|
|
933
|
+
'.github/workflows/deploy-github-pages.yml'
|
|
934
|
+
)
|
|
702
935
|
if (siteYml?.base && existsSync(workflowPath) && ghTarget) {
|
|
703
936
|
issues.push({
|
|
704
937
|
id: 'site-base-overridden-by-workflow',
|
|
705
938
|
type: 'warn',
|
|
706
939
|
site: siteName,
|
|
707
|
-
message: `site.yml::base ('${siteYml.base}') is dormant — the GH Pages workflow sets UNIWEB_BASE at build time, which takes precedence
|
|
940
|
+
message: `site.yml::base ('${siteYml.base}') is dormant — the GH Pages workflow sets UNIWEB_BASE at build time, which takes precedence`
|
|
708
941
|
})
|
|
709
|
-
warn(
|
|
710
|
-
|
|
711
|
-
|
|
942
|
+
warn(
|
|
943
|
+
`[site-base-overridden-by-workflow] ${siteName}: site.yml has \`base: ${siteYml.base}\`, but the GH Pages workflow sets UNIWEB_BASE.`
|
|
944
|
+
)
|
|
945
|
+
log(
|
|
946
|
+
` ${colors.dim}site.yml::base is the fallback when no UNIWEB_BASE env var is set. The workflow always sets it, so this value is ignored on GH Pages deploys.${colors.reset}`
|
|
947
|
+
)
|
|
948
|
+
log(
|
|
949
|
+
` ${colors.dim}If the GH Pages deploy is the only target, you can remove \`base:\` from site.yml. Otherwise leave it — it still applies to other deploy targets.${colors.reset}`
|
|
950
|
+
)
|
|
712
951
|
}
|
|
713
952
|
}
|
|
714
953
|
|
|
@@ -717,12 +956,17 @@ export async function doctor(args = []) {
|
|
|
717
956
|
// Check @uniweb/* dep alignment with the running CLI
|
|
718
957
|
log('')
|
|
719
958
|
const depSurvey = await surveyWorkspaceDeps(workspaceDir)
|
|
720
|
-
const behindDeps = depSurvey.rows.filter(r => r.status === 'behind')
|
|
959
|
+
const behindDeps = depSurvey.rows.filter((r) => r.status === 'behind')
|
|
721
960
|
if (behindDeps.length > 0) {
|
|
722
|
-
const names = [...new Set(behindDeps.map(r => r.name))].sort()
|
|
723
|
-
warn(
|
|
961
|
+
const names = [...new Set(behindDeps.map((r) => r.name))].sort()
|
|
962
|
+
warn(
|
|
963
|
+
`${behindDeps.length} workspace dep declaration${behindDeps.length === 1 ? '' : 's'} lag the CLI (v${cliVersion}): ${names.join(', ')}`
|
|
964
|
+
)
|
|
724
965
|
info(`Run: uniweb update`)
|
|
725
|
-
issues.push({
|
|
966
|
+
issues.push({
|
|
967
|
+
type: 'warn',
|
|
968
|
+
message: `${behindDeps.length} @uniweb/* dep declaration(s) behind CLI v${cliVersion}`
|
|
969
|
+
})
|
|
726
970
|
} else if (depSurvey.anyAhead) {
|
|
727
971
|
success(`@uniweb/* deps are aligned or ahead of the CLI (v${cliVersion})`)
|
|
728
972
|
} else {
|
|
@@ -746,7 +990,10 @@ export async function doctor(args = []) {
|
|
|
746
990
|
} else if (agentsVersion !== cliVersion) {
|
|
747
991
|
warn(`AGENTS.md is outdated (v${agentsVersion} → v${cliVersion})`)
|
|
748
992
|
info(`Run: uniweb update`)
|
|
749
|
-
issues.push({
|
|
993
|
+
issues.push({
|
|
994
|
+
type: 'warn',
|
|
995
|
+
message: `AGENTS.md outdated (v${agentsVersion} → v${cliVersion})`
|
|
996
|
+
})
|
|
750
997
|
} else {
|
|
751
998
|
success(`AGENTS.md is up to date (v${cliVersion})`)
|
|
752
999
|
}
|
|
@@ -757,9 +1004,9 @@ export async function doctor(args = []) {
|
|
|
757
1004
|
|
|
758
1005
|
// Fixed issues no longer count toward errors/warnings — they're
|
|
759
1006
|
// resolved during this run. Exit code is based on what remains.
|
|
760
|
-
const errors = issues.filter(i => i.type === 'error' && !i.fixed)
|
|
761
|
-
const warnings = issues.filter(i => i.type === 'warn' && !i.fixed)
|
|
762
|
-
const fixedCount = issues.filter(i => i.fixed).length
|
|
1007
|
+
const errors = issues.filter((i) => i.type === 'error' && !i.fixed)
|
|
1008
|
+
const warnings = issues.filter((i) => i.type === 'warn' && !i.fixed)
|
|
1009
|
+
const fixedCount = issues.filter((i) => i.fixed).length
|
|
763
1010
|
|
|
764
1011
|
if (errors.length === 0 && warnings.length === 0 && fixedCount === 0) {
|
|
765
1012
|
log('')
|
|
@@ -774,13 +1021,22 @@ export async function doctor(args = []) {
|
|
|
774
1021
|
log(`${colors.red}${errors.length} error(s) remaining${colors.reset}`)
|
|
775
1022
|
}
|
|
776
1023
|
if (warnings.length > 0) {
|
|
777
|
-
log(
|
|
1024
|
+
log(
|
|
1025
|
+
`${colors.yellow}${warnings.length} warning(s) remaining${colors.reset}`
|
|
1026
|
+
)
|
|
778
1027
|
}
|
|
779
1028
|
if (fixFlag && (errors.length > 0 || warnings.length > 0)) {
|
|
780
|
-
log(
|
|
1029
|
+
log(
|
|
1030
|
+
`${colors.dim}Some issues were not auto-fixable. See the diagnostics above.${colors.reset}`
|
|
1031
|
+
)
|
|
781
1032
|
}
|
|
782
1033
|
log('')
|
|
783
1034
|
}
|
|
784
1035
|
|
|
785
|
-
return {
|
|
1036
|
+
return {
|
|
1037
|
+
issues,
|
|
1038
|
+
errors: errors.length,
|
|
1039
|
+
warnings: warnings.length,
|
|
1040
|
+
fixed: fixedCount
|
|
1041
|
+
}
|
|
786
1042
|
}
|