@astrale-os/cli 1.0.0-beta.1 → 1.0.0-beta.11
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 +23 -12
- package/dist/astrale.js +48048 -56798
- package/dist/public/connect-core.js +2011 -30765
- package/dist/public/keys/index.js +884 -29483
- package/dist/public/paths/index.js +13101 -41459
- package/dist/types/admin/binding.d.ts +19 -0
- package/dist/types/admin/instance/client.d.ts +22 -0
- package/dist/types/admin/instance/index.d.ts +2 -0
- package/dist/types/admin/instance/model.d.ts +5 -12
- package/dist/types/connection/auth.d.ts +3 -0
- package/dist/types/connection/call.d.ts +3 -0
- package/dist/types/connection/command.d.ts +27 -0
- package/dist/types/connection/credential.d.ts +14 -0
- package/dist/types/connection/errors.d.ts +1 -0
- package/dist/types/connection/exchange.d.ts +9 -0
- package/dist/types/connection/failure/classify.d.ts +2 -0
- package/dist/types/connection/failure/index.d.ts +5 -0
- package/dist/types/connection/failure/model.d.ts +25 -0
- package/dist/types/connection/failure/render.d.ts +3 -0
- package/dist/types/connection/index.d.ts +8 -0
- package/dist/types/connection/reasons.d.ts +37 -0
- package/dist/types/connection/self.d.ts +37 -0
- package/dist/types/connection/session.d.ts +31 -0
- package/dist/types/connection/target.d.ts +38 -0
- package/dist/types/errors.d.ts +1 -1
- package/dist/types/lib/admin-instance.d.ts +17 -0
- package/dist/types/lib/admin-target.d.ts +11 -36
- package/dist/types/lib/config.d.ts +7 -59
- package/dist/types/lib/failure-debug.d.ts +1 -0
- package/dist/types/lib/idp.d.ts +26 -168
- package/dist/types/lib/instance-target.d.ts +1 -1
- package/dist/types/lib/instance.d.ts +30 -88
- package/dist/types/lib/log.d.ts +5 -3
- package/dist/types/lib/meta.d.ts +2 -2
- package/dist/types/lib/proc.d.ts +43 -0
- package/dist/types/lib/self.d.ts +9 -0
- package/dist/types/lib/update.d.ts +91 -0
- package/dist/types/lib/validation.d.ts +4 -1
- package/package.json +16 -19
- package/src/__tests__/fixtures/publication.ts +20 -0
- package/src/admin/__tests__/binding.test.ts +31 -0
- package/src/admin/__tests__/fixture.ts +101 -0
- package/src/admin/binding.ts +98 -0
- package/src/admin/catalog/.spec/api.d.ts +2 -2
- package/src/admin/catalog/__tests__/client.test.ts +20 -49
- package/src/admin/catalog/client.ts +36 -32
- package/src/admin/catalog/model.ts +3 -4
- package/src/admin/graph/.spec/api.d.ts +4 -10
- package/src/admin/graph/nodes.ts +1 -1
- package/src/admin/instance/.spec/api.d.ts +2 -2
- package/src/admin/instance/__tests__/client.test.ts +39 -164
- package/src/admin/instance/client.ts +35 -124
- package/src/admin/instance/index.ts +1 -2
- package/src/admin/instance/model.ts +11 -20
- package/src/commands/__tests__/admin-instance.test.ts +11 -8
- package/src/commands/__tests__/auth-login.test.ts +54 -1
- package/src/commands/__tests__/call-describe.test.ts +46 -33
- package/src/commands/__tests__/call.test.ts +5 -1
- package/src/commands/__tests__/domain-install-operation.test.ts +12 -7
- package/src/commands/__tests__/domain-install-owned.test.ts +6 -2
- package/src/commands/__tests__/domain-list.test.ts +63 -4
- package/src/commands/__tests__/install-identity-override.test.ts +19 -19
- package/src/commands/__tests__/instance-list-rows.test.ts +1 -1
- package/src/commands/__tests__/instance-status.test.ts +139 -0
- package/src/commands/__tests__/read-commands.test.ts +35 -10
- package/src/commands/__tests__/studio-descriptor.test.ts +22 -0
- package/src/commands/__tests__/update.test.ts +48 -19
- package/src/commands/call-describe.ts +60 -68
- package/src/commands/call.ts +43 -56
- package/src/commands/domain/install.ts +13 -17
- package/src/commands/domain/list.ts +18 -13
- package/src/commands/domain/publish.ts +14 -11
- package/src/commands/domain/uninstall.ts +2 -2
- package/src/commands/get.ts +14 -15
- package/src/commands/identity/__tests__/register.test.ts +3 -3
- package/src/commands/identity/register.ts +5 -4
- package/src/commands/instance/create.ts +8 -16
- package/src/commands/instance/delete.ts +12 -9
- package/src/commands/instance/list.ts +2 -2
- package/src/commands/instance/status.ts +66 -20
- package/src/commands/instance/use.ts +16 -12
- package/src/commands/introspect.ts +9 -16
- package/src/commands/logs.ts +24 -25
- package/src/commands/mutate.ts +6 -5
- package/src/commands/query.ts +27 -30
- package/src/commands/studio.ts +20 -0
- package/src/commands/token.ts +8 -7
- package/src/commands/update.ts +38 -14
- package/src/commands/view.ts +7 -6
- package/src/connection/.spec/api.d.ts +7 -8
- package/src/connection/.spec/architecture.md +5 -4
- package/src/connection/.spec/flows/session.ts +2 -2
- package/src/connection/.spec/laws/connection.ts +6 -2
- package/src/connection/.spec/layout.ts +1 -0
- package/src/connection/__tests__/credential.test.ts +20 -1
- package/src/connection/__tests__/errors.test.ts +43 -32
- package/src/connection/__tests__/exchange.test.ts +36 -6
- package/src/connection/__tests__/failure-fixtures.ts +39 -0
- package/src/connection/__tests__/failure-safety.test.ts +75 -0
- package/src/connection/__tests__/self.test.ts +31 -11
- package/src/connection/__tests__/session.test.ts +3 -3
- package/src/connection/__tests__/target.test.ts +1 -0
- package/src/connection/auth.ts +13 -0
- package/src/connection/call.ts +1 -1
- package/src/connection/credential.ts +2 -2
- package/src/connection/errors.ts +1 -355
- package/src/connection/exchange.ts +52 -27
- package/src/connection/failure/classify.ts +110 -0
- package/src/connection/failure/index.ts +18 -0
- package/src/connection/failure/model.ts +28 -0
- package/src/connection/failure/render.ts +116 -0
- package/src/connection/reasons.ts +16 -0
- package/src/connection/self.ts +39 -21
- package/src/connection/session.ts +19 -8
- package/src/errors.ts +3 -2
- package/src/graph/.spec/api.d.ts +12 -5
- package/src/graph/.spec/layout.ts +9 -1
- package/src/graph/__tests__/mutation.test.ts +2 -2
- package/src/graph/__tests__/query.test.ts +25 -6
- package/src/graph/class.ts +30 -0
- package/src/graph/index.ts +1 -0
- package/src/graph/query.ts +21 -32
- package/src/identity/.spec/api.d.ts +2 -2
- package/src/identity/__tests__/registration.test.ts +1 -1
- package/src/identity/registration.ts +3 -3
- package/src/keys/__tests__/keys.test.ts +4 -0
- package/src/keys/credential.ts +1 -0
- package/src/lib/__tests__/admin-target.test.ts +12 -0
- package/src/lib/__tests__/command-dx.test.ts +1 -1
- package/src/lib/__tests__/domain-publication.test.ts +138 -0
- package/src/lib/__tests__/idp-session.test.ts +22 -0
- package/src/lib/__tests__/idp.test.ts +36 -4
- package/src/lib/__tests__/instance-candidates.test.ts +1 -3
- package/src/lib/__tests__/instance-target.test.ts +7 -39
- package/src/lib/__tests__/instance.test.ts +6 -0
- package/src/lib/__tests__/log-errors.test.ts +64 -0
- package/src/lib/__tests__/studio-server-deps.test.ts +53 -16
- package/src/lib/__tests__/update.test.ts +113 -8
- package/src/lib/__tests__/view-open-intent.test.ts +0 -2
- package/src/lib/__tests__/view-server.test.ts +18 -3
- package/src/lib/__tests__/view-session.test.ts +1 -1
- package/src/lib/admin-instance.ts +14 -8
- package/src/lib/admin-target.ts +21 -2
- package/src/lib/command-dx.ts +9 -9
- package/src/lib/domain-publication.ts +104 -0
- package/src/lib/failure-debug.ts +26 -0
- package/src/lib/idp.ts +39 -4
- package/src/lib/instance-candidates.ts +2 -2
- package/src/lib/instance-target.ts +3 -14
- package/src/lib/instance.ts +22 -12
- package/src/lib/log.ts +16 -9
- package/src/lib/login-flow.ts +13 -3
- package/src/lib/meta.ts +2 -2
- package/src/lib/provision-instance.ts +34 -57
- package/src/lib/update.ts +124 -30
- package/src/lib/validation.ts +2 -2
- package/src/lib/view/resolve.ts +1 -1
- package/src/lib/view/server.ts +17 -3
- package/src/program/__tests__/program.test.ts +20 -4
- package/src/program/build.ts +2 -2
- package/src/setup/__tests__/instance-step.test.ts +27 -10
- package/src/setup/steps/instance.ts +22 -15
- package/studio/client/dist/assets/elk-api-DjmKsHXc.js +1 -0
- package/studio/client/dist/assets/index-7YHdeCxp.js +81 -0
- package/studio/client/dist/assets/index-C0FAnwNw.css +2 -0
- package/studio/client/dist/assets/rolldown-runtime-Dd_uD5pT.js +1 -0
- package/studio/client/dist/assets/schema-studio-BilUNb6Z.css +1 -0
- package/studio/client/dist/assets/schema-studio-CABpTfvH.js +8 -0
- package/studio/client/dist/index.html +3 -2
- package/studio/package.json +10 -14
- package/studio/server/agent/ask.test.ts +1 -1
- package/studio/server/agent/bridge/grant.test.ts +1 -1
- package/studio/server/agent/bridge/routes.test.ts +1 -1
- package/studio/server/agent/bridge/stdio.ts +8 -1
- package/studio/server/agent/conversation.test.ts +10 -5
- package/studio/server/agent/conversation.ts +56 -8
- package/studio/server/agent/harness/claude/adapter.test.ts +15 -5
- package/studio/server/agent/harness/codex/loadout.ts +18 -21
- package/studio/server/agent/harness/gateway/config.ts +20 -18
- package/studio/server/agent/harness/selection.ts +9 -1
- package/studio/server/agent/prompts/anchors.test.ts +32 -36
- package/studio/server/agent/prompts/anchors.ts +20 -49
- package/studio/server/agent/prompts/system.test.ts +3 -2
- package/studio/server/agent/prompts/system.ts +6 -5
- package/studio/server/agent/prompts/turn.ts +13 -4
- package/studio/server/agent/routes.test.ts +1 -1
- package/studio/server/agent/run/completion.ts +2 -2
- package/studio/server/agent/run/coordinator.test.ts +1 -1
- package/studio/server/agent/run/preparation.ts +6 -5
- package/studio/server/agent/run/transcript.test.ts +35 -1
- package/studio/server/agent/run/transcript.ts +169 -3
- package/studio/server/agent/run/usage.test.ts +24 -0
- package/studio/server/agent/run/usage.ts +26 -1
- package/studio/server/api/canvas.ts +43 -0
- package/studio/server/api/comments.ts +85 -0
- package/studio/server/api/context.ts +61 -0
- package/studio/server/api/deployment.ts +39 -0
- package/studio/server/api/documents.ts +55 -0
- package/studio/server/api/domain.ts +59 -0
- package/studio/server/api/http.ts +45 -0
- package/studio/server/api/project.ts +57 -0
- package/studio/server/api/schema.ts +12 -0
- package/studio/server/api/views.ts +57 -0
- package/studio/server/api/workspace.ts +60 -0
- package/studio/server/api-agent-loadout.test.ts +1 -1
- package/studio/server/api.test.ts +147 -0
- package/studio/server/api.ts +20 -422
- package/studio/server/cache.test.ts +122 -0
- package/studio/server/cache.ts +162 -11
- package/studio/server/cli-consumers.test.ts +494 -0
- package/studio/server/cli.test.ts +88 -0
- package/studio/server/cli.ts +240 -0
- package/studio/server/client-package.ts +25 -1
- package/studio/server/domain.test.ts +50 -40
- package/studio/server/domain.ts +83 -52
- package/studio/server/environment/dotenv-preview.test.ts +39 -0
- package/studio/server/environment/dotenv-preview.ts +27 -0
- package/studio/server/environment/files.test.ts +53 -0
- package/studio/server/{state/env.ts → environment/files.ts} +7 -48
- package/studio/server/{state → handoff}/copy.ts +21 -6
- package/studio/server/handoff/service.test.ts +22 -0
- package/studio/server/{state/handoff.ts → handoff/service.ts} +21 -14
- package/studio/server/index.ts +1 -1
- package/studio/server/instances/active.ts +72 -0
- package/studio/server/instances/deploy-record.test.ts +31 -0
- package/studio/server/instances/deploy-record.ts +37 -0
- package/studio/server/instances/deploy.ts +56 -0
- package/studio/server/{state/instance.test.ts → instances/probe.test.ts} +25 -21
- package/studio/server/instances/probe.ts +67 -0
- package/studio/server/instances/status.test.ts +12 -0
- package/studio/server/instances/status.ts +49 -0
- package/studio/server/introspect/anatomy/client-tree.ts +57 -0
- package/studio/server/introspect/anatomy/env-fields.ts +66 -0
- package/studio/server/introspect/anatomy/schema-definition.ts +53 -0
- package/studio/server/introspect/anatomy/source.ts +181 -0
- package/studio/server/introspect/anatomy/views/routes.ts +114 -0
- package/studio/server/introspect/anatomy/views.ts +53 -0
- package/studio/server/introspect/anatomy-extras.test.ts +106 -35
- package/studio/server/introspect/anatomy-extras.ts +7 -726
- package/studio/server/introspect/anatomy.test.ts +22 -14
- package/studio/server/introspect/anatomy.ts +52 -28
- package/studio/server/introspect/bundle.ts +15 -9
- package/studio/server/introspect/canonical-schema.test.ts +93 -309
- package/studio/server/introspect/canonical-schema.ts +257 -425
- package/studio/server/introspect/config-preview.test.ts +32 -0
- package/studio/server/introspect/config-preview.ts +119 -0
- package/studio/server/introspect/core-extractor.ts +18 -127
- package/studio/server/introspect/core.ts +1 -2
- package/studio/server/introspect/diff.test.ts +18 -18
- package/studio/server/introspect/diff.ts +44 -90
- package/studio/server/introspect/extractor.ts +23 -116
- package/studio/server/introspect/hash.ts +5 -2
- package/studio/server/introspect/overlay-tsmorph.test.ts +119 -240
- package/studio/server/introspect/overlay-tsmorph.ts +7 -1305
- package/studio/server/introspect/overlay.test.ts +26 -54
- package/studio/server/introspect/overlay.ts +10 -37
- package/studio/server/introspect/revision.test.ts +54 -0
- package/studio/server/introspect/revision.ts +49 -0
- package/studio/server/introspect/runtime.test.ts +83 -143
- package/studio/server/introspect/runtime.ts +40 -11
- package/studio/server/introspect/schema-ir-json.test.ts +70 -0
- package/studio/server/introspect/schema-ir-json.ts +151 -0
- package/studio/server/introspect/schema-refs.test.ts +43 -88
- package/studio/server/introspect/schema-refs.ts +14 -49
- package/studio/server/introspect/source-overlay/annotations.ts +14 -0
- package/studio/server/introspect/source-overlay/handlers.ts +161 -0
- package/studio/server/introspect/source-overlay/kernel-calls.ts +46 -0
- package/studio/server/introspect/source-overlay/project.ts +248 -0
- package/studio/server/introspect/source-overlay/spans.ts +330 -0
- package/studio/server/json.ts +46 -0
- package/studio/server/lifecycle.ts +5 -1
- package/studio/server/sse.test.ts +26 -0
- package/studio/server/sse.ts +4 -1
- package/studio/server/state/baseline.test.ts +223 -7
- package/studio/server/state/baseline.ts +148 -34
- package/studio/server/state/comments.test.ts +66 -1
- package/studio/server/state/comments.ts +173 -10
- package/studio/server/state/context.ts +34 -2
- package/studio/server/state/documents.ts +42 -1
- package/studio/server/state/integrations.ts +27 -1
- package/studio/server/state/layout.test.ts +34 -0
- package/studio/server/state/layout.ts +44 -17
- package/studio/server/state/settings.ts +29 -22
- package/studio/server/state/store.test.ts +122 -0
- package/studio/server/state/store.ts +66 -17
- package/studio/server/state/visibility.ts +22 -11
- package/studio/server/views/model.test.ts +57 -0
- package/studio/server/views/model.ts +66 -0
- package/studio/server/views/runtime.ts +40 -0
- package/studio/server/views/selection-repository.test.ts +31 -0
- package/studio/server/views/selection-repository.ts +60 -0
- package/studio/server/views/session.test.ts +73 -0
- package/studio/server/views/session.ts +166 -0
- package/studio/server/views/target.test.ts +78 -0
- package/studio/server/views/target.ts +222 -0
- package/studio/server/watch.test.ts +11 -11
- package/studio/server/watch.ts +9 -5
- package/studio/server/workspace/catalog.test.ts +17 -0
- package/studio/server/workspace/catalog.ts +43 -0
- package/studio/server/{state → workspace}/create.test.ts +1 -1
- package/studio/server/{state → workspace}/create.ts +1 -1
- package/studio/server/{state → workspace}/git.ts +1 -1
- package/studio/server/workspace/updates.ts +83 -0
- package/studio/server/workspace-state.ts +1 -1
- package/studio/server/workspace-watch.ts +2 -2
- package/studio/shared/contracts/README.md +16 -0
- package/studio/shared/contracts/agent.ts +270 -0
- package/studio/shared/contracts/runtime.ts +50 -0
- package/studio/shared/contracts/schema.ts +393 -0
- package/studio/shared/contracts/surface.test.ts +317 -0
- package/studio/shared/contracts/workspace.ts +265 -0
- package/studio/shared/layout.test.ts +116 -0
- package/studio/shared/schema/identity.test.ts +34 -0
- package/studio/shared/schema/identity.ts +68 -0
- package/studio/shared/types.ts +11 -1099
- package/studio/tsconfig.json +1 -1
- package/viewer/dist/main.js +40 -61
- package/.check-workspace.cjs +0 -40
- package/src/commands/__tests__/instance-create-hosts.test.ts +0 -29
- package/studio/client/dist/assets/elk-api-D2xgMJvi.js +0 -1
- package/studio/client/dist/assets/index-BMdnsIJA.css +0 -1
- package/studio/client/dist/assets/index-ChOr3yP0.css +0 -1
- package/studio/client/dist/assets/index-D-vRV8w7.js +0 -8
- package/studio/client/dist/assets/index-LGSWRrk8.js +0 -81
- package/studio/server/state/catalog.ts +0 -117
- package/studio/server/state/instance.ts +0 -280
- package/studio/server/state/updates.ts +0 -63
- package/studio/server/state/views.test.ts +0 -286
- package/studio/server/state/views.ts +0 -535
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { InstanceStatus, SchemaRevision } from '../../shared/types'
|
|
2
|
+
import type { DomainHandle } from '../domain'
|
|
3
|
+
|
|
4
|
+
import { admittedBundleRevision } from '../introspect/revision'
|
|
5
|
+
import { hasProdScript } from './deploy'
|
|
6
|
+
import { lastDeploy } from './deploy-record'
|
|
7
|
+
import { getInstalledDomain } from './probe'
|
|
8
|
+
|
|
9
|
+
export function schemaRevisionDrift(
|
|
10
|
+
localRevision: SchemaRevision | null,
|
|
11
|
+
installedRevision: SchemaRevision | null,
|
|
12
|
+
): InstanceStatus['drift'] {
|
|
13
|
+
if (!localRevision || !installedRevision) return 'unknown'
|
|
14
|
+
return localRevision === installedRevision ? 'in-sync' : 'drifted'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function instanceStatus(
|
|
18
|
+
handle: DomainHandle,
|
|
19
|
+
deployTarget: string | null,
|
|
20
|
+
origin: string | null,
|
|
21
|
+
localRevision: SchemaRevision | null,
|
|
22
|
+
): Promise<InstanceStatus> {
|
|
23
|
+
const deployable = hasProdScript(handle.root)
|
|
24
|
+
const last = lastDeploy(handle.root)
|
|
25
|
+
let install: InstanceStatus['install'] = 'unknown'
|
|
26
|
+
let installedRevision: SchemaRevision | null = null
|
|
27
|
+
let drift: InstanceStatus['drift'] = 'unknown'
|
|
28
|
+
|
|
29
|
+
if (origin && deployTarget) {
|
|
30
|
+
const probe = await getInstalledDomain(origin, deployTarget)
|
|
31
|
+
if (probe.state === 'installed') {
|
|
32
|
+
install = 'installed'
|
|
33
|
+
installedRevision = await admittedBundleRevision(handle.root, probe.bundle)
|
|
34
|
+
drift = schemaRevisionDrift(localRevision, installedRevision)
|
|
35
|
+
} else if (probe.state === 'not-installed') {
|
|
36
|
+
install = 'not-installed'
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
deployTarget,
|
|
42
|
+
deployable,
|
|
43
|
+
install,
|
|
44
|
+
drift,
|
|
45
|
+
localRevision,
|
|
46
|
+
installedRevision,
|
|
47
|
+
lastDeploy: last,
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import type { ClientTree } from '../../../shared/types'
|
|
5
|
+
|
|
6
|
+
import { listDirs, listFiles, readTextSafe } from './source'
|
|
7
|
+
|
|
8
|
+
// Client tree and best-effort top-level route registry parsing.
|
|
9
|
+
const RESERVED_CLIENT_DIRS = new Set(['shell', 'ui', 'views'])
|
|
10
|
+
|
|
11
|
+
export function buildClientTree(
|
|
12
|
+
root: string,
|
|
13
|
+
clientDir: string | null = join(root, 'client'),
|
|
14
|
+
): ClientTree {
|
|
15
|
+
// Current SDK projects own presentation modules directly under ui/. Legacy
|
|
16
|
+
// projects still point at a client package whose authored tree is client/src.
|
|
17
|
+
const srcDir = clientDir
|
|
18
|
+
? join(clientDir, 'src')
|
|
19
|
+
: existsSync(join(root, 'ui'))
|
|
20
|
+
? join(root, 'ui')
|
|
21
|
+
: ''
|
|
22
|
+
if (!existsSync(srcDir)) {
|
|
23
|
+
return { shell: [], features: [], routes: {}, present: false }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const shell = listFiles(join(srcDir, 'shell'))
|
|
27
|
+
|
|
28
|
+
const features = listDirs(srcDir)
|
|
29
|
+
.filter((d) => !RESERVED_CLIENT_DIRS.has(d))
|
|
30
|
+
.map((name) => ({ name, files: listFiles(join(srcDir, name)) }))
|
|
31
|
+
|
|
32
|
+
const routes = parseRoutes(
|
|
33
|
+
listFiles(srcDir)
|
|
34
|
+
.filter((file) => /\.[cm]?[jt]sx?$/.test(file))
|
|
35
|
+
.map((file) => join(srcDir, file)),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
return { shell, features, routes, present: true }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Best-effort parse of top-level client route registries. Domains call these
|
|
43
|
+
* maps ROUTES, VIEW_REGISTRY, or another local name; the stable contract is a
|
|
44
|
+
* quoted `/ui/…` key pointing at a component identifier.
|
|
45
|
+
*/
|
|
46
|
+
function parseRoutes(files: string[]): Record<string, string> {
|
|
47
|
+
const routes: Record<string, string> = {}
|
|
48
|
+
for (const file of files) {
|
|
49
|
+
const src = readTextSafe(file)
|
|
50
|
+
const entryRe = /['"`](\/ui\/[^'"`]+)['"`]\s*:\s*([A-Za-z_$][\w$.]*)/g
|
|
51
|
+
let match: RegExpExecArray | null
|
|
52
|
+
while ((match = entryRe.exec(src)) !== null) {
|
|
53
|
+
routes[match[1]] = match[2]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return routes
|
|
57
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { InterfaceDeclaration, Node, Project } from 'ts-morph'
|
|
4
|
+
|
|
5
|
+
import type { EnvField } from '../../../shared/types'
|
|
6
|
+
|
|
7
|
+
import { makeProject } from './source'
|
|
8
|
+
|
|
9
|
+
/** Strip the leading-JSDoc decorations to a single trimmed line of prose. */
|
|
10
|
+
function cleanDoc(raw: string | undefined): string | undefined {
|
|
11
|
+
if (!raw) return undefined
|
|
12
|
+
const text = raw
|
|
13
|
+
.replace(/^\/\*\*?/, '')
|
|
14
|
+
.replace(/\*\/$/, '')
|
|
15
|
+
.split('\n')
|
|
16
|
+
.map((l) => l.replace(/^\s*\*\s?/, '').trimEnd())
|
|
17
|
+
.join('\n')
|
|
18
|
+
.trim()
|
|
19
|
+
.replace(/\s*\n\s*/g, ' ')
|
|
20
|
+
.trim()
|
|
21
|
+
return text.length ? text : undefined
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ───────────────────────────── 3) env fields ──────────────────────────────
|
|
25
|
+
|
|
26
|
+
const KNOWN_INFRA_FIELDS = new Set(['WORKER_URL', 'ASSETS', 'SELF', 'VIEW_DEV_URL'])
|
|
27
|
+
|
|
28
|
+
export function buildEnvFields(root: string): EnvField[] {
|
|
29
|
+
const envFile = join(root, 'env.ts')
|
|
30
|
+
if (!existsSync(envFile)) return []
|
|
31
|
+
|
|
32
|
+
let project: Project
|
|
33
|
+
let sf
|
|
34
|
+
try {
|
|
35
|
+
project = makeProject()
|
|
36
|
+
sf = project.addSourceFileAtPath(envFile)
|
|
37
|
+
} catch {
|
|
38
|
+
return []
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const iface: InterfaceDeclaration | undefined =
|
|
42
|
+
sf.getInterface('Env') ?? sf.getInterfaces().find((i) => i.getName() === 'Env')
|
|
43
|
+
if (!iface) return []
|
|
44
|
+
|
|
45
|
+
const fields: EnvField[] = []
|
|
46
|
+
for (const member of iface.getMembers()) {
|
|
47
|
+
// Skip index signatures: `[key: string]: unknown`.
|
|
48
|
+
if (Node.isIndexSignatureDeclaration(member)) continue
|
|
49
|
+
if (!Node.isPropertySignature(member)) continue
|
|
50
|
+
|
|
51
|
+
const name = member.getName()
|
|
52
|
+
if (!name) continue
|
|
53
|
+
|
|
54
|
+
const optional = member.hasQuestionToken()
|
|
55
|
+
const doc = cleanDoc(member.getJsDocs()[0]?.getInnerText())
|
|
56
|
+
|
|
57
|
+
const isInfra = KNOWN_INFRA_FIELDS.has(name)
|
|
58
|
+
const secret = !isInfra
|
|
59
|
+
|
|
60
|
+
const field: EnvField = { name, optional, secret }
|
|
61
|
+
if (doc) field.doc = doc
|
|
62
|
+
fields.push(field)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return fields
|
|
66
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
import { Node, type SourceFile, SyntaxKind } from 'ts-morph'
|
|
3
|
+
|
|
4
|
+
import { addSource, listSourceFiles, literalString, makeProject } from './source'
|
|
5
|
+
|
|
6
|
+
// Canonical schema source discovery.
|
|
7
|
+
|
|
8
|
+
export interface SchemaDefinitionLocation {
|
|
9
|
+
origin: string
|
|
10
|
+
file: string
|
|
11
|
+
line: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function schemaProject(root: string, schemaDirName: string): SourceFile[] {
|
|
15
|
+
const project = makeProject()
|
|
16
|
+
return listSourceFiles(join(root, schemaDirName))
|
|
17
|
+
.map((file) => addSource(project, file))
|
|
18
|
+
.filter((source): source is SourceFile => source !== null)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function defineSchemaCalls(source: SourceFile) {
|
|
22
|
+
return source.getDescendantsOfKind(SyntaxKind.CallExpression).filter((call) => {
|
|
23
|
+
const expression = call.getExpression()
|
|
24
|
+
return (
|
|
25
|
+
(Node.isIdentifier(expression) && expression.getText() === 'defineSchema') ||
|
|
26
|
+
(Node.isPropertyAccessExpression(expression) && expression.getName() === 'defineSchema')
|
|
27
|
+
)
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Find the accepted schema without importing it. Current project roots are
|
|
33
|
+
* barrels, so all authored schema modules are inspected and local origin
|
|
34
|
+
* constants such as `defineSchema(ORIGIN, ...)` are resolved.
|
|
35
|
+
*/
|
|
36
|
+
export function findSchemaDefinition(
|
|
37
|
+
root: string,
|
|
38
|
+
schemaDirName = 'schema',
|
|
39
|
+
): SchemaDefinitionLocation | null {
|
|
40
|
+
for (const source of schemaProject(root, schemaDirName)) {
|
|
41
|
+
for (const call of defineSchemaCalls(source)) {
|
|
42
|
+
const origin = literalString(call.getArguments()[0], source)
|
|
43
|
+
if (!origin) continue
|
|
44
|
+
const statement = call.getFirstAncestorByKind(SyntaxKind.VariableStatement)
|
|
45
|
+
return {
|
|
46
|
+
origin,
|
|
47
|
+
file: source.getFilePath(),
|
|
48
|
+
line: statement?.getStartLineNumber() ?? call.getStartLineNumber(),
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { Node, Project, type SourceFile } from 'ts-morph'
|
|
4
|
+
|
|
5
|
+
export function readTextSafe(file: string): string {
|
|
6
|
+
try {
|
|
7
|
+
return existsSync(file) ? readFileSync(file, 'utf8') : ''
|
|
8
|
+
} catch {
|
|
9
|
+
return ''
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function listFiles(dir: string): string[] {
|
|
14
|
+
try {
|
|
15
|
+
return readdirSync(dir)
|
|
16
|
+
.filter((e) => {
|
|
17
|
+
try {
|
|
18
|
+
return statSync(join(dir, e)).isFile()
|
|
19
|
+
} catch {
|
|
20
|
+
return false
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
.sort()
|
|
24
|
+
} catch {
|
|
25
|
+
return []
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function listDirs(dir: string): string[] {
|
|
30
|
+
try {
|
|
31
|
+
return readdirSync(dir)
|
|
32
|
+
.filter((e) => {
|
|
33
|
+
try {
|
|
34
|
+
return statSync(join(dir, e)).isDirectory()
|
|
35
|
+
} catch {
|
|
36
|
+
return false
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
.sort()
|
|
40
|
+
} catch {
|
|
41
|
+
return []
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const SOURCE_FILE = /\.[cm]?[jt]sx?$/
|
|
46
|
+
const SKIP_SOURCE_DIRS = new Set(['__tests__', 'node_modules', '.git', '.astrale', '.dist', 'dist'])
|
|
47
|
+
|
|
48
|
+
export function listSourceFiles(dir: string): string[] {
|
|
49
|
+
const files: string[] = []
|
|
50
|
+
const walk = (current: string) => {
|
|
51
|
+
let entries: string[]
|
|
52
|
+
try {
|
|
53
|
+
entries = readdirSync(current).sort()
|
|
54
|
+
} catch {
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
if (SKIP_SOURCE_DIRS.has(entry)) continue
|
|
59
|
+
const file = join(current, entry)
|
|
60
|
+
let stat
|
|
61
|
+
try {
|
|
62
|
+
stat = statSync(file)
|
|
63
|
+
} catch {
|
|
64
|
+
continue
|
|
65
|
+
}
|
|
66
|
+
if (stat.isDirectory()) walk(file)
|
|
67
|
+
else if (
|
|
68
|
+
stat.isFile() &&
|
|
69
|
+
SOURCE_FILE.test(entry) &&
|
|
70
|
+
!entry.endsWith('.d.ts') &&
|
|
71
|
+
!/\.(?:test|spec)\.[cm]?[jt]sx?$/.test(entry)
|
|
72
|
+
) {
|
|
73
|
+
files.push(file)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
walk(dir)
|
|
78
|
+
return files
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A throwaway in-memory ts-morph project (no tsconfig, no type-checking IO). */
|
|
82
|
+
export function makeProject(): Project {
|
|
83
|
+
return new Project({
|
|
84
|
+
useInMemoryFileSystem: false,
|
|
85
|
+
skipAddingFilesFromTsConfig: true,
|
|
86
|
+
skipFileDependencyResolution: true,
|
|
87
|
+
skipLoadingLibFiles: true,
|
|
88
|
+
compilerOptions: { allowJs: true },
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function addSource(project: Project, file: string): SourceFile | null {
|
|
93
|
+
try {
|
|
94
|
+
return project.getSourceFile(file) ?? project.addSourceFileAtPath(file)
|
|
95
|
+
} catch {
|
|
96
|
+
return null
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function unwrap(node: Node): Node {
|
|
101
|
+
let current = node
|
|
102
|
+
while (
|
|
103
|
+
Node.isAsExpression(current) ||
|
|
104
|
+
Node.isSatisfiesExpression(current) ||
|
|
105
|
+
Node.isParenthesizedExpression(current) ||
|
|
106
|
+
Node.isTypeAssertion(current) ||
|
|
107
|
+
Node.isNonNullExpression(current)
|
|
108
|
+
) {
|
|
109
|
+
current = current.getExpression()
|
|
110
|
+
}
|
|
111
|
+
return current
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function localValue(
|
|
115
|
+
node: Node | undefined,
|
|
116
|
+
source: SourceFile,
|
|
117
|
+
seen = new Set<string>(),
|
|
118
|
+
): Node | null {
|
|
119
|
+
if (!node) return null
|
|
120
|
+
const value = unwrap(node)
|
|
121
|
+
if (!Node.isIdentifier(value)) return value
|
|
122
|
+
const key = `${source.getFilePath()}:${value.getText()}`
|
|
123
|
+
if (seen.has(key)) return value
|
|
124
|
+
seen.add(key)
|
|
125
|
+
const init = source.getVariableDeclaration(value.getText())?.getInitializer()
|
|
126
|
+
return init ? localValue(init, source, seen) : value
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function objectValue(node: Node | undefined, source: SourceFile): Node | null {
|
|
130
|
+
const value = localValue(node, source)
|
|
131
|
+
return value && Node.isObjectLiteralExpression(value) ? value : null
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function objectProperty(object: Node, name: string, source: SourceFile): Node | null {
|
|
135
|
+
if (!Node.isObjectLiteralExpression(object)) return null
|
|
136
|
+
for (const property of object.getProperties()) {
|
|
137
|
+
if (Node.isPropertyAssignment(property) && property.getName() === name) {
|
|
138
|
+
return localValue(property.getInitializer(), source)
|
|
139
|
+
}
|
|
140
|
+
if (Node.isShorthandPropertyAssignment(property) && property.getName() === name) {
|
|
141
|
+
return localValue(property.getNameNode(), source)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return null
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function literalString(node: Node | undefined, source: SourceFile): string | undefined {
|
|
148
|
+
const value = localValue(node, source)
|
|
149
|
+
if (!value) return undefined
|
|
150
|
+
if (Node.isStringLiteral(value) || Node.isNoSubstitutionTemplateLiteral(value)) {
|
|
151
|
+
return value.getLiteralValue()
|
|
152
|
+
}
|
|
153
|
+
return undefined
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function callName(node: Node | null): string | undefined {
|
|
157
|
+
if (!node || !Node.isCallExpression(node)) return undefined
|
|
158
|
+
const expression = node.getExpression()
|
|
159
|
+
if (Node.isIdentifier(expression)) return expression.getText()
|
|
160
|
+
if (Node.isPropertyAccessExpression(expression)) return expression.getName()
|
|
161
|
+
return undefined
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function propertySlug(property: Node): string | undefined {
|
|
165
|
+
if (!Node.isPropertyAssignment(property) && !Node.isShorthandPropertyAssignment(property)) {
|
|
166
|
+
return undefined
|
|
167
|
+
}
|
|
168
|
+
const name = property.getNameNode()
|
|
169
|
+
if (Node.isStringLiteral(name) || Node.isNoSubstitutionTemplateLiteral(name)) {
|
|
170
|
+
return name.getLiteralValue()
|
|
171
|
+
}
|
|
172
|
+
return property.getName()
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function propertyValue(property: Node, source: SourceFile): Node | null {
|
|
176
|
+
if (Node.isPropertyAssignment(property)) return localValue(property.getInitializer(), source)
|
|
177
|
+
if (Node.isShorthandPropertyAssignment(property)) {
|
|
178
|
+
return localValue(property.getNameNode(), source)
|
|
179
|
+
}
|
|
180
|
+
return null
|
|
181
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { join, relative } from 'node:path'
|
|
2
|
+
import { Node, type SourceFile, SyntaxKind } from 'ts-morph'
|
|
3
|
+
|
|
4
|
+
import type { ViewInfo } from '../../../../shared/types'
|
|
5
|
+
|
|
6
|
+
import { resolveApplicationEntry } from '../../../domain'
|
|
7
|
+
import { defineSchemaCalls, schemaProject } from '../schema-definition'
|
|
8
|
+
import {
|
|
9
|
+
addSource,
|
|
10
|
+
callName,
|
|
11
|
+
listSourceFiles,
|
|
12
|
+
literalString,
|
|
13
|
+
makeProject,
|
|
14
|
+
objectProperty,
|
|
15
|
+
objectValue,
|
|
16
|
+
propertySlug,
|
|
17
|
+
propertyValue,
|
|
18
|
+
} from '../source'
|
|
19
|
+
|
|
20
|
+
/** Locate authored declarations for canonical View keys without projecting any semantics. */
|
|
21
|
+
export function buildSchemaViewSources(root: string, schemaDirName: string): Map<string, string> {
|
|
22
|
+
const sources = new Map<string, string>()
|
|
23
|
+
for (const source of schemaProject(root, schemaDirName)) {
|
|
24
|
+
for (const call of defineSchemaCalls(source)) {
|
|
25
|
+
const input = objectValue(call.getArguments()[1], source)
|
|
26
|
+
const declared = input
|
|
27
|
+
? objectValue(objectProperty(input, 'views', source) ?? undefined, source)
|
|
28
|
+
: null
|
|
29
|
+
if (!declared || !Node.isObjectLiteralExpression(declared)) continue
|
|
30
|
+
for (const property of declared.getProperties()) {
|
|
31
|
+
const slug = propertySlug(property)
|
|
32
|
+
const declaration = propertyValue(property, source)
|
|
33
|
+
if (
|
|
34
|
+
slug &&
|
|
35
|
+
declaration &&
|
|
36
|
+
Node.isCallExpression(declaration) &&
|
|
37
|
+
callName(declaration) === 'view'
|
|
38
|
+
) {
|
|
39
|
+
sources.set(slug, relative(root, source.getFilePath()).replaceAll('\\', '/'))
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return sources
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function defaultRoute(name: string): string {
|
|
48
|
+
return `/${name
|
|
49
|
+
.replace(/([a-z0-9])([A-Z])/gu, '$1-$2')
|
|
50
|
+
.replace(/[_\s]+/gu, '-')
|
|
51
|
+
.toLowerCase()}`
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function frontendSource(input: Node, source: SourceFile): { externalOrigin?: string } {
|
|
55
|
+
const value = objectProperty(input, 'source', source)
|
|
56
|
+
if (!value || !Node.isCallExpression(value)) return {}
|
|
57
|
+
if (callName(value) !== 'external') return {}
|
|
58
|
+
const origin = literalString(value.getArguments()[0], source)
|
|
59
|
+
return origin === undefined ? {} : { externalOrigin: origin }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function buildFrontendViews(
|
|
63
|
+
root: string,
|
|
64
|
+
canonicalViewNames: readonly string[],
|
|
65
|
+
): ViewInfo[] {
|
|
66
|
+
const project = makeProject()
|
|
67
|
+
const application = resolveApplicationEntry(root)
|
|
68
|
+
const applicationFiles = application === null ? [] : [application]
|
|
69
|
+
const sources = [...new Set([...listSourceFiles(join(root, 'views')), ...applicationFiles])]
|
|
70
|
+
.map((file) => addSource(project, file))
|
|
71
|
+
.filter((source): source is SourceFile => source !== null)
|
|
72
|
+
const views: ViewInfo[] = []
|
|
73
|
+
|
|
74
|
+
for (const source of sources) {
|
|
75
|
+
for (const call of source.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
76
|
+
const frontendName = callName(call)
|
|
77
|
+
if (frontendName !== 'defineFrontend') continue
|
|
78
|
+
const input = objectValue(call.getArguments()[0], source)
|
|
79
|
+
if (!input) continue
|
|
80
|
+
const routes = objectValue(objectProperty(input, 'routes', source) ?? undefined, source)
|
|
81
|
+
const declared = new Map<string, string>()
|
|
82
|
+
if (routes && Node.isObjectLiteralExpression(routes)) {
|
|
83
|
+
for (const property of routes.getProperties()) {
|
|
84
|
+
const slug = propertySlug(property)
|
|
85
|
+
const routeValue = propertyValue(property, source)
|
|
86
|
+
if (!slug || !routeValue) continue
|
|
87
|
+
const direct = literalString(routeValue, source)
|
|
88
|
+
const routeInput = objectValue(routeValue, source)
|
|
89
|
+
const path =
|
|
90
|
+
direct ??
|
|
91
|
+
(routeInput
|
|
92
|
+
? literalString(objectProperty(routeInput, 'path', source) ?? undefined, source)
|
|
93
|
+
: undefined)
|
|
94
|
+
if (path) declared.set(slug, path)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const frontend = frontendSource(input, source)
|
|
98
|
+
|
|
99
|
+
for (const slug of canonicalViewNames) {
|
|
100
|
+
const path = declared.get(slug) ?? defaultRoute(slug)
|
|
101
|
+
const url = frontend.externalOrigin
|
|
102
|
+
? new URL(path, `${frontend.externalOrigin}/`).toString()
|
|
103
|
+
: undefined
|
|
104
|
+
views.push({
|
|
105
|
+
slug,
|
|
106
|
+
kind: 'spa',
|
|
107
|
+
...(url ? { url } : { mount: path }),
|
|
108
|
+
file: relative(root, source.getFilePath()).replaceAll('\\', '/'),
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return views
|
|
114
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { SchemaIR, ViewInfo } from '../../../shared/types'
|
|
2
|
+
|
|
3
|
+
import { buildFrontendViews, buildSchemaViewSources } from './views/routes'
|
|
4
|
+
|
|
5
|
+
function mergeRoute(target: ViewInfo, incoming: ViewInfo): void {
|
|
6
|
+
if (incoming.kind !== 'unknown') target.kind = incoming.kind
|
|
7
|
+
if (incoming.mount !== undefined) target.mount = incoming.mount
|
|
8
|
+
if (incoming.url !== undefined) target.url = incoming.url
|
|
9
|
+
if (incoming.file) target.file = incoming.file
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function canonicalViewInfo(slug: string, view: NonNullable<SchemaIR['views']>[string]): ViewInfo {
|
|
13
|
+
const targets =
|
|
14
|
+
view.target.kind === 'definition' ? view.target.definitions.map((ref) => ref.name) : []
|
|
15
|
+
return {
|
|
16
|
+
slug,
|
|
17
|
+
kind: 'unknown',
|
|
18
|
+
auth: view.auth,
|
|
19
|
+
...(view.description ? { description: view.description } : {}),
|
|
20
|
+
...(targets.length === 1
|
|
21
|
+
? { viewFor: targets[0] }
|
|
22
|
+
: targets.length > 1
|
|
23
|
+
? { viewFor: targets }
|
|
24
|
+
: {}),
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Join canonical View definitions with route/source metadata. Passing a
|
|
30
|
+
* canonical map (including an empty map) makes it authoritative: static routes
|
|
31
|
+
* cannot invent Views or override identity, target, auth, or description.
|
|
32
|
+
* Without admitted canonical Views, Studio does not invent authoring semantics.
|
|
33
|
+
*/
|
|
34
|
+
export function buildViews(
|
|
35
|
+
root: string,
|
|
36
|
+
_schemaDirName = 'schema',
|
|
37
|
+
canonicalViews?: NonNullable<SchemaIR['views']>,
|
|
38
|
+
): ViewInfo[] {
|
|
39
|
+
const merged = new Map<string, ViewInfo>()
|
|
40
|
+
const admittedViews = canonicalViews ?? {}
|
|
41
|
+
for (const [slug, view] of Object.entries(admittedViews)) {
|
|
42
|
+
merged.set(slug, canonicalViewInfo(slug, view))
|
|
43
|
+
}
|
|
44
|
+
for (const [slug, file] of buildSchemaViewSources(root, _schemaDirName)) {
|
|
45
|
+
const current = merged.get(slug)
|
|
46
|
+
if (current) current.file = file
|
|
47
|
+
}
|
|
48
|
+
for (const view of buildFrontendViews(root, Object.keys(admittedViews))) {
|
|
49
|
+
const current = merged.get(view.slug)
|
|
50
|
+
if (current) mergeRoute(current, view)
|
|
51
|
+
}
|
|
52
|
+
return [...merged.values()]
|
|
53
|
+
}
|