uniweb 0.10.13 → 0.12.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 +20 -18
- package/package.json +2 -2
- package/partials/agents.md +163 -39
- package/src/commands/add.js +152 -233
- package/src/commands/build.js +14 -42
- package/src/commands/deploy.js +262 -3
- package/src/commands/docs.js +5 -6
- package/src/commands/doctor.js +21 -22
- package/src/commands/publish.js +255 -34
- package/src/framework-index.json +3 -3
- package/src/index.js +27 -14
- package/src/utils/auth.js +82 -6
- package/src/utils/names.js +9 -2
- package/src/utils/registry.js +88 -16
- package/src/utils/scaffold.js +8 -2
- package/src/utils/workspace.js +8 -46
- package/starter/site/pages/home/1-welcome.md.hbs +1 -1
- package/templates/foundation/{src/foundation.js.hbs → main.js.hbs} +1 -1
- package/templates/foundation/package.json.hbs +6 -6
- package/templates/foundation/{src/styles.css → styles.css} +1 -1
- package/templates/site/index.html.hbs +1 -1
- package/templates/site/theme.yml +1 -1
- package/templates/workspace/README.md.hbs +9 -7
- /package/starter/foundation/{src/foundation.js → main.js} +0 -0
- /package/starter/foundation/{src/sections → sections}/Section/index.jsx +0 -0
- /package/starter/foundation/{src/sections → sections}/Section/meta.js +0 -0
- /package/templates/foundation/{src/components → components}/.gitkeep +0 -0
- /package/templates/foundation/{src/sections → sections}/.gitkeep +0 -0
- /package/templates/site/{main.js → entry.js} +0 -0
package/src/commands/deploy.js
CHANGED
|
@@ -42,6 +42,8 @@ import { resolve, join, basename, relative, sep } from 'node:path'
|
|
|
42
42
|
import { execSync } from 'node:child_process'
|
|
43
43
|
import yaml from 'js-yaml'
|
|
44
44
|
|
|
45
|
+
import { detectFoundationType } from '@uniweb/build'
|
|
46
|
+
|
|
45
47
|
import { ensureAuth } from '../utils/auth.js'
|
|
46
48
|
import { getBackendUrl, getRegistryUrl } from '../utils/config.js'
|
|
47
49
|
import {
|
|
@@ -55,6 +57,70 @@ import { isNonInteractive, getCliPrefix } from '../utils/interactive.js'
|
|
|
55
57
|
const REVIEW_TIMEOUT_MS = 15 * 60 * 1000 // 15 min — matches PHP session TTL
|
|
56
58
|
const ASSET_UPLOAD_CONCURRENCY = 6
|
|
57
59
|
const ASSET_UPLOAD_RETRIES = 2
|
|
60
|
+
|
|
61
|
+
const FOUNDATION_POLICIES = new Set(['exact', 'auto-patch', 'auto-minor'])
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Parse the `foundation:` field from site.yml into a normalized shape.
|
|
65
|
+
*
|
|
66
|
+
* Accepts:
|
|
67
|
+
* - string: '@uniweb/votiverse@0.1.1'
|
|
68
|
+
* - object: { ref: '@uniweb/votiverse@0.1.1', policy?: ..., pinned?: true }
|
|
69
|
+
*
|
|
70
|
+
* Returns one of:
|
|
71
|
+
* - { error: 'description of what's wrong' }
|
|
72
|
+
* - { normalized, policy?, pinned } where `normalized` is whichever
|
|
73
|
+
* shape we received (string or { ref, policy?, pinned? }) — the Worker
|
|
74
|
+
* accepts both. `policy`/`pinned` are also returned individually so
|
|
75
|
+
* the CLI can print friendly diagnostics.
|
|
76
|
+
*
|
|
77
|
+
* Validation rules (mirrors publish.js::parseFoundationConfig):
|
|
78
|
+
* - `policy` must be one of 'exact', 'auto-patch', 'auto-minor'
|
|
79
|
+
* - `pinned: true` + `policy: not-exact` is rejected as conflicting
|
|
80
|
+
*/
|
|
81
|
+
function parseSiteFoundation(input) {
|
|
82
|
+
if (typeof input === 'string') {
|
|
83
|
+
return { normalized: input, policy: null, pinned: false }
|
|
84
|
+
}
|
|
85
|
+
if (!input || typeof input !== 'object') {
|
|
86
|
+
return { error: 'foundation must be a string or object' }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Object form must carry `ref`; everything else is metadata.
|
|
90
|
+
if (!input.ref || typeof input.ref !== 'string') {
|
|
91
|
+
return { error: 'foundation.ref is required when using object form' }
|
|
92
|
+
}
|
|
93
|
+
if (!/^@[a-z0-9_-]+\/[a-z0-9_-]+@.+$/.test(input.ref)) {
|
|
94
|
+
return {
|
|
95
|
+
error: `foundation.ref does not match @namespace/name@version: '${input.ref}'`,
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let policy = null
|
|
100
|
+
if (input.policy != null) {
|
|
101
|
+
if (!FOUNDATION_POLICIES.has(input.policy)) {
|
|
102
|
+
return {
|
|
103
|
+
error: `foundation.policy must be one of 'exact', 'auto-patch', 'auto-minor' (got '${input.policy}')`,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
policy = input.policy
|
|
107
|
+
}
|
|
108
|
+
const pinned = input.pinned === true
|
|
109
|
+
|
|
110
|
+
if (pinned && policy && policy !== 'exact') {
|
|
111
|
+
return {
|
|
112
|
+
error: `foundation: 'pinned: true' conflicts with policy '${policy}'. ` +
|
|
113
|
+
`Use either 'pinned: true' or 'policy: \"exact\"' (they're equivalent), or drop one.`,
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
normalized: { ref: input.ref, ...(policy ? { policy } : {}), ...(pinned ? { pinned: true } : {}) },
|
|
119
|
+
policy: pinned ? 'exact' : policy,
|
|
120
|
+
pinned,
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
58
124
|
// Vite content-addresses these formats. Same filename → same content, so we
|
|
59
125
|
// can skip upload without checking size. Unhashed formats fall through to
|
|
60
126
|
// size-compare diffing.
|
|
@@ -98,6 +164,100 @@ const say = {
|
|
|
98
164
|
dim: (m) => console.log(` ${c.dim}${m}${c.reset}`),
|
|
99
165
|
}
|
|
100
166
|
|
|
167
|
+
function readGitState(dir) {
|
|
168
|
+
try {
|
|
169
|
+
const sha = execSync('git rev-parse HEAD', {
|
|
170
|
+
cwd: dir,
|
|
171
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
172
|
+
}).toString().trim()
|
|
173
|
+
const status = execSync('git status --porcelain', {
|
|
174
|
+
cwd: dir,
|
|
175
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
176
|
+
}).toString()
|
|
177
|
+
return { gitSha: sha || null, gitDirty: status.length > 0 }
|
|
178
|
+
} catch {
|
|
179
|
+
return { gitSha: null, gitDirty: false }
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function composeFoundationUrl(ref, registryBase) {
|
|
184
|
+
if (typeof ref !== 'string') return null
|
|
185
|
+
if (ref.startsWith('https://') || ref.startsWith('http://')) return ref
|
|
186
|
+
const m = ref.match(/^(@[^/]+\/[^@]+|[^@]+)@(.+)$/)
|
|
187
|
+
if (!m || !registryBase) return null
|
|
188
|
+
const [, name, version] = m
|
|
189
|
+
return `${registryBase.replace(/\/$/, '')}/${name}/${version}/`
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Inspect a workspace-local foundation's `dist/publish.json` (Phase 1 receipt)
|
|
194
|
+
* and decide whether it's stale relative to the current source tree.
|
|
195
|
+
*
|
|
196
|
+
* Returns `{ stale, reason, receipt }`. The caller decides whether to
|
|
197
|
+
* auto-publish (Phase 2 default) or fail (`--no-auto-publish`).
|
|
198
|
+
*/
|
|
199
|
+
async function inspectLocalFoundationReceipt(localPath, { dirtyAsStale }) {
|
|
200
|
+
const receiptPath = join(localPath, 'dist', 'publish.json')
|
|
201
|
+
let receipt = null
|
|
202
|
+
try {
|
|
203
|
+
receipt = JSON.parse(await readFile(receiptPath, 'utf8'))
|
|
204
|
+
} catch {
|
|
205
|
+
return { stale: true, reason: 'no dist/publish.json (foundation has not been published from this checkout)' }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const { gitSha, gitDirty } = readGitState(localPath)
|
|
209
|
+
if (!gitSha) {
|
|
210
|
+
return { stale: true, reason: 'foundation directory is not in a git repo or has no commits', receipt }
|
|
211
|
+
}
|
|
212
|
+
if (receipt.publishedFromGitSha && receipt.publishedFromGitSha !== gitSha) {
|
|
213
|
+
return {
|
|
214
|
+
stale: true,
|
|
215
|
+
reason: `foundation has new commits since last publish (${receipt.publishedFromGitSha.slice(0, 7)} → ${gitSha.slice(0, 7)})`,
|
|
216
|
+
receipt,
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (gitDirty && dirtyAsStale) {
|
|
220
|
+
return { stale: true, reason: 'foundation working tree is dirty', receipt }
|
|
221
|
+
}
|
|
222
|
+
return { stale: false, receipt }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Read a workspace-local foundation's identity (scoped name + version) from
|
|
227
|
+
* its `dist/meta/schema.json` + `package.json`, mirroring `publish.js`'s
|
|
228
|
+
* namespace resolution. Returns the registry ref (`@ns/name@ver`), or null
|
|
229
|
+
* if any of the inputs are missing.
|
|
230
|
+
*/
|
|
231
|
+
async function deriveLocalFoundationRef(localPath) {
|
|
232
|
+
let pkg
|
|
233
|
+
try {
|
|
234
|
+
pkg = JSON.parse(await readFile(join(localPath, 'package.json'), 'utf8'))
|
|
235
|
+
} catch {
|
|
236
|
+
return null
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
let rawName, version
|
|
240
|
+
try {
|
|
241
|
+
const schema = JSON.parse(await readFile(join(localPath, 'dist', 'meta', 'schema.json'), 'utf8'))
|
|
242
|
+
rawName = schema._self?.name
|
|
243
|
+
version = schema._self?.version
|
|
244
|
+
} catch {
|
|
245
|
+
// Fallback to package.json when the build hasn't run yet.
|
|
246
|
+
}
|
|
247
|
+
rawName = rawName || pkg.name
|
|
248
|
+
version = version || pkg.version
|
|
249
|
+
if (!rawName || !version) return null
|
|
250
|
+
|
|
251
|
+
const uniwebNamespace = pkg.uniweb?.namespace
|
|
252
|
+
const pkgScopeMatch = (pkg.name || '').match(/^@([a-z0-9_-]+)\//)
|
|
253
|
+
const selfScopeMatch = rawName.match(/^@([a-z0-9_-]+)\//)
|
|
254
|
+
const namespace = uniwebNamespace || pkgScopeMatch?.[1] || selfScopeMatch?.[1]
|
|
255
|
+
if (!namespace) return null
|
|
256
|
+
|
|
257
|
+
const bareName = selfScopeMatch ? rawName.slice(selfScopeMatch[0].length) : rawName
|
|
258
|
+
return `@${namespace}/${bareName}@${version}`
|
|
259
|
+
}
|
|
260
|
+
|
|
101
261
|
// ─── Main ───────────────────────────────────────────────────
|
|
102
262
|
|
|
103
263
|
export async function deploy(args = []) {
|
|
@@ -111,6 +271,11 @@ export async function deploy(args = []) {
|
|
|
111
271
|
// to DB, so any change the user makes there ends up in site.yml after
|
|
112
272
|
// the loopback finalize roundtrip.
|
|
113
273
|
const forceReview = args.includes('--review')
|
|
274
|
+
// Phase 2 (deploy-ux-v4): when `foundation:` in site.yml points at a
|
|
275
|
+
// workspace-local file: ref, deploy auto-publishes the foundation when
|
|
276
|
+
// its `dist/publish.json` receipt is missing/stale. These flags opt out.
|
|
277
|
+
const autoPublishFoundation = !args.includes('--no-auto-publish')
|
|
278
|
+
const treatDirtyAsStale = !args.includes('--no-dirty-as-stale')
|
|
114
279
|
|
|
115
280
|
const siteDir = await resolveSiteDir(args)
|
|
116
281
|
const backendUrl = getBackendUrl()
|
|
@@ -120,13 +285,79 @@ export async function deploy(args = []) {
|
|
|
120
285
|
// site.id / site.handle from prior deploys.
|
|
121
286
|
const siteYmlPath = join(siteDir, 'site.yml')
|
|
122
287
|
const siteYml = await readSiteYml(siteYmlPath)
|
|
123
|
-
|
|
124
|
-
if (!foundation) {
|
|
288
|
+
if (!siteYml.foundation) {
|
|
125
289
|
say.err('site.yml is missing `foundation`.')
|
|
126
290
|
say.dim('Add a line like: foundation: \'@uniweb/docs-foundation@0.1.20\'')
|
|
127
291
|
process.exit(1)
|
|
128
292
|
}
|
|
129
293
|
|
|
294
|
+
// Foundation may be string or object form (see site.yml docs).
|
|
295
|
+
const fnd = parseSiteFoundation(siteYml.foundation)
|
|
296
|
+
if (fnd.error) {
|
|
297
|
+
say.err(`site.yml: ${fnd.error}`)
|
|
298
|
+
process.exit(1)
|
|
299
|
+
}
|
|
300
|
+
// `foundation` is the on-the-wire shape we forward to PHP authorize +
|
|
301
|
+
// Worker publish. PHP only inspects the namespace via the ref string;
|
|
302
|
+
// it doesn't care about policy/pinned, so the object form passes through.
|
|
303
|
+
// The Worker (publish.js::parseFoundationConfig) handles both shapes.
|
|
304
|
+
let foundation = fnd.normalized
|
|
305
|
+
if (fnd.policy && fnd.policy !== 'auto-patch') {
|
|
306
|
+
say.dim(`Foundation policy: ${fnd.policy}${fnd.pinned ? ' (pinned)' : ''}`)
|
|
307
|
+
} else if (fnd.pinned) {
|
|
308
|
+
say.dim('Foundation policy: exact (pinned)')
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Phase 2: resolve workspace-local `file:` foundation refs.
|
|
312
|
+
//
|
|
313
|
+
// The object form of `foundation:` already requires a registry ref
|
|
314
|
+
// (`@ns/name@ver`) per parseSiteFoundation, so only the string form can
|
|
315
|
+
// resolve to a local path. Pass-through cases (registry ref, full URL,
|
|
316
|
+
// npm package) all leave `foundation` untouched. The resolved registry
|
|
317
|
+
// ref is also passed to the site build via UNIWEB_FOUNDATION_REF so the
|
|
318
|
+
// build runs in runtime mode against the just-published artifact instead
|
|
319
|
+
// of bundling the local foundation source. site.yml on disk is never
|
|
320
|
+
// modified.
|
|
321
|
+
let foundationBuildOverride = null
|
|
322
|
+
if (typeof foundation === 'string') {
|
|
323
|
+
const detected = detectFoundationType(foundation, siteDir)
|
|
324
|
+
if (detected.type === 'local') {
|
|
325
|
+
const localPath = detected.path
|
|
326
|
+
const relPath = relative(siteDir, localPath) || localPath
|
|
327
|
+
|
|
328
|
+
const inspection = await inspectLocalFoundationReceipt(localPath, {
|
|
329
|
+
dirtyAsStale: treatDirtyAsStale,
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
if (inspection.stale && !autoPublishFoundation) {
|
|
333
|
+
say.err(`Local foundation at ${relPath} is stale: ${inspection.reason}.`)
|
|
334
|
+
say.dim(`Run \`${getCliPrefix()} publish\` from ${relPath}, or drop --no-auto-publish to let deploy publish it for you.`)
|
|
335
|
+
process.exit(1)
|
|
336
|
+
}
|
|
337
|
+
if (inspection.stale) {
|
|
338
|
+
say.info(`Foundation at ${relPath} is stale (${inspection.reason}). Auto-publishing…`)
|
|
339
|
+
console.log('')
|
|
340
|
+
try {
|
|
341
|
+
execSync('npx uniweb publish', { cwd: localPath, stdio: 'inherit' })
|
|
342
|
+
} catch {
|
|
343
|
+
say.err(`Auto-publish of foundation at ${relPath} failed. See output above.`)
|
|
344
|
+
process.exit(1)
|
|
345
|
+
}
|
|
346
|
+
console.log('')
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const resolved = await deriveLocalFoundationRef(localPath)
|
|
350
|
+
if (!resolved) {
|
|
351
|
+
say.err(`Could not derive a registry ref for foundation at ${relPath}.`)
|
|
352
|
+
say.dim(`Make sure its package.json has a name + version and a namespace (uniweb.namespace, scoped name, or run \`uniweb publish --namespace <handle>\` once).`)
|
|
353
|
+
process.exit(1)
|
|
354
|
+
}
|
|
355
|
+
say.dim(`Foundation: ${foundation} → ${resolved} (resolved from ${relPath})`)
|
|
356
|
+
foundation = resolved
|
|
357
|
+
foundationBuildOverride = resolved
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
130
361
|
// Runtime defaults to "latest" resolved at authorize time.
|
|
131
362
|
let runtimeVersion = siteYml.runtime
|
|
132
363
|
if (!runtimeVersion) {
|
|
@@ -160,9 +391,18 @@ export async function deploy(args = []) {
|
|
|
160
391
|
// link-mode URLs, which auto-enters runtime mode. Prerender also
|
|
161
392
|
// auto-skips for link-mode foundations (HTML is rendered on the
|
|
162
393
|
// serving edge, not here).
|
|
394
|
+
//
|
|
395
|
+
// For workspace-local foundations (Phase 2 resolution above),
|
|
396
|
+
// UNIWEB_FOUNDATION_REF tells defineSiteConfig to use the resolved
|
|
397
|
+
// registry ref instead of site.yml's literal value, so the build
|
|
398
|
+
// produces a runtime-mode bundle pointing at the just-published
|
|
399
|
+
// foundation rather than embedding the local source.
|
|
163
400
|
execSync('npx uniweb build', {
|
|
164
401
|
cwd: siteDir,
|
|
165
402
|
stdio: 'inherit',
|
|
403
|
+
env: foundationBuildOverride
|
|
404
|
+
? { ...process.env, UNIWEB_FOUNDATION_REF: foundationBuildOverride }
|
|
405
|
+
: process.env,
|
|
166
406
|
})
|
|
167
407
|
console.log('')
|
|
168
408
|
} else if (!existsSync(contentPath)) {
|
|
@@ -202,7 +442,7 @@ export async function deploy(args = []) {
|
|
|
202
442
|
say.info('Dry run — showing what would be deployed:')
|
|
203
443
|
say.dim(`Site dir : ${siteDir}`)
|
|
204
444
|
say.dim(`site.id : ${siteYml.site?.id || '(none — would use create flow)'}`)
|
|
205
|
-
say.dim(`Foundation : ${foundation}`)
|
|
445
|
+
say.dim(`Foundation : ${typeof foundation === 'string' ? foundation : foundation.ref}`)
|
|
206
446
|
say.dim(`Runtime : ${runtimeVersion}`)
|
|
207
447
|
say.dim(`Languages : ${languages.join(', ')}`)
|
|
208
448
|
say.dim(`Default locale : ${defaultLanguage}`)
|
|
@@ -397,6 +637,25 @@ export async function deploy(args = []) {
|
|
|
397
637
|
}
|
|
398
638
|
await callPublish({ url: publishUrl, token: publishToken, body: publishPayload })
|
|
399
639
|
|
|
640
|
+
// Local event memory — used by future re-deploys (e.g., to skip
|
|
641
|
+
// redundant work when nothing has changed). Lives under dist/ which is
|
|
642
|
+
// gitignored; the platform never reads it.
|
|
643
|
+
const foundationRef = typeof foundation === 'string' ? foundation : foundation?.ref
|
|
644
|
+
const { gitSha, gitDirty } = readGitState(siteDir)
|
|
645
|
+
const deployReceipt = {
|
|
646
|
+
schemaVersion: 1,
|
|
647
|
+
deployedFromGitSha: gitSha,
|
|
648
|
+
deployedFromGitDirty: gitDirty,
|
|
649
|
+
deployedAt: new Date().toISOString(),
|
|
650
|
+
url: handleResolved ? `https://${handleResolved}.uniweb.website/` : null,
|
|
651
|
+
foundation: {
|
|
652
|
+
ref: foundationRef,
|
|
653
|
+
url: composeFoundationUrl(foundationRef, getRegistryUrl()),
|
|
654
|
+
},
|
|
655
|
+
locales: languages,
|
|
656
|
+
}
|
|
657
|
+
await writeFile(join(distDir, 'deploy.json'), JSON.stringify(deployReceipt, null, 2) + '\n')
|
|
658
|
+
|
|
400
659
|
// Write site.id / site.handle / features back to site.yml so the file
|
|
401
660
|
// stays in sync with the live billing state. site.id and site.handle
|
|
402
661
|
// are written on first deploy and any time the server-side handle drifts.
|
package/src/commands/docs.js
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
import { existsSync } from 'node:fs'
|
|
24
24
|
import { readFile } from 'node:fs/promises'
|
|
25
25
|
import { resolve, join, dirname } from 'node:path'
|
|
26
|
-
import { generateDocs } from '@uniweb/build'
|
|
26
|
+
import { generateDocs, resolveFoundationSrcPath, classifyPackage } from '@uniweb/build'
|
|
27
27
|
import {
|
|
28
28
|
isWorkspaceRoot,
|
|
29
29
|
findFoundations,
|
|
@@ -160,7 +160,7 @@ ${colors.dim}Schema: https://raw.githubusercontent.com/uniweb/cli/main/schemas/p
|
|
|
160
160
|
const META_REFERENCE = `
|
|
161
161
|
${colors.cyan}${colors.bright}Component meta.js Reference${colors.reset}
|
|
162
162
|
|
|
163
|
-
Component metadata in foundation/
|
|
163
|
+
Component metadata in <foundation>/components/[Name]/meta.js (or sections/[Name]/meta.js)
|
|
164
164
|
|
|
165
165
|
${colors.bright}Identity:${colors.reset}
|
|
166
166
|
${colors.cyan}title${colors.reset} Display name in editor
|
|
@@ -314,16 +314,15 @@ ${colors.dim}Notes:${colors.reset}
|
|
|
314
314
|
* Detect if current directory is a foundation
|
|
315
315
|
*/
|
|
316
316
|
function isFoundation(dir) {
|
|
317
|
-
|
|
318
|
-
const componentsDir = join(srcDir, 'components')
|
|
319
|
-
return existsSync(componentsDir)
|
|
317
|
+
return classifyPackage(dir) === 'foundation'
|
|
320
318
|
}
|
|
321
319
|
|
|
322
320
|
/**
|
|
323
321
|
* Detect if current directory is a site
|
|
324
322
|
*/
|
|
325
323
|
function isSite(dir) {
|
|
326
|
-
|
|
324
|
+
// Use the canonical classifier; also accept legacy `site.yaml`.
|
|
325
|
+
return classifyPackage(dir) === 'site' || existsSync(join(dir, 'site.yaml'))
|
|
327
326
|
}
|
|
328
327
|
|
|
329
328
|
/**
|
package/src/commands/doctor.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
|
6
6
|
import { join, resolve, basename, dirname, relative } from 'node:path'
|
|
7
7
|
import yaml from 'js-yaml'
|
|
8
|
+
import { resolveFoundationSrcPath, classifyPackage, isExtensionPackage as buildIsExtensionPackage } from '@uniweb/build'
|
|
8
9
|
import { getCliVersion } from '../versions.js'
|
|
9
10
|
import { readAgentsVersion } from '../utils/agents-stamp.js'
|
|
10
11
|
|
|
@@ -29,36 +30,37 @@ const log = console.log
|
|
|
29
30
|
* Check if a directory is a site
|
|
30
31
|
*/
|
|
31
32
|
function isSite(dir) {
|
|
32
|
-
|
|
33
|
+
// Use the canonical classifier; also accept legacy `site.yaml`
|
|
34
|
+
// (the classifier only recognizes `.yml` and `.document.yml`).
|
|
35
|
+
return classifyPackage(dir) === 'site' || existsSync(join(dir, 'site.yaml'))
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
/**
|
|
36
39
|
* Check if a directory is a foundation
|
|
37
40
|
*/
|
|
38
41
|
function isFoundation(dir) {
|
|
39
|
-
|
|
40
|
-
if (existsSync(join(dir, 'src', 'foundation.js'))) return true
|
|
41
|
-
// Fallback: has src/sections/
|
|
42
|
-
if (existsSync(join(dir, 'src', 'sections'))) return true
|
|
43
|
-
// Legacy fallback: has src/components/
|
|
44
|
-
if (existsSync(join(dir, 'src', 'components'))) return true
|
|
45
|
-
return false
|
|
42
|
+
return classifyPackage(dir) === 'foundation'
|
|
46
43
|
}
|
|
47
44
|
|
|
48
45
|
/**
|
|
49
|
-
* Load foundation.js
|
|
50
|
-
*
|
|
46
|
+
* Load the foundation's authored declarations file (main.js or legacy
|
|
47
|
+
* foundation.js) and return a minimal summary used by the doctor.
|
|
48
|
+
* Returns null if no declarations file is found.
|
|
51
49
|
*/
|
|
52
50
|
function loadFoundationJs(dir) {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
51
|
+
const srcDir = resolveFoundationSrcPath(dir)
|
|
52
|
+
for (const name of ['main.js', 'foundation.js']) {
|
|
53
|
+
const filePath = join(srcDir, name)
|
|
54
|
+
if (existsSync(filePath)) {
|
|
55
|
+
try {
|
|
56
|
+
const content = readFileSync(filePath, 'utf8')
|
|
57
|
+
return { extension: /extension\s*:\s*true/.test(content) }
|
|
58
|
+
} catch {
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
}
|
|
61
62
|
}
|
|
63
|
+
return null
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
/**
|
|
@@ -78,10 +80,7 @@ function loadSchemaJson(dir) {
|
|
|
78
80
|
* Check if a foundation is an extension (via schema.json or foundation.js)
|
|
79
81
|
*/
|
|
80
82
|
function isExtensionPackage(dir) {
|
|
81
|
-
|
|
82
|
-
if (schema?._self?.role === 'extension') return true
|
|
83
|
-
const config = loadFoundationJs(dir)
|
|
84
|
-
return config?.extension === true
|
|
83
|
+
return buildIsExtensionPackage(dir)
|
|
85
84
|
}
|
|
86
85
|
|
|
87
86
|
/**
|