@elytracms/cli 0.0.87 → 0.0.90
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 +56 -37
- package/package.json +2 -2
- package/src/cli.js +81 -34
- package/src/config-harness.js +173 -3
- package/src/doctor.js +166 -24
- package/src/layout.js +288 -0
- package/src/previews.js +39 -22
- package/src/project.js +105 -48
- package/src/routes-check.js +18 -4
package/src/previews.js
CHANGED
|
@@ -3,6 +3,7 @@ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync
|
|
|
3
3
|
import { dirname, isAbsolute, join, relative, sep } from 'node:path'
|
|
4
4
|
import { pathToFileURL } from 'node:url'
|
|
5
5
|
import { CONFIG_FILE, pickPort, resolveFromDir, startConsumerVite, virtualModulesPlugin } from './config-harness.js'
|
|
6
|
+
import { appDisplay, resolveProjectLayout } from './layout.js'
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* `elytra previews` (EC-440) — build-time fixture-preview generation.
|
|
@@ -561,9 +562,10 @@ export function previewsDoctorChecks(artifacts) {
|
|
|
561
562
|
* The engine version for the input hash — read from the `@elytracms/studio`
|
|
562
563
|
* (fallback `@elytracms/core`) package the CONSUMER has installed, never
|
|
563
564
|
* hardcoded. Direct node_modules paths (pnpm keeps symlinks for direct deps).
|
|
565
|
+
* `studioDir` is the resolved studio app (EC-603 layout).
|
|
564
566
|
*/
|
|
565
|
-
export function resolveEngineVersion(root) {
|
|
566
|
-
const bases = [
|
|
567
|
+
export function resolveEngineVersion(root, studioDir) {
|
|
568
|
+
const bases = [studioDir, root]
|
|
567
569
|
for (const pkg of ['@elytracms/studio', '@elytracms/core']) {
|
|
568
570
|
for (const base of bases) {
|
|
569
571
|
const path = join(base, 'node_modules', ...pkg.split('/'), 'package.json')
|
|
@@ -581,16 +583,26 @@ export function resolveEngineVersion(root) {
|
|
|
581
583
|
|
|
582
584
|
/**
|
|
583
585
|
* The consumer theme entry the harness page imports: the studio's canvas
|
|
584
|
-
* stylesheet (
|
|
585
|
-
* theme plus any engine-required `@source` lines
|
|
586
|
-
*
|
|
587
|
-
*
|
|
586
|
+
* stylesheet (`<studio>/src/styles/canvas.css` — the EC-414 shape: the site
|
|
587
|
+
* theme plus any engine-required `@source` lines; the stylesheet the canvas
|
|
588
|
+
* iframe really links) when present, else the theme itself — `package.json#elytra
|
|
589
|
+
* .theme`, default `components/theme.css` (EC-603: the project's declaration, never
|
|
590
|
+
* a fixed path). `null` → no theme, the command refuses (an unstyled preview would
|
|
591
|
+
* be a lying artifact).
|
|
592
|
+
*
|
|
593
|
+
* The canvas stylesheet stays first so a conventional project's recorded render
|
|
594
|
+
* dependencies (its committed manifest) are byte-for-byte unchanged by EC-603. It is
|
|
595
|
+
* looked up relative to the RESOLVED studio app; a project without one falls back to
|
|
596
|
+
* the declared theme, so it is a preference, never a requirement.
|
|
588
597
|
*/
|
|
589
|
-
export function themeStylesheetPath(root) {
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
598
|
+
export function themeStylesheetPath(root, layout = resolveProjectLayout(root)) {
|
|
599
|
+
const studio = layout.entries.studio
|
|
600
|
+
const theme = layout.entries.theme
|
|
601
|
+
const candidates = [
|
|
602
|
+
...(studio.path ? [join(studio.path, 'src', 'styles', 'canvas.css')] : []),
|
|
603
|
+
...(theme.path ? [theme.path] : []),
|
|
604
|
+
]
|
|
605
|
+
for (const candidate of candidates) {
|
|
594
606
|
if (existsSync(candidate)) return candidate
|
|
595
607
|
}
|
|
596
608
|
return null
|
|
@@ -652,7 +664,7 @@ function harnessPlugins({ configPath, themePath }) {
|
|
|
652
664
|
}
|
|
653
665
|
|
|
654
666
|
/** Boot the shared consumer-Vite harness with the previews page + modules. */
|
|
655
|
-
async function startHarnessServer({ root, studioDir, port }) {
|
|
667
|
+
async function startHarnessServer({ root, studioDir, layout, port }) {
|
|
656
668
|
// Resolve vite first so a missing toolchain names vite (the same failure
|
|
657
669
|
// order as before the boot moved to the shared config-harness).
|
|
658
670
|
resolveFromDir(studioDir, 'vite')
|
|
@@ -660,11 +672,13 @@ async function startHarnessServer({ root, studioDir, port }) {
|
|
|
660
672
|
const tailwind = (await import(pathToFileURL(tailwindEntry).href)).default
|
|
661
673
|
|
|
662
674
|
const configPath = join(root, 'elytra.config.ts')
|
|
663
|
-
const themePath = themeStylesheetPath(root)
|
|
675
|
+
const themePath = themeStylesheetPath(root, layout)
|
|
664
676
|
if (!themePath) {
|
|
677
|
+
const studio = appDisplay(layout, 'studio')
|
|
678
|
+
const theme = appDisplay(layout, 'theme')
|
|
665
679
|
throw new Error(
|
|
666
|
-
|
|
667
|
-
'previews must render against the consumer theme.',
|
|
680
|
+
`No theme stylesheet found (looked for ${studio}/src/styles/canvas.css and ${theme} — ` +
|
|
681
|
+
'declare yours as package.json#elytra.theme) — previews must render against the consumer theme.',
|
|
668
682
|
)
|
|
669
683
|
}
|
|
670
684
|
|
|
@@ -708,8 +722,8 @@ const PLAYWRIGHT_INSTALL_HELP = [
|
|
|
708
722
|
].join('\n')
|
|
709
723
|
|
|
710
724
|
/** Dynamic, on-demand Playwright load from the consumer repo. `null` = not installed. */
|
|
711
|
-
async function loadPlaywright(root) {
|
|
712
|
-
for (const base of [
|
|
725
|
+
async function loadPlaywright(root, studioDir) {
|
|
726
|
+
for (const base of [studioDir, root]) {
|
|
713
727
|
let resolved
|
|
714
728
|
try {
|
|
715
729
|
resolved = resolveFromDir(base, 'playwright')
|
|
@@ -795,8 +809,11 @@ async function captureViewport(page, cdp) {
|
|
|
795
809
|
*/
|
|
796
810
|
export async function runPreviews(project, { check = false, port, log = (l) => console.log(l) } = {}) {
|
|
797
811
|
const root = project.root
|
|
798
|
-
|
|
799
|
-
const
|
|
812
|
+
// EC-603: the studio app and theme where the project's layout says they are.
|
|
813
|
+
const layout = project.layout ?? resolveProjectLayout(root)
|
|
814
|
+
const studioDir = layout.entries.studio.path
|
|
815
|
+
const studioLabel = layout.entries.studio.display
|
|
816
|
+
const engine = resolveEngineVersion(root, studioDir)
|
|
800
817
|
const manifestPath = join(studioDir, PREVIEWS_MANIFEST_FILE)
|
|
801
818
|
const imagesDir = join(studioDir, PREVIEWS_DIR)
|
|
802
819
|
|
|
@@ -807,7 +824,7 @@ export async function runPreviews(project, { check = false, port, log = (l) => c
|
|
|
807
824
|
const chosenPort = port ? Number(port) : await pickPort()
|
|
808
825
|
let harness
|
|
809
826
|
try {
|
|
810
|
-
harness = await startHarnessServer({ root, studioDir, port: chosenPort })
|
|
827
|
+
harness = await startHarnessServer({ root, studioDir, layout, port: chosenPort })
|
|
811
828
|
} catch (error) {
|
|
812
829
|
log(`Could not start the render harness: ${error.message}`)
|
|
813
830
|
return 1
|
|
@@ -863,7 +880,7 @@ export async function runPreviews(project, { check = false, port, log = (l) => c
|
|
|
863
880
|
|
|
864
881
|
const failures = []
|
|
865
882
|
if (toRender.length > 0) {
|
|
866
|
-
const playwright = await loadPlaywright(root)
|
|
883
|
+
const playwright = await loadPlaywright(root, studioDir)
|
|
867
884
|
if (!playwright) {
|
|
868
885
|
log(PLAYWRIGHT_INSTALL_HELP)
|
|
869
886
|
return 1
|
|
@@ -933,7 +950,7 @@ export async function runPreviews(project, { check = false, port, log = (l) => c
|
|
|
933
950
|
log(
|
|
934
951
|
`Done: ${rendered} rendered, ${jobs.length - toRender.length} unchanged, ` +
|
|
935
952
|
`${orphans.length} pruned, manifest ${manifestChanged ? 'written' : 'unchanged'} ` +
|
|
936
|
-
`(
|
|
953
|
+
`(${studioLabel}/${PREVIEWS_MANIFEST_FILE}).`,
|
|
937
954
|
)
|
|
938
955
|
if (failures.length > 0) {
|
|
939
956
|
log(`✗ ${failures.length} fixture(s) failed to render — their entries were left out of the manifest.`)
|
package/src/project.js
CHANGED
|
@@ -1,20 +1,36 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from 'node:fs'
|
|
2
2
|
import { dirname, join } from 'node:path'
|
|
3
|
+
import { parseBackendMode } from '@elytracms/core/export-sync/deployment-ready'
|
|
4
|
+
import { appDisplay, resolveProjectLayout } from './layout.js'
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
|
-
* Project detection for the Elytra CLI (EC-235).
|
|
6
|
-
* executing:
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
7
|
+
* Project detection for the Elytra CLI (EC-235). Declaration-aware, not config-
|
|
8
|
+
* executing: the repo root is the directory holding `elytra.config.ts`; the LAYOUT —
|
|
9
|
+
* where the studio app, the frontend app and the theme live — is declared in the
|
|
10
|
+
* `package.json` beside it (`"elytra": { studio, frontend, theme }`, EC-603), every key
|
|
11
|
+
* defaulting to the scaffold layout (`./studio`, `./frontend`,
|
|
12
|
+
* `./components/theme.css`) — see `layout.js`. The backend mode comes from the studio
|
|
13
|
+
* app's env files, judged by the ONE parser the studio itself runs (core's
|
|
14
|
+
* `parseBackendMode`, EC-603).
|
|
15
|
+
*
|
|
16
|
+
* Detection deliberately does NOT load the config: it must work before `pnpm install`
|
|
17
|
+
* of the consuming repo (the CLI's own dependency, `@elytracms/core`, is installed WITH
|
|
18
|
+
* the CLI — this module imports exactly two of its built files, the bundler-free
|
|
19
|
+
* `export-sync/deployment-ready` and the parser it re-exports), and the operative
|
|
20
|
+
* backend signal lives in env regardless. (Commands that need the config's CONTENTS
|
|
21
|
+
* execute it through the consumer's own Vite instead — `config-harness.js`,
|
|
22
|
+
* EC-440/EC-495 — and degrade honestly when that is unavailable. Node-native TS is not
|
|
23
|
+
* an option: real configs import `.tsx` barrels, EC-474.)
|
|
15
24
|
*/
|
|
16
25
|
|
|
17
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Walk up from `start` until a directory containing `elytra.config.ts` is found.
|
|
28
|
+
*
|
|
29
|
+
* EC-603 kept this key: the config is the one file Elytra requires of a repo and it
|
|
30
|
+
* stays at the root, while the layout section is OPTIONAL (defaults declare nothing) —
|
|
31
|
+
* so a `package.json` can never mark the root on its own, and the studio/frontend apps'
|
|
32
|
+
* own `package.json`s sit on the walk up from inside them.
|
|
33
|
+
*/
|
|
18
34
|
export function findRepoRoot(start) {
|
|
19
35
|
let dir = start
|
|
20
36
|
for (;;) {
|
|
@@ -25,7 +41,14 @@ export function findRepoRoot(start) {
|
|
|
25
41
|
}
|
|
26
42
|
}
|
|
27
43
|
|
|
28
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Parse a dotenv file into a flat record. Missing file → {}.
|
|
46
|
+
*
|
|
47
|
+
* Deliberately small, and NOT Vite's dotenv: no variable expansion, and an inline
|
|
48
|
+
* ` #…` is stripped even inside quotes. Values the backend parser reads are trimmed by
|
|
49
|
+
* the parser itself, which covers the measured P21 case (quoted padding); the remaining
|
|
50
|
+
* dotenv-dialect differences are recorded in EC-603's Done section.
|
|
51
|
+
*/
|
|
29
52
|
function readEnvFile(path) {
|
|
30
53
|
if (!existsSync(path)) return {}
|
|
31
54
|
const out = {}
|
|
@@ -43,26 +66,51 @@ function readEnvFile(path) {
|
|
|
43
66
|
return out
|
|
44
67
|
}
|
|
45
68
|
|
|
46
|
-
/**
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
69
|
+
/**
|
|
70
|
+
* The env files Vite loads for `mode`, lowest precedence first — the studio's own
|
|
71
|
+
* toolchain's file set (`vite dev` = `development`, `vite build` = `production`), so the
|
|
72
|
+
* CLI reads exactly the files the studio it starts will read (EC-603: a mode set only in
|
|
73
|
+
* `.env.development.local` used to be invisible here).
|
|
74
|
+
*/
|
|
75
|
+
export function viteEnvFiles(mode) {
|
|
76
|
+
return ['.env', '.env.local', `.env.${mode}`, `.env.${mode}.local`]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Merge an app's env files (Vite's order: later wins), then process.env over them. */
|
|
80
|
+
function resolveEnv(appDir, processEnv, viteMode) {
|
|
81
|
+
let merged = {}
|
|
82
|
+
if (appDir) {
|
|
83
|
+
for (const file of viteEnvFiles(viteMode)) merged = { ...merged, ...readEnvFile(join(appDir, file)) }
|
|
52
84
|
}
|
|
85
|
+
return { ...merged, ...processEnv }
|
|
53
86
|
}
|
|
54
87
|
|
|
55
88
|
/**
|
|
56
|
-
* Resolve the studio app's effective env (
|
|
57
|
-
* the same signal the studio itself reads. Used by `elytra doctor`
|
|
58
|
-
* deployment to diagnose.
|
|
89
|
+
* Resolve the studio app's effective env (its env files for `viteMode` ← process.env) —
|
|
90
|
+
* the same signal the studio itself reads. Used by `elytra doctor` / `elytra init` to
|
|
91
|
+
* find the Convex deployment to diagnose. `rootOrProject` is the repo root (the layout
|
|
92
|
+
* is resolved from it) or an {@link inspectProject} result (its resolved layout is
|
|
93
|
+
* used). Exported so the doctor and project inspection share one reader.
|
|
59
94
|
*/
|
|
60
|
-
export function resolveStudioEnv(
|
|
61
|
-
|
|
95
|
+
export function resolveStudioEnv(rootOrProject, processEnv = {}, { viteMode = 'development' } = {}) {
|
|
96
|
+
const layout =
|
|
97
|
+
typeof rootOrProject === 'string' ? resolveProjectLayout(rootOrProject) : rootOrProject.layout
|
|
98
|
+
const studio = layout.entries.studio
|
|
99
|
+
return resolveEnv(studio.found ? studio.path : null, processEnv, viteMode)
|
|
62
100
|
}
|
|
63
101
|
|
|
64
|
-
|
|
65
|
-
|
|
102
|
+
/**
|
|
103
|
+
* The CLI's vocabulary for the ONE parser's verdict (EC-603) — the single place the two
|
|
104
|
+
* vocabularies meet. The CLI keeps `connected` / `unconfigured` (its user-facing words:
|
|
105
|
+
* "connected mode", "Backend not configured") over the studio's `convex` /
|
|
106
|
+
* `misconfigured`; the mapping is 1:1, and an `unconfigured` project carries the
|
|
107
|
+
* parser's verdict (`project.backend`) so its notes can say WHY — a missing URL, or an
|
|
108
|
+
* unknown mode the studio will refuse to boot with.
|
|
109
|
+
*/
|
|
110
|
+
export function cliModeOf(backend) {
|
|
111
|
+
if (backend.kind === 'convex') return 'connected'
|
|
112
|
+
if (backend.kind === 'playground') return 'playground'
|
|
113
|
+
return 'unconfigured'
|
|
66
114
|
}
|
|
67
115
|
|
|
68
116
|
/**
|
|
@@ -113,21 +161,29 @@ export function resolveDevPortOffset(env = {}) {
|
|
|
113
161
|
|
|
114
162
|
/**
|
|
115
163
|
* Inspect a consuming repo. Returns:
|
|
116
|
-
* { root, apps: {studio, frontend}, mode, convexProvisioned,
|
|
117
|
-
*
|
|
118
|
-
* `
|
|
164
|
+
* { root, layout, apps: {studio, frontend}, backend, mode, convexProvisioned,
|
|
165
|
+
* isProdDeployment, hasDeployKey, devPortOffset, issues }
|
|
166
|
+
* `layout` is the resolved `package.json#elytra` layout (`layout.js`); `backend` is the
|
|
167
|
+
* shared parser's verdict and `mode` its CLI name ({@link cliModeOf}). `issues` are
|
|
168
|
+
* actionable, human-readable prerequisite problems (empty = ok) — an invalid layout
|
|
169
|
+
* section or a declared path that does not exist is one of them, so no command runs
|
|
170
|
+
* against a layout the project did not mean.
|
|
171
|
+
*
|
|
172
|
+
* `viteMode` selects the studio env files read (Vite's set for that mode): `elytra build`
|
|
173
|
+
* runs `vite build`, so it inspects `production`; everything else `development`.
|
|
119
174
|
*/
|
|
120
|
-
export function inspectProject(cwd, processEnv = {}) {
|
|
175
|
+
export function inspectProject(cwd, processEnv = {}, { viteMode = 'development' } = {}) {
|
|
121
176
|
const root = findRepoRoot(cwd)
|
|
122
177
|
if (!root) {
|
|
123
178
|
return {
|
|
124
179
|
root: null,
|
|
180
|
+
layout: null,
|
|
125
181
|
apps: { studio: false, frontend: false },
|
|
182
|
+
backend: null,
|
|
126
183
|
mode: 'unknown',
|
|
127
184
|
convexProvisioned: false,
|
|
128
185
|
isProdDeployment: false,
|
|
129
186
|
hasDeployKey: false,
|
|
130
|
-
hasAiRoute: false,
|
|
131
187
|
devPortOffset: 0,
|
|
132
188
|
issues: [
|
|
133
189
|
'No elytra.config.ts found in this directory or any parent. Run `elytra` from inside an Elytra project (or scaffold one with `npm create elytra@latest`).',
|
|
@@ -135,23 +191,27 @@ export function inspectProject(cwd, processEnv = {}) {
|
|
|
135
191
|
}
|
|
136
192
|
}
|
|
137
193
|
|
|
138
|
-
const
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
194
|
+
const layout = resolveProjectLayout(root)
|
|
195
|
+
const apps = { studio: layout.entries.studio.found, frontend: layout.entries.frontend.found }
|
|
196
|
+
const issues = layout.issues.map((i) => i.message)
|
|
197
|
+
if (layout.issues.length === 0 && !apps.studio && !apps.frontend) {
|
|
198
|
+
issues.push(
|
|
199
|
+
`No studio or frontend app found (looked for ${appDisplay(layout, 'studio')}/package.json and ` +
|
|
200
|
+
`${appDisplay(layout, 'frontend')}/package.json` +
|
|
201
|
+
(layout.source === 'defaults' ? ' — the scaffold layout; declare yours in package.json#elytra).' : ').'),
|
|
202
|
+
)
|
|
142
203
|
}
|
|
143
204
|
if (!existsSync(join(root, 'node_modules'))) {
|
|
144
205
|
issues.push('Dependencies are not installed. Run `pnpm install` first.')
|
|
145
206
|
}
|
|
146
207
|
|
|
147
|
-
// Backend mode is read from the studio app's env
|
|
148
|
-
//
|
|
149
|
-
// means connected; neither means it still
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
else mode = 'unconfigured'
|
|
208
|
+
// Backend mode is read from the studio app's env through the ONE parser the studio
|
|
209
|
+
// itself runs (EC-603): explicit playground flag wins; an unknown mode is a visible
|
|
210
|
+
// misconfiguration; otherwise a Convex URL means connected; neither means it still
|
|
211
|
+
// needs configuring.
|
|
212
|
+
const studioEnv = resolveEnv(apps.studio ? layout.entries.studio.path : null, processEnv, viteMode)
|
|
213
|
+
const backend = parseBackendMode(studioEnv)
|
|
214
|
+
const mode = cliModeOf(backend)
|
|
155
215
|
|
|
156
216
|
// A Convex dev/deploy step only makes sense once a deployment is provisioned
|
|
157
217
|
// (`npx convex dev` writes CONVEX_DEPLOYMENT). Otherwise we skip it and say so.
|
|
@@ -165,14 +225,11 @@ export function inspectProject(cwd, processEnv = {}) {
|
|
|
165
225
|
// so `elytra dev` and the bare `elytra build` are unchanged by its presence.
|
|
166
226
|
const hasDeployKey = Boolean(studioEnv.CONVEX_DEPLOY_KEY)
|
|
167
227
|
|
|
168
|
-
//
|
|
169
|
-
//
|
|
170
|
-
// scaffold mounts the handler at `studio/src/server/ai-handler.ts` (the
|
|
171
|
-
// `createAiDevPlugin` default path); we never execute config to find out.
|
|
172
|
-
const hasAiRoute = apps.studio && existsSync(join(root, 'studio', 'src', 'server', 'ai-handler.ts'))
|
|
228
|
+
// EC-603: the AI route is no longer guessed from a fixed file path here — `elytra
|
|
229
|
+
// doctor` reads it from the studio's resolved Vite config (`config-harness.js`).
|
|
173
230
|
|
|
174
231
|
// EC-464: the one dev-port lever, read from the same studio env as everything else here.
|
|
175
232
|
const devPortOffset = resolveDevPortOffset(studioEnv)
|
|
176
233
|
|
|
177
|
-
return { root, apps, mode, convexProvisioned, isProdDeployment, hasDeployKey,
|
|
234
|
+
return { root, layout, apps, backend, mode, convexProvisioned, isProdDeployment, hasDeployKey, devPortOffset, issues }
|
|
178
235
|
}
|
package/src/routes-check.js
CHANGED
|
@@ -50,8 +50,10 @@ import {
|
|
|
50
50
|
* `elytra.config.ts` passes straight through. (`elytra.config.ts` itself is NOT
|
|
51
51
|
* loadable this way — it imports `.tsx` component barrels, which no Node version
|
|
52
52
|
* executes, EC-474. Where the consumer's deps ARE installed, EC-495's Vite harness
|
|
53
|
-
* executes the full config and `runDoctor` hands the collections it resolved
|
|
54
|
-
* checks in place of the `cms/index.ts` re-export;
|
|
53
|
+
* executes the full config and `runDoctor` hands the ROUTES and collections it resolved
|
|
54
|
+
* to these checks in place of the `cms/index.ts` re-export — EC-603; before, only the
|
|
55
|
+
* collections were swapped in. This loader stays the floor, and the first row says
|
|
56
|
+
* which of the two was judged.)
|
|
55
57
|
*
|
|
56
58
|
* Two things stand in the way, and both are handled rather than assumed away:
|
|
57
59
|
*
|
|
@@ -311,6 +313,16 @@ const MOUNT_SHADOW_FIX =
|
|
|
311
313
|
const UNREACHABLE_FIX =
|
|
312
314
|
'Give an all-dynamic route a static segment (e.g. "/blog/:slug"), mount only collections with a `parent` self-relation, and keep a mount prefix static — in cms/routes.ts.'
|
|
313
315
|
|
|
316
|
+
/**
|
|
317
|
+
* Where the judged route set came from (EC-603, RESIDUE-4), appended to the first row so
|
|
318
|
+
* the report says which reader it used: the executed config (what the studio and
|
|
319
|
+
* delivery serve) or the bundler-free `cms/index.ts` floor. No `source` → no note.
|
|
320
|
+
*/
|
|
321
|
+
const ROUTE_SOURCE_NOTE = {
|
|
322
|
+
config: ' (Routes from the executed elytra.config.ts.)',
|
|
323
|
+
native: ` (Routes read natively from ${CMS_ENTRY} — elytra.config.ts was not resolved here.)`,
|
|
324
|
+
}
|
|
325
|
+
|
|
314
326
|
/** An `unknown` check row carrying the reason the routes could not be read. */
|
|
315
327
|
function unavailableCheck(id, facts) {
|
|
316
328
|
return {
|
|
@@ -360,6 +372,7 @@ export function routesDoctorChecks(facts) {
|
|
|
360
372
|
const count = facts.routeCount ?? facts.routes.length
|
|
361
373
|
const plural = count === 1 ? '' : 's'
|
|
362
374
|
const conflicts = routeIssuesOf(facts.routes).filter((issue) => issue.code === 'route-conflict')
|
|
375
|
+
const source = ROUTE_SOURCE_NOTE[facts.source] ?? ''
|
|
363
376
|
|
|
364
377
|
const routeConflicts =
|
|
365
378
|
conflicts.length === 0
|
|
@@ -367,7 +380,7 @@ export function routesDoctorChecks(facts) {
|
|
|
367
380
|
id: 'route-conflicts',
|
|
368
381
|
bucket: 'app',
|
|
369
382
|
status: 'ok',
|
|
370
|
-
detail: `${count} declared route${plural}, no URL conflicts
|
|
383
|
+
detail: `${count} declared route${plural}, no URL conflicts.${source}`,
|
|
371
384
|
}
|
|
372
385
|
: {
|
|
373
386
|
id: 'route-conflicts',
|
|
@@ -375,7 +388,8 @@ export function routesDoctorChecks(facts) {
|
|
|
375
388
|
status: 'fail',
|
|
376
389
|
detail:
|
|
377
390
|
`${conflicts.length} route conflict(s) among ${count} declared route${plural}. ` +
|
|
378
|
-
conflicts.map((issue) => issue.message).join(' ')
|
|
391
|
+
conflicts.map((issue) => issue.message).join(' ') +
|
|
392
|
+
source,
|
|
379
393
|
fix: CONFLICT_FIX,
|
|
380
394
|
}
|
|
381
395
|
|