@softize/opus 8.6.6
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/CHANGELOG.md +1616 -0
- package/LICENSE +21 -0
- package/README.md +113 -0
- package/bin/cli.mjs +528 -0
- package/bin/lib/check.mjs +307 -0
- package/bin/lib/components.mjs +151 -0
- package/bin/lib/create.mjs +208 -0
- package/bin/lib/db-check-runner.mjs +86 -0
- package/bin/lib/db-migrate-runner.mjs +89 -0
- package/bin/lib/db-scaffold-runner.mjs +84 -0
- package/bin/lib/db.mjs +261 -0
- package/bin/lib/docs-include.mjs +48 -0
- package/bin/lib/gen-dicts.mjs +134 -0
- package/bin/lib/gen-docs.mjs +288 -0
- package/bin/lib/gen-manifest.mjs +102 -0
- package/bin/lib/gen-openapi.mjs +195 -0
- package/bin/lib/gen-runner.mjs +472 -0
- package/bin/lib/gen-stubs.mjs +463 -0
- package/bin/lib/gen.mjs +311 -0
- package/bin/lib/init.mjs +514 -0
- package/bin/lib/introspect.mjs +107 -0
- package/bin/lib/mcp.mjs +85 -0
- package/bin/lib/postinstall.mjs +56 -0
- package/docs/chat-event-protocol.md +85 -0
- package/docs/code-style.md +16 -0
- package/docs/data-layer.md +246 -0
- package/docs/ownership-vs-shadcn-lock.md +102 -0
- package/docs/protocol.md +2053 -0
- package/docs/releasing.md +110 -0
- package/docs/shellnav.md +131 -0
- package/package.json +338 -0
- package/registry/hooks/hooks.json +26 -0
- package/registry/hooks/link-memory-on-start.mjs +46 -0
- package/registry/hooks/opus-check-on-stop.mjs +114 -0
- package/registry/skills/create-action/SKILL.md +49 -0
- package/registry/skills/create-action/scaffold.mjs +122 -0
- package/registry/templates/app/_gitignore +3 -0
- package/registry/templates/app/_npmrc +1 -0
- package/registry/templates/app/_opus/_gitignore +5 -0
- package/registry/templates/app/_prettierrc.json +6 -0
- package/registry/templates/app/index.html +13 -0
- package/registry/templates/app/opus.config.ts +16 -0
- package/registry/templates/app/package.json +43 -0
- package/registry/templates/app/pnpm-workspace.yaml +11 -0
- package/registry/templates/app/public/favicon.svg +4 -0
- package/registry/templates/app/src/App.tsx +37 -0
- package/registry/templates/app/src/domains/tasks/actions/list.test.ts +34 -0
- package/registry/templates/app/src/domains/tasks/actions/list.ts +33 -0
- package/registry/templates/app/src/domains/tasks/index.ts +13 -0
- package/registry/templates/app/src/index.css +18 -0
- package/registry/templates/app/src/main.tsx +25 -0
- package/registry/templates/app/tsconfig.json +20 -0
- package/registry/templates/app/vite.config.ts +46 -0
- package/registry/templates/monorepo/_gitignore +3 -0
- package/registry/templates/monorepo/_npmrc +1 -0
- package/registry/templates/monorepo/package.json +9 -0
- package/registry/templates/monorepo/pnpm-workspace.yaml +14 -0
- package/src/ai/ask.ts +64 -0
- package/src/ai/drivers/anthropic.ts +309 -0
- package/src/ai/index.ts +17 -0
- package/src/audit/drivers/console.ts +117 -0
- package/src/audit/drivers/pg.ts +172 -0
- package/src/audit/index.ts +51 -0
- package/src/auth/drivers/better-auth.ts +103 -0
- package/src/auth/drivers/jwt.ts +188 -0
- package/src/auth/index.ts +9 -0
- package/src/client/drivers/fetch.ts +202 -0
- package/src/client/index.ts +22 -0
- package/src/core/actions.ts +110 -0
- package/src/core/audit.ts +239 -0
- package/src/core/contracts.ts +137 -0
- package/src/core/domain.ts +310 -0
- package/src/core/errors.ts +181 -0
- package/src/core/index.ts +174 -0
- package/src/core/logical-type.ts +31 -0
- package/src/core/reactions.ts +81 -0
- package/src/core/runtime.ts +1167 -0
- package/src/core/schedules.ts +41 -0
- package/src/core/types.ts +1356 -0
- package/src/data/drivers/kysely.ts +389 -0
- package/src/data/index.ts +10 -0
- package/src/data/readonly-pool.ts +160 -0
- package/src/dsl/eval.ts +136 -0
- package/src/dsl/index.ts +29 -0
- package/src/dsl/kysely.ts +230 -0
- package/src/dsl/loads.ts +123 -0
- package/src/dsl/parser.ts +423 -0
- package/src/dsl/types.ts +113 -0
- package/src/events/drivers/mitt.ts +70 -0
- package/src/events/index.ts +9 -0
- package/src/log/drivers/pino.ts +57 -0
- package/src/log/index.ts +9 -0
- package/src/mcp/index.ts +62 -0
- package/src/queue/drivers/bullmq.ts +190 -0
- package/src/queue/index.ts +9 -0
- package/src/scheduler/drivers/node-cron.ts +93 -0
- package/src/scheduler/every.ts +45 -0
- package/src/scheduler/index.ts +9 -0
- package/src/schema/drivers/zod.ts +765 -0
- package/src/schema/entity.ts +439 -0
- package/src/schema/format/locale.ts +144 -0
- package/src/schema/index.ts +65 -0
- package/src/schema/openapi.ts +302 -0
- package/src/schema/scaffold.ts +160 -0
- package/src/server/drivers/fastify.ts +224 -0
- package/src/server/drivers/node.ts +386 -0
- package/src/server/index.ts +142 -0
- package/src/storage/drivers/fs.ts +90 -0
- package/src/storage/drivers/s3.ts +117 -0
- package/src/storage/index.ts +27 -0
- package/src/testing/fake.ts +298 -0
- package/src/testing/index.ts +324 -0
- package/src/ui/components/patterns/action-form-card.tsx +48 -0
- package/src/ui/components/patterns/action-list-dialog.tsx +93 -0
- package/src/ui/components/patterns/app-shell.tsx +227 -0
- package/src/ui/components/patterns/confirm.tsx +226 -0
- package/src/ui/components/patterns/data-state.tsx +75 -0
- package/src/ui/components/patterns/form-dialog.tsx +64 -0
- package/src/ui/components/patterns/form.tsx +584 -0
- package/src/ui/components/patterns/list.tsx +1488 -0
- package/src/ui/components/patterns/page.tsx +46 -0
- package/src/ui/components/patterns/section-shell.tsx +246 -0
- package/src/ui/components/patterns/shell-nav.tsx +150 -0
- package/src/ui/components/patterns/sidebar.tsx +89 -0
- package/src/ui/components/patterns/split.tsx +93 -0
- package/src/ui/components/patterns/trigger.tsx +196 -0
- package/src/ui/components/patterns/view.tsx +84 -0
- package/src/ui/components/primitives/accordion.tsx +64 -0
- package/src/ui/components/primitives/alert-dialog.tsx +190 -0
- package/src/ui/components/primitives/alert.tsx +116 -0
- package/src/ui/components/primitives/aspect-ratio.tsx +9 -0
- package/src/ui/components/primitives/avatar.tsx +107 -0
- package/src/ui/components/primitives/badge.tsx +37 -0
- package/src/ui/components/primitives/breadcrumb.tsx +109 -0
- package/src/ui/components/primitives/button-group.tsx +83 -0
- package/src/ui/components/primitives/button.tsx +102 -0
- package/src/ui/components/primitives/calendar.tsx +218 -0
- package/src/ui/components/primitives/card.tsx +56 -0
- package/src/ui/components/primitives/carousel.tsx +239 -0
- package/src/ui/components/primitives/chat.tsx +407 -0
- package/src/ui/components/primitives/checkbox.tsx +30 -0
- package/src/ui/components/primitives/collapsible.tsx +31 -0
- package/src/ui/components/primitives/command.tsx +182 -0
- package/src/ui/components/primitives/composer.tsx +121 -0
- package/src/ui/components/primitives/copyable.tsx +50 -0
- package/src/ui/components/primitives/dialog.tsx +147 -0
- package/src/ui/components/primitives/drawer.tsx +141 -0
- package/src/ui/components/primitives/empty.tsx +104 -0
- package/src/ui/components/primitives/field.tsx +246 -0
- package/src/ui/components/primitives/icon-picker.tsx +180 -0
- package/src/ui/components/primitives/input-group.tsx +168 -0
- package/src/ui/components/primitives/input-otp.tsx +75 -0
- package/src/ui/components/primitives/input.tsx +72 -0
- package/src/ui/components/primitives/item.tsx +193 -0
- package/src/ui/components/primitives/kbd.tsx +28 -0
- package/src/ui/components/primitives/label.tsx +22 -0
- package/src/ui/components/primitives/markdown.tsx +35 -0
- package/src/ui/components/primitives/menu.tsx +255 -0
- package/src/ui/components/primitives/pagination.tsx +127 -0
- package/src/ui/components/primitives/popover.tsx +87 -0
- package/src/ui/components/primitives/progress.tsx +29 -0
- package/src/ui/components/primitives/radio-group.tsx +43 -0
- package/src/ui/components/primitives/resizable.tsx +51 -0
- package/src/ui/components/primitives/scroll-area.tsx +56 -0
- package/src/ui/components/primitives/select.tsx +479 -0
- package/src/ui/components/primitives/separator.tsx +26 -0
- package/src/ui/components/primitives/skeleton.tsx +13 -0
- package/src/ui/components/primitives/slider.tsx +61 -0
- package/src/ui/components/primitives/sonner.tsx +46 -0
- package/src/ui/components/primitives/spinner.tsx +29 -0
- package/src/ui/components/primitives/switch.tsx +33 -0
- package/src/ui/components/primitives/table.tsx +114 -0
- package/src/ui/components/primitives/tabs.tsx +104 -0
- package/src/ui/components/primitives/textarea.tsx +18 -0
- package/src/ui/components/primitives/toggle-group.tsx +81 -0
- package/src/ui/components/primitives/toggle.tsx +45 -0
- package/src/ui/components/primitives/tooltip.tsx +55 -0
- package/src/ui/components/primitives/truncate.tsx +49 -0
- package/src/ui/docs/DocBrowser.tsx +90 -0
- package/src/ui/docs/changelog.tsx +80 -0
- package/src/ui/docs/content/accordion.md +86 -0
- package/src/ui/docs/content/action-form-card.md +24 -0
- package/src/ui/docs/content/action-form-dialog.md +30 -0
- package/src/ui/docs/content/action-form.md +125 -0
- package/src/ui/docs/content/action-list-dialog.md +68 -0
- package/src/ui/docs/content/action-list.md +194 -0
- package/src/ui/docs/content/action-trigger.md +72 -0
- package/src/ui/docs/content/action-view.md +47 -0
- package/src/ui/docs/content/actions.md +138 -0
- package/src/ui/docs/content/ai.md +112 -0
- package/src/ui/docs/content/alert-dialog.md +73 -0
- package/src/ui/docs/content/alert.md +69 -0
- package/src/ui/docs/content/app-shell.md +155 -0
- package/src/ui/docs/content/aspect-ratio.md +66 -0
- package/src/ui/docs/content/audit.md +84 -0
- package/src/ui/docs/content/auth.md +70 -0
- package/src/ui/docs/content/avatar.md +94 -0
- package/src/ui/docs/content/badge.md +48 -0
- package/src/ui/docs/content/breadcrumb.md +87 -0
- package/src/ui/docs/content/button-group.md +71 -0
- package/src/ui/docs/content/button.md +60 -0
- package/src/ui/docs/content/calendar.md +62 -0
- package/src/ui/docs/content/card.md +49 -0
- package/src/ui/docs/content/carousel.md +85 -0
- package/src/ui/docs/content/chat.md +69 -0
- package/src/ui/docs/content/checkbox.md +75 -0
- package/src/ui/docs/content/cli.md +58 -0
- package/src/ui/docs/content/collapsible.md +64 -0
- package/src/ui/docs/content/command.md +56 -0
- package/src/ui/docs/content/composer.md +50 -0
- package/src/ui/docs/content/confirm.md +120 -0
- package/src/ui/docs/content/copyable.md +30 -0
- package/src/ui/docs/content/customization.md +110 -0
- package/src/ui/docs/content/cycle.md +34 -0
- package/src/ui/docs/content/data-state.md +47 -0
- package/src/ui/docs/content/data.md +99 -0
- package/src/ui/docs/content/dialog.md +60 -0
- package/src/ui/docs/content/drawer.md +55 -0
- package/src/ui/docs/content/empty.md +66 -0
- package/src/ui/docs/content/events.md +61 -0
- package/src/ui/docs/content/field.md +58 -0
- package/src/ui/docs/content/getting-started.md +109 -0
- package/src/ui/docs/content/icon-picker.md +51 -0
- package/src/ui/docs/content/input-group.md +78 -0
- package/src/ui/docs/content/input-otp.md +72 -0
- package/src/ui/docs/content/input.md +78 -0
- package/src/ui/docs/content/item.md +84 -0
- package/src/ui/docs/content/kbd.md +62 -0
- package/src/ui/docs/content/label.md +32 -0
- package/src/ui/docs/content/log.md +55 -0
- package/src/ui/docs/content/markdown.md +41 -0
- package/src/ui/docs/content/mcp.md +44 -0
- package/src/ui/docs/content/menu.md +114 -0
- package/src/ui/docs/content/microcopy.md +83 -0
- package/src/ui/docs/content/page.md +34 -0
- package/src/ui/docs/content/pagination.md +99 -0
- package/src/ui/docs/content/popover.md +49 -0
- package/src/ui/docs/content/progress.md +69 -0
- package/src/ui/docs/content/queue.md +62 -0
- package/src/ui/docs/content/radio-group.md +77 -0
- package/src/ui/docs/content/resizable.md +86 -0
- package/src/ui/docs/content/router.md +56 -0
- package/src/ui/docs/content/runtime.md +77 -0
- package/src/ui/docs/content/scheduler.md +66 -0
- package/src/ui/docs/content/scroll-area.md +89 -0
- package/src/ui/docs/content/section-shell.md +121 -0
- package/src/ui/docs/content/select.md +342 -0
- package/src/ui/docs/content/separator.md +33 -0
- package/src/ui/docs/content/sidebar.md +38 -0
- package/src/ui/docs/content/skeleton.md +34 -0
- package/src/ui/docs/content/slider.md +64 -0
- package/src/ui/docs/content/spinner.md +37 -0
- package/src/ui/docs/content/split.md +33 -0
- package/src/ui/docs/content/storage.md +69 -0
- package/src/ui/docs/content/switch.md +69 -0
- package/src/ui/docs/content/table.md +102 -0
- package/src/ui/docs/content/tabs.md +94 -0
- package/src/ui/docs/content/testing.md +89 -0
- package/src/ui/docs/content/textarea.md +30 -0
- package/src/ui/docs/content/toast.md +67 -0
- package/src/ui/docs/content/toggle-group.md +81 -0
- package/src/ui/docs/content/toggle.md +72 -0
- package/src/ui/docs/content/tokens.md +171 -0
- package/src/ui/docs/content/tooltip.md +50 -0
- package/src/ui/docs/content/truncate.md +37 -0
- package/src/ui/docs/content/ui.md +40 -0
- package/src/ui/docs/content/upgrading.md +48 -0
- package/src/ui/docs/doc-client.tsx +214 -0
- package/src/ui/docs/doc.tsx +301 -0
- package/src/ui/docs/folder.tsx +149 -0
- package/src/ui/docs/index.ts +21 -0
- package/src/ui/docs/markdown.tsx +130 -0
- package/src/ui/docs/md-raw.d.ts +4 -0
- package/src/ui/docs/plugin.ts +104 -0
- package/src/ui/docs/registry.tsx +424 -0
- package/src/ui/docs/standalone.tsx +107 -0
- package/src/ui/drivers/react.tsx +627 -0
- package/src/ui/index.ts +92 -0
- package/src/ui/lib/cn.ts +10 -0
- package/src/ui/lib/zod-pt-br.ts +38 -0
- package/src/ui/meta.ts +412 -0
- package/src/ui/react.tsx +235 -0
- package/src/ui/router.ts +96 -0
- package/src/ui/theme.css +234 -0
- package/src/vite/design.ts +652 -0
- package/src/vite/index.ts +8 -0
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* opusDesign() — plugin vite que sobe o runtime Opus DENTRO do dev server e
|
|
3
|
+
* serve `/api` in-process no preview de design.
|
|
4
|
+
*
|
|
5
|
+
* Por quê: o preview de design precisa ser AUTOSSUFICIENTE e isolado de prod.
|
|
6
|
+
* Antes, o dev server proxeava `/api` pra um backend real — o que vaza escrita
|
|
7
|
+
* pra prod e exige infra de pé pra desenhar tela. Com o plugin, `vite --mode
|
|
8
|
+
* design` monta o runtime (driver `nodeServer`) no próprio dev server: os
|
|
9
|
+
* `mockHandler` dos contratos respondem (`OPUS_MODE=design`), qualquer proxy
|
|
10
|
+
* configurado é DESLIGADO (prefixo ex-proxy sem cobertura responde 503 em
|
|
11
|
+
* envelope, nunca vaza), e o Maestro sonda `GET /__opus/design` pra saber que
|
|
12
|
+
* o preview está montado.
|
|
13
|
+
*
|
|
14
|
+
* Invariante: o runtime de design é request/response — reactions e schedules
|
|
15
|
+
* dos entries são IGNORADOS (não registrados), senão o `start()` exige
|
|
16
|
+
* eventbus/scheduler adapters que o design não tem por que montar.
|
|
17
|
+
*
|
|
18
|
+
* Uso (vite.config.ts do app):
|
|
19
|
+
* import { opusDesign } from '@softize/opus/vite'
|
|
20
|
+
* export default defineConfig({ plugins: [react(), opusDesign()] })
|
|
21
|
+
* // vite --mode design
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { readdirSync, readFileSync } from 'node:fs'
|
|
25
|
+
import type { IncomingMessage, ServerResponse } from 'node:http'
|
|
26
|
+
import path from 'node:path'
|
|
27
|
+
import type { Plugin, ViteDevServer } from 'vite'
|
|
28
|
+
import {
|
|
29
|
+
createRuntime,
|
|
30
|
+
error,
|
|
31
|
+
flattenDomain,
|
|
32
|
+
isDomainConfig,
|
|
33
|
+
isReaction,
|
|
34
|
+
isSchedule,
|
|
35
|
+
type ActionDef,
|
|
36
|
+
type AuthAdapter,
|
|
37
|
+
type ErrorInput,
|
|
38
|
+
type LoaderResolver,
|
|
39
|
+
type Runtime,
|
|
40
|
+
type RuntimeSetup,
|
|
41
|
+
type User,
|
|
42
|
+
} from '../core/index.ts'
|
|
43
|
+
import { mittEvents } from '../events/drivers/mitt.ts'
|
|
44
|
+
import { nodeServer, type NodeServerHandle } from '../server/drivers/node.ts'
|
|
45
|
+
import { serializeResult } from '../server/index.ts'
|
|
46
|
+
// Import relativo de propósito: `memStorage` é harness de teste, não superfície
|
|
47
|
+
// pública de runtime — o plugin o reusa sem promover export novo.
|
|
48
|
+
import { memStorage } from '../testing/index.ts'
|
|
49
|
+
|
|
50
|
+
// =============================================================================
|
|
51
|
+
// Options
|
|
52
|
+
// =============================================================================
|
|
53
|
+
|
|
54
|
+
export type DesignStubHandler = (
|
|
55
|
+
req: IncomingMessage,
|
|
56
|
+
res: ServerResponse,
|
|
57
|
+
) => void | Promise<void>
|
|
58
|
+
|
|
59
|
+
export interface OpusDesignOptions {
|
|
60
|
+
/**
|
|
61
|
+
* Entry(s) com os registráveis do design. Default './opus.config.ts'.
|
|
62
|
+
*
|
|
63
|
+
* Entry ESTÁTICO: módulo que default-exporta `{ domains: DomainConfig[] }`
|
|
64
|
+
* (shape do opus.config) OU um array de DomainConfig/ActionDef/contratos.
|
|
65
|
+
*
|
|
66
|
+
* Entry GLOB (contém `*`, ex.: 'src/domains/*\/contract.ts'): expandido a
|
|
67
|
+
* CADA rebuild — domínio novo entra no ar ao salvar o arquivo, sem editar
|
|
68
|
+
* entry nenhum. Dos módulos casados entram os exports (default e named) com
|
|
69
|
+
* shape de registrável; schemas/helpers exportados junto ficam de fora.
|
|
70
|
+
* Pode misturar estáticos e globs no array.
|
|
71
|
+
*/
|
|
72
|
+
entry?: string | string[]
|
|
73
|
+
|
|
74
|
+
/** Prefixo do path. Default '/api'. */
|
|
75
|
+
apiPrefix?: string
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Sessão fixa do preview. Default `{ id: 'design' }` — `execute()` exige
|
|
79
|
+
* user pra action não-public. `null` explícito simula anônimo.
|
|
80
|
+
*/
|
|
81
|
+
user?: User | null
|
|
82
|
+
|
|
83
|
+
/** Tenant da sessão de design. Default null. */
|
|
84
|
+
tenantId?: string | null
|
|
85
|
+
|
|
86
|
+
/** Resolvers da DSL `loads` — domínio bound com `loads` falha ANTES do mock sem resolver. */
|
|
87
|
+
loaders?: Record<string, LoaderResolver>
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Handlers raw por prefixo (podem streamar SSE). Mais-específico vence:
|
|
91
|
+
* um stub em '/api/auth' sombreia o '/api' do runtime.
|
|
92
|
+
*/
|
|
93
|
+
stubs?: Record<string, DesignStubHandler>
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Monta o /api também no dev NORMAL (handler real, sem OPUS_MODE).
|
|
97
|
+
* Default false — devBackend puro não é design (sem sonda, proxy intacto).
|
|
98
|
+
*/
|
|
99
|
+
devBackend?: boolean
|
|
100
|
+
|
|
101
|
+
/** Overrides do setup do runtime de design (adapters extras etc). */
|
|
102
|
+
runtime?: Partial<RuntimeSetup>
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// =============================================================================
|
|
106
|
+
// designAuth
|
|
107
|
+
// =============================================================================
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* AuthAdapter de design: ignora o request e devolve a sessão fixa do preview,
|
|
111
|
+
* com `can` liberado — autorização de verdade é preocupação do backend real.
|
|
112
|
+
*/
|
|
113
|
+
export function designAuth(
|
|
114
|
+
opts: { user?: User | null; tenantId?: string | null } = {},
|
|
115
|
+
): AuthAdapter {
|
|
116
|
+
const user = opts.user === undefined ? { id: 'design' } : opts.user
|
|
117
|
+
const tenantId = opts.tenantId ?? null
|
|
118
|
+
return {
|
|
119
|
+
name: 'design',
|
|
120
|
+
kind: 'auth',
|
|
121
|
+
resolveContext: async () => ({ user, tenantId, can: () => true }),
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// =============================================================================
|
|
126
|
+
// Plugin
|
|
127
|
+
// =============================================================================
|
|
128
|
+
|
|
129
|
+
const PROBE_PATH = '/__opus/design'
|
|
130
|
+
|
|
131
|
+
// Paths default que o mountEndpoints do driver monta — o middleware precisa
|
|
132
|
+
// deixá-los chegar no handler (moram fora do apiPrefix).
|
|
133
|
+
const RUNTIME_ENDPOINT_PATHS = new Set(['/health', '/ready', '/openapi.json'])
|
|
134
|
+
|
|
135
|
+
interface DesignState {
|
|
136
|
+
handle: NodeServerHandle
|
|
137
|
+
runtime: Runtime
|
|
138
|
+
actions: ActionDef[]
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function opusDesign(options: OpusDesignOptions = {}): Plugin {
|
|
142
|
+
const apiPrefix = options.apiPrefix ?? '/api'
|
|
143
|
+
const entries = Array.isArray(options.entry)
|
|
144
|
+
? options.entry
|
|
145
|
+
: [options.entry ?? './opus.config.ts']
|
|
146
|
+
const devBackend = options.devBackend === true
|
|
147
|
+
// Mais-específico primeiro: prefixo mais longo vence.
|
|
148
|
+
const stubEntries = Object.entries(options.stubs ?? {}).sort(
|
|
149
|
+
(a, b) => b[0].length - a[0].length,
|
|
150
|
+
)
|
|
151
|
+
const opusVersion = readOpusVersion()
|
|
152
|
+
|
|
153
|
+
let exProxyPrefixes: string[] = []
|
|
154
|
+
let current: Promise<DesignState> | undefined
|
|
155
|
+
let stale = false
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
name: 'opus-design',
|
|
159
|
+
|
|
160
|
+
apply(_config, env) {
|
|
161
|
+
return env.command === 'serve' && (env.mode === 'design' || devBackend)
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
config(config, env) {
|
|
165
|
+
// O pacote embarca TS source: externalizado no SSR, o node recusa TS sob
|
|
166
|
+
// node_modules — e o entry importa @softize/opus. noExternal faz o vite
|
|
167
|
+
// transformar o pacote no ssrLoadModule (vale pro dev normal com devBackend).
|
|
168
|
+
const ssr = (config.ssr ??= {})
|
|
169
|
+
if (ssr.noExternal !== true) {
|
|
170
|
+
const prev =
|
|
171
|
+
ssr.noExternal === undefined
|
|
172
|
+
? []
|
|
173
|
+
: Array.isArray(ssr.noExternal)
|
|
174
|
+
? ssr.noExternal
|
|
175
|
+
: [ssr.noExternal]
|
|
176
|
+
ssr.noExternal = [...prev, '@softize/opus']
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (env.mode !== 'design') return
|
|
180
|
+
// 1) Guarda os prefixos que iam pra proxy — sem cobertura viram 503 explícito.
|
|
181
|
+
exProxyPrefixes = Object.keys(config.server?.proxy ?? {})
|
|
182
|
+
// 2) Mata o proxy: fecha o vazamento preview→prod pra QUALQUER prefixo.
|
|
183
|
+
if (config.server?.proxy !== undefined) config.server.proxy = undefined
|
|
184
|
+
// 3) O execute() roteia handler×mockHandler lendo OPUS_MODE (isOpusDesignMode);
|
|
185
|
+
// com o runtime local, o preview fica autossuficiente.
|
|
186
|
+
process.env.OPUS_MODE = 'design'
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
configureServer(server) {
|
|
190
|
+
const isDesign = server.config.mode === 'design'
|
|
191
|
+
const logger = server.config.logger
|
|
192
|
+
// Um rebuild-por-404 por ciclo de staleness (rearma quando o watcher marca
|
|
193
|
+
// stale de novo) — cura o "domínio novo dá 404" sem virar rebuild-storm.
|
|
194
|
+
let unknownRetryDone = false
|
|
195
|
+
|
|
196
|
+
const boot = async (): Promise<DesignState> => {
|
|
197
|
+
const registrables: unknown[] = []
|
|
198
|
+
for (const entry of entries) {
|
|
199
|
+
if (isGlobPattern(entry)) {
|
|
200
|
+
registrables.push(...(await loadGlobEntry(server, entry)))
|
|
201
|
+
} else {
|
|
202
|
+
registrables.push(...(await loadEntry(server, entry)))
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const { actions, dropped } = collectActions(registrables)
|
|
206
|
+
|
|
207
|
+
const handle = nodeServer({ apiPrefix })
|
|
208
|
+
const runtime = createRuntime({
|
|
209
|
+
server: handle.adapter,
|
|
210
|
+
auth: designAuth({ user: options.user, tenantId: options.tenantId }),
|
|
211
|
+
eventBus: mittEvents(),
|
|
212
|
+
storage: memStorage(),
|
|
213
|
+
// SEM `data` de propósito: design não toca ctx.db — handler real sem
|
|
214
|
+
// mock falhando é o sinal certo de que falta mockar.
|
|
215
|
+
...options.runtime,
|
|
216
|
+
config: { env: 'development', dev: true, ...options.runtime?.config },
|
|
217
|
+
})
|
|
218
|
+
if (options.loaders !== undefined) runtime.registerLoaders(options.loaders)
|
|
219
|
+
runtime.register(actions)
|
|
220
|
+
|
|
221
|
+
if (dropped > 0) {
|
|
222
|
+
logger.info(
|
|
223
|
+
`[opus-design] ${dropped} reaction(s)/schedule(s) ignorada(s) — o runtime de design é request/response.`,
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
if (isDesign) {
|
|
227
|
+
for (const action of actions) {
|
|
228
|
+
if (action.mockHandler === undefined) {
|
|
229
|
+
logger.warn(
|
|
230
|
+
`[opus-design] Action "${action.name}" sem mockHandler — em design cai no handler real e pode falhar sem adapters.`,
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
await runtime.start()
|
|
237
|
+
handle.adapter.mountEndpoints({ health: true, ready: true, openapi: true })
|
|
238
|
+
return { handle, runtime, actions }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Promise-mutex: requests em voo aguardam o MESMO boot; rebuild (stale)
|
|
242
|
+
// descarta o runtime anterior antes de reconstruir — register/start não
|
|
243
|
+
// são re-entrantes.
|
|
244
|
+
const ensureBoot = (): Promise<DesignState> => {
|
|
245
|
+
if (current !== undefined && !stale) return current
|
|
246
|
+
stale = false
|
|
247
|
+
const previous = current
|
|
248
|
+
current = (async () => {
|
|
249
|
+
if (previous !== undefined) {
|
|
250
|
+
const old = await previous.catch(() => undefined)
|
|
251
|
+
if (old !== undefined) await old.runtime.dispose().catch(() => undefined)
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
return await boot()
|
|
255
|
+
} catch (err) {
|
|
256
|
+
// Falha fica retriable: a próxima request tenta reconstruir.
|
|
257
|
+
stale = true
|
|
258
|
+
logger.error(`[opus-design] Boot do runtime de design falhou: ${String(err)}`)
|
|
259
|
+
throw err
|
|
260
|
+
}
|
|
261
|
+
})()
|
|
262
|
+
current.catch(() => undefined)
|
|
263
|
+
return current
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Boot eager: o custo de carregar o entry acontece no subir do server,
|
|
267
|
+
// não na primeira request.
|
|
268
|
+
void ensureBoot().catch(() => undefined)
|
|
269
|
+
|
|
270
|
+
// HMR — dois critérios; a próxima request reconstrói do zero (lazy):
|
|
271
|
+
// change: arquivo com módulo no graph (ou o próprio entry) invalida;
|
|
272
|
+
// add/unlink: QUALQUER arquivo sob o root (fora node_modules/.git) —
|
|
273
|
+
// arquivo novo nunca importado não está no graph, e é exatamente ele
|
|
274
|
+
// que um entry glob precisa enxergar. Over-invalidation é barata.
|
|
275
|
+
const entryPaths = new Set(
|
|
276
|
+
entries
|
|
277
|
+
.filter((entry) => !isGlobPattern(entry))
|
|
278
|
+
.map((entry) => path.resolve(server.config.root, entry)),
|
|
279
|
+
)
|
|
280
|
+
// Specs dos entries glob. O watcher do vite só cobre o root, mas um glob
|
|
281
|
+
// pode apontar pra FORA dele (ex.: '../../services/*/contract.ts', caso GB):
|
|
282
|
+
// mandamos o chokidar observar o PREFIXO ESTÁTICO de cada glob — o mesmo que
|
|
283
|
+
// o expandGlob anda, nunca o mundo — pra change/unlink lá também invalidar.
|
|
284
|
+
const globSpecs = entries
|
|
285
|
+
.filter(isGlobPattern)
|
|
286
|
+
.map((pattern) => globSpec(server.config.root, pattern))
|
|
287
|
+
for (const { prefixDir } of globSpecs) server.watcher.add(prefixDir)
|
|
288
|
+
const matchesGlob = (file: string): boolean => {
|
|
289
|
+
const rel = path.relative(server.config.root, file).split(path.sep).join('/')
|
|
290
|
+
return globSpecs.some((spec) => spec.re.test(rel))
|
|
291
|
+
}
|
|
292
|
+
const markStale = (): void => {
|
|
293
|
+
if (current === undefined) return
|
|
294
|
+
stale = true
|
|
295
|
+
unknownRetryDone = false
|
|
296
|
+
}
|
|
297
|
+
const onChange = (file: string): void => {
|
|
298
|
+
const mods = server.moduleGraph.getModulesByFile(file)
|
|
299
|
+
const known = (mods !== undefined && mods.size > 0) || entryPaths.has(file)
|
|
300
|
+
// Um mock casando um glob pra fora do root pode não estar no module graph —
|
|
301
|
+
// o match do glob é o sinal (editar o mock invalida o runtime de design).
|
|
302
|
+
if (!known && !matchesGlob(file)) return
|
|
303
|
+
markStale()
|
|
304
|
+
}
|
|
305
|
+
const onAddOrUnlink = (file: string): void => {
|
|
306
|
+
// Casou um glob (in ou out do root): invalida — unlink de contrato some do
|
|
307
|
+
// rebuild seguinte (sem action fantasma); add entra. Precede a checagem de
|
|
308
|
+
// root, que rejeita `../` — senão a remoção lá fora nunca invalidaria.
|
|
309
|
+
if (matchesGlob(file)) {
|
|
310
|
+
markStale()
|
|
311
|
+
return
|
|
312
|
+
}
|
|
313
|
+
const rel = path.relative(server.config.root, file)
|
|
314
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) return
|
|
315
|
+
const segments = rel.split(path.sep)
|
|
316
|
+
if (segments.includes('node_modules') || segments.includes('.git')) return
|
|
317
|
+
markStale()
|
|
318
|
+
}
|
|
319
|
+
server.watcher.on('change', onChange)
|
|
320
|
+
server.watcher.on('add', onAddOrUnlink)
|
|
321
|
+
server.watcher.on('unlink', onAddOrUnlink)
|
|
322
|
+
|
|
323
|
+
const respondProbe = async (res: ServerResponse): Promise<void> => {
|
|
324
|
+
const payload: Record<string, unknown> = {
|
|
325
|
+
design: true,
|
|
326
|
+
opus: opusVersion,
|
|
327
|
+
stubs: stubEntries.map(([prefix]) => ({ prefix })),
|
|
328
|
+
}
|
|
329
|
+
try {
|
|
330
|
+
const state = await ensureBoot()
|
|
331
|
+
const unmocked = state.actions.filter((a) => a.mockHandler === undefined)
|
|
332
|
+
payload.runtime = {
|
|
333
|
+
prefix: apiPrefix,
|
|
334
|
+
actions: state.actions.length,
|
|
335
|
+
mocked: state.actions.length - unmocked.length,
|
|
336
|
+
unmocked: unmocked.map((a) => a.name),
|
|
337
|
+
}
|
|
338
|
+
} catch (err) {
|
|
339
|
+
payload.error = String(err)
|
|
340
|
+
}
|
|
341
|
+
res.statusCode = 200
|
|
342
|
+
res.setHeader('content-type', 'application/json; charset=utf-8')
|
|
343
|
+
res.setHeader('cache-control', 'no-store')
|
|
344
|
+
res.end(JSON.stringify(payload))
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Registrado DIRETO no body do hook: no vite 5/6 esses middlewares rodam
|
|
348
|
+
// antes do proxy interno e do transform.
|
|
349
|
+
server.middlewares.use((req, res, next) => {
|
|
350
|
+
const pathname = new URL(req.url ?? '/', 'http://localhost').pathname
|
|
351
|
+
|
|
352
|
+
// 1. Stubs primeiro (mais-específico primeiro) — um stub em /api/auth
|
|
353
|
+
// sombreia o /api do runtime.
|
|
354
|
+
for (const [prefix, fn] of stubEntries) {
|
|
355
|
+
if (matchesPrefix(pathname, prefix)) {
|
|
356
|
+
if (isDesign) res.setHeader('x-opus-design', '1')
|
|
357
|
+
try {
|
|
358
|
+
Promise.resolve(fn(req, res)).catch(next)
|
|
359
|
+
} catch (err) {
|
|
360
|
+
next(err)
|
|
361
|
+
}
|
|
362
|
+
return
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// 2. Protocolo: /api + endpoints do runtime. Aguarda o boot.
|
|
367
|
+
if (matchesPrefix(pathname, apiPrefix) || RUNTIME_ENDPOINT_PATHS.has(pathname)) {
|
|
368
|
+
void (async () => {
|
|
369
|
+
let state: DesignState
|
|
370
|
+
try {
|
|
371
|
+
state = await ensureBoot()
|
|
372
|
+
} catch (err) {
|
|
373
|
+
sendEnvelope(res, 503, {
|
|
374
|
+
code: 'design.boot_failed',
|
|
375
|
+
category: 'dependency',
|
|
376
|
+
message: `Design runtime failed to boot: ${String(err)}`,
|
|
377
|
+
})
|
|
378
|
+
return
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const method = req.method ?? 'GET'
|
|
382
|
+
if (matchesPrefix(pathname, apiPrefix) && !state.handle.hasRoute(method, pathname)) {
|
|
383
|
+
// 404 que se cura: action desconhecida pode ser só staleness que o
|
|
384
|
+
// watcher não viu (domínio recém-criado). Força UM rebuild por
|
|
385
|
+
// ciclo (mutex = ensureBoot) e re-tenta o match uma vez.
|
|
386
|
+
if (!unknownRetryDone) {
|
|
387
|
+
unknownRetryDone = true
|
|
388
|
+
stale = true
|
|
389
|
+
}
|
|
390
|
+
state = (await ensureBoot().catch(() => undefined)) ?? state
|
|
391
|
+
if (!state.handle.hasRoute(method, pathname)) {
|
|
392
|
+
sendEnvelope(res, 404, {
|
|
393
|
+
code: 'design.action_not_found',
|
|
394
|
+
category: 'not_found',
|
|
395
|
+
message:
|
|
396
|
+
`Rota ${method} ${pathname} não registrada no runtime do opusDesign — ` +
|
|
397
|
+
`confira se o contrato está exportado pelo entry (${entries.join(', ')}); ` +
|
|
398
|
+
`domínio novo exige registro lá (ou use entry glob).`,
|
|
399
|
+
})
|
|
400
|
+
return
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (isDesign) res.setHeader('x-opus-design', '1')
|
|
405
|
+
state.handle.handler(req, res, next)
|
|
406
|
+
})()
|
|
407
|
+
return
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// 3. Sonda — o contrato com o Maestro (mínimo garantido: design true).
|
|
411
|
+
if (isDesign && req.method === 'GET' && pathname === PROBE_PATH) {
|
|
412
|
+
void respondProbe(res)
|
|
413
|
+
return
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// 4. Prefixo ex-proxy sem cobertura: falha explícita em envelope,
|
|
417
|
+
// nunca vazamento pra prod.
|
|
418
|
+
if (isDesign) {
|
|
419
|
+
const orphan = exProxyPrefixes.find((p) => matchesPrefix(pathname, p))
|
|
420
|
+
if (orphan !== undefined) {
|
|
421
|
+
sendEnvelope(res, 503, {
|
|
422
|
+
code: 'design.no_backend',
|
|
423
|
+
category: 'dependency',
|
|
424
|
+
message: `No backend for "${orphan}" in design mode — add a stub or serve it from the opus runtime`,
|
|
425
|
+
})
|
|
426
|
+
return
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
next()
|
|
431
|
+
})
|
|
432
|
+
},
|
|
433
|
+
|
|
434
|
+
async closeBundle() {
|
|
435
|
+
const state = current === undefined ? undefined : await current.catch(() => undefined)
|
|
436
|
+
current = undefined
|
|
437
|
+
if (state !== undefined) await state.runtime.dispose().catch(() => undefined)
|
|
438
|
+
},
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// =============================================================================
|
|
443
|
+
// Entry loading
|
|
444
|
+
// =============================================================================
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Carrega um entry pelo pipeline SSR do vite — respeita aliases e TS do app.
|
|
448
|
+
* Isolado nesta função de propósito: no vite 7 o sucessor é
|
|
449
|
+
* `server.environments.ssr` (module runner); quando o piso de versão subir,
|
|
450
|
+
* a troca acontece só aqui.
|
|
451
|
+
*/
|
|
452
|
+
async function loadEntry(server: ViteDevServer, entry: string): Promise<unknown[]> {
|
|
453
|
+
const mod = (await server.ssrLoadModule(normalizeEntryUrl(entry))) as Record<string, unknown>
|
|
454
|
+
const root = mod.default ?? mod.config
|
|
455
|
+
if (Array.isArray(root)) return root
|
|
456
|
+
if (
|
|
457
|
+
typeof root === 'object' &&
|
|
458
|
+
root !== null &&
|
|
459
|
+
Array.isArray((root as { domains?: unknown }).domains)
|
|
460
|
+
) {
|
|
461
|
+
return (root as { domains: unknown[] }).domains
|
|
462
|
+
}
|
|
463
|
+
if (isDomainConfig(root)) return [root]
|
|
464
|
+
throw error({
|
|
465
|
+
code: 'design.invalid_entry',
|
|
466
|
+
category: 'internal',
|
|
467
|
+
message: `Entry "${entry}" must default-export { domains: [...] } or an array of registrables`,
|
|
468
|
+
})
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function normalizeEntryUrl(entry: string): string {
|
|
472
|
+
if (path.isAbsolute(entry)) return entry
|
|
473
|
+
return `/${entry.replace(/^\.\//, '')}`
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function isGlobPattern(entry: string): boolean {
|
|
477
|
+
return entry.includes('*')
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Entry por glob: expande o padrão A CADA boot — domínio novo entra no rebuild
|
|
482
|
+
* seguinte sem editar entry nenhum (o watcher de `add` já invalida na criação
|
|
483
|
+
* do arquivo). Cada match carrega pelo pipeline SSR; dos exports entram só os
|
|
484
|
+
* com shape de registrável (schemas/helpers exportados junto ficam de fora).
|
|
485
|
+
*/
|
|
486
|
+
async function loadGlobEntry(server: ViteDevServer, pattern: string): Promise<unknown[]> {
|
|
487
|
+
const out = new Set<unknown>()
|
|
488
|
+
for (const file of expandGlob(server.config.root, pattern)) {
|
|
489
|
+
const mod = (await server.ssrLoadModule(file)) as Record<string, unknown>
|
|
490
|
+
for (const item of registrablesOfModule(mod)) out.add(item)
|
|
491
|
+
}
|
|
492
|
+
return [...out]
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Matcher próprio de propósito (sem dep nova): `*` casa um segmento (sem '/'),
|
|
497
|
+
* `**` casa qualquer profundidade. Cobre o entry por pasta de domínio; padrão
|
|
498
|
+
* exótico não suportado = aponte entries estáticos.
|
|
499
|
+
*/
|
|
500
|
+
function globToRegExp(pattern: string): RegExp {
|
|
501
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
502
|
+
// `\u0000` como placeholder do `**`: nunca ocorre num path real.
|
|
503
|
+
const body = escaped
|
|
504
|
+
.replace(/\*\*/g, '\u0000')
|
|
505
|
+
.replace(/\*/g, '[^/]*')
|
|
506
|
+
.replace(/\u0000/g, '.*')
|
|
507
|
+
return new RegExp(`^${body}$`)
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
interface GlobSpec {
|
|
511
|
+
/** Regex que casa o path RELATIVO ao root (posix), inclusive `../` pra fora. */
|
|
512
|
+
re: RegExp
|
|
513
|
+
/** Prefixo estático do padrão (absoluto): `path.join(root, ...estáticos)`. É o
|
|
514
|
+
* ponto de partida do walk E o caminho que o watcher observa. */
|
|
515
|
+
prefixDir: string
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* O par (regex, prefixo estático) de um glob relativo ao `root`. Isolado pra ser
|
|
520
|
+
* reusado: o `expandGlob` anda a partir do prefixo, e o `configureServer` manda o
|
|
521
|
+
* chokidar observar o MESMO prefixo (globs pra fora do root inclusos) — um cálculo
|
|
522
|
+
* só decide o que carregar E o que vigiar, sem observar o mundo.
|
|
523
|
+
*/
|
|
524
|
+
function globSpec(root: string, pattern: string): GlobSpec {
|
|
525
|
+
const normalized = pattern.replace(/^\.\//, '').replace(/^\/+/, '')
|
|
526
|
+
const re = globToRegExp(normalized)
|
|
527
|
+
const staticSegments: string[] = []
|
|
528
|
+
for (const segment of normalized.split('/')) {
|
|
529
|
+
if (segment.includes('*')) break
|
|
530
|
+
staticSegments.push(segment)
|
|
531
|
+
}
|
|
532
|
+
return { re, prefixDir: path.join(root, ...staticSegments) }
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function expandGlob(root: string, pattern: string): string[] {
|
|
536
|
+
const { re, prefixDir } = globSpec(root, pattern)
|
|
537
|
+
const matches: string[] = []
|
|
538
|
+
const walk = (dir: string): void => {
|
|
539
|
+
let dirents
|
|
540
|
+
try {
|
|
541
|
+
dirents = readdirSync(dir, { withFileTypes: true })
|
|
542
|
+
} catch {
|
|
543
|
+
return
|
|
544
|
+
}
|
|
545
|
+
for (const dirent of dirents) {
|
|
546
|
+
if (dirent.name === 'node_modules' || dirent.name === '.git') continue
|
|
547
|
+
const full = path.join(dir, dirent.name)
|
|
548
|
+
if (dirent.isDirectory()) {
|
|
549
|
+
walk(full)
|
|
550
|
+
} else if (re.test(path.relative(root, full).split(path.sep).join('/'))) {
|
|
551
|
+
matches.push(full)
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
walk(prefixDir)
|
|
556
|
+
return matches.sort()
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const ACTION_KINDS = new Set(['simple', 'form', 'list', 'view'])
|
|
560
|
+
|
|
561
|
+
/** Registrável por shape: domínio, reaction/schedule (contados e dropados no
|
|
562
|
+
* collect) ou action/contrato (`name` + `kind` canônico). */
|
|
563
|
+
function isRegistrable(value: unknown): boolean {
|
|
564
|
+
if (typeof value !== 'object' || value === null) return false
|
|
565
|
+
if (isDomainConfig(value) || isReaction(value) || isSchedule(value)) return true
|
|
566
|
+
const candidate = value as { name?: unknown; kind?: unknown }
|
|
567
|
+
return (
|
|
568
|
+
typeof candidate.name === 'string' &&
|
|
569
|
+
typeof candidate.kind === 'string' &&
|
|
570
|
+
ACTION_KINDS.has(candidate.kind)
|
|
571
|
+
)
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/** Colhe registráveis de um módulo do glob: named e default; arrays e o shape
|
|
575
|
+
* `{ domains }` são desdobrados; o resto (schemas, helpers) fica de fora. */
|
|
576
|
+
function registrablesOfModule(mod: Record<string, unknown>): unknown[] {
|
|
577
|
+
const out = new Set<unknown>()
|
|
578
|
+
const add = (value: unknown): void => {
|
|
579
|
+
if (Array.isArray(value)) {
|
|
580
|
+
for (const item of value) add(item)
|
|
581
|
+
return
|
|
582
|
+
}
|
|
583
|
+
if (isRegistrable(value)) {
|
|
584
|
+
out.add(value)
|
|
585
|
+
return
|
|
586
|
+
}
|
|
587
|
+
if (typeof value === 'object' && value !== null && Array.isArray((value as { domains?: unknown }).domains)) {
|
|
588
|
+
add((value as { domains: unknown[] }).domains)
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
for (const value of Object.values(mod)) add(value)
|
|
592
|
+
return [...out]
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/** Achata os registráveis em actions; reactions/schedules ficam de fora (contados). */
|
|
596
|
+
function collectActions(items: unknown[]): { actions: ActionDef[]; dropped: number } {
|
|
597
|
+
const actions: ActionDef[] = []
|
|
598
|
+
let dropped = 0
|
|
599
|
+
for (const item of items) {
|
|
600
|
+
if (typeof item !== 'object' || item === null) continue
|
|
601
|
+
if (isDomainConfig(item)) {
|
|
602
|
+
const flat = flattenDomain(item)
|
|
603
|
+
actions.push(...flat.actions)
|
|
604
|
+
dropped += flat.reactions.length + flat.schedules.length
|
|
605
|
+
continue
|
|
606
|
+
}
|
|
607
|
+
if (isReaction(item) || isSchedule(item)) {
|
|
608
|
+
dropped += 1
|
|
609
|
+
continue
|
|
610
|
+
}
|
|
611
|
+
// ActionDef ou contrato (sem handler): em design o mockHandler responde.
|
|
612
|
+
actions.push(item as ActionDef)
|
|
613
|
+
}
|
|
614
|
+
return { actions, dropped }
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// =============================================================================
|
|
618
|
+
// Helpers
|
|
619
|
+
// =============================================================================
|
|
620
|
+
|
|
621
|
+
function matchesPrefix(pathname: string, prefix: string): boolean {
|
|
622
|
+
return pathname === prefix || pathname.startsWith(`${prefix}/`)
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/** Envelope opus com `meta` completo — o fetch client só reconhece envelope com `meta`. */
|
|
626
|
+
function sendEnvelope(res: ServerResponse, status: number, input: ErrorInput): void {
|
|
627
|
+
res.statusCode = status
|
|
628
|
+
res.setHeader('content-type', 'application/json; charset=utf-8')
|
|
629
|
+
res.end(
|
|
630
|
+
JSON.stringify(
|
|
631
|
+
serializeResult({
|
|
632
|
+
ok: false,
|
|
633
|
+
error: error(input),
|
|
634
|
+
meta: { actionId: crypto.randomUUID(), action: 'opus.design', durationMs: 0 },
|
|
635
|
+
}),
|
|
636
|
+
),
|
|
637
|
+
)
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function readOpusVersion(): string {
|
|
641
|
+
// `import.meta.url` sobrevive ao bundling do vite.config (o vite injeta a
|
|
642
|
+
// URL original do arquivo), então o package.json resolve do source real.
|
|
643
|
+
try {
|
|
644
|
+
const pkg = JSON.parse(
|
|
645
|
+
readFileSync(new URL('../../package.json', import.meta.url), 'utf8'),
|
|
646
|
+
) as { version?: string }
|
|
647
|
+
return pkg.version ?? '0.0.0'
|
|
648
|
+
/* v8 ignore next 3 — defensivo; o package.json viaja junto do source */
|
|
649
|
+
} catch {
|
|
650
|
+
return '0.0.0'
|
|
651
|
+
}
|
|
652
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @softize/opus/vite — plugins vite do Opus.
|
|
3
|
+
*
|
|
4
|
+
* `opusDesign()` sobe o runtime em modo design dentro do dev server (preview
|
|
5
|
+
* isolado de prod); `designAuth()` é o AuthAdapter de sessão fixa que ele usa.
|
|
6
|
+
*/
|
|
7
|
+
export { designAuth, opusDesign } from './design.ts'
|
|
8
|
+
export type { DesignStubHandler, OpusDesignOptions } from './design.ts'
|