@cosmicdrift/kumiko-dev-server 0.328.1 → 0.330.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/bin/kumiko-build.ts +3 -2
- package/bin/kumiko-dev.ts +1 -1
- package/bin/kumiko-init-deploy.ts +1 -1
- package/bin/kumiko-schema-check.ts +4 -1
- package/dist/build-server-bundle.d.ts +31 -0
- package/dist/build-server-bundle.js +282 -0
- package/dist/build.d.ts +2 -0
- package/dist/build.js +5 -0
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +9 -0
- package/dist/codegen/index.d.ts +4 -0
- package/dist/codegen/index.js +4 -0
- package/dist/codegen/render.d.ts +55 -0
- package/dist/codegen/render.js +273 -0
- package/dist/codegen/run-codegen.d.ts +20 -0
- package/dist/codegen/run-codegen.js +219 -0
- package/dist/codegen/scan-events.d.ts +76 -0
- package/dist/codegen/scan-events.js +447 -0
- package/dist/codegen/watch.d.ts +33 -0
- package/dist/codegen/watch.js +112 -0
- package/dist/compose-stacks.d.ts +60 -0
- package/dist/compose-stacks.js +127 -0
- package/dist/crash-tracker.d.ts +17 -0
- package/dist/crash-tracker.js +37 -0
- package/dist/create-kumiko-server.d.ts +195 -0
- package/dist/create-kumiko-server.js +1040 -0
- package/dist/env-schema.d.ts +15 -0
- package/dist/env-schema.js +60 -0
- package/dist/few-shot-corpus.d.ts +72 -0
- package/dist/few-shot-corpus.js +262 -0
- package/dist/framework-version.d.ts +1 -0
- package/dist/framework-version.js +31 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +33 -0
- package/dist/init-deploy-cli.d.ts +10 -0
- package/dist/init-deploy-cli.js +111 -0
- package/dist/kebab.d.ts +1 -0
- package/{src/kebab.ts → dist/kebab.js} +2 -3
- package/dist/run-dev-app.d.ts +174 -0
- package/dist/run-dev-app.js +403 -0
- package/dist/scaffold-app-feature.d.ts +16 -0
- package/dist/scaffold-app-feature.js +240 -0
- package/dist/scaffold-app.d.ts +45 -0
- package/dist/scaffold-app.js +852 -0
- package/dist/scaffold-demo-tasks.d.ts +8 -0
- package/{src/scaffold-demo-tasks.ts → dist/scaffold-demo-tasks.js} +69 -98
- package/dist/scaffold-deploy.d.ts +92 -0
- package/dist/scaffold-deploy.js +254 -0
- package/dist/scaffold-feature.d.ts +26 -0
- package/dist/scaffold-feature.js +163 -0
- package/dist/schema-apply.d.ts +11 -0
- package/dist/schema-apply.js +95 -0
- package/dist/schema-check-core.d.ts +2 -0
- package/{src/schema-check-core.ts → dist/schema-check-core.js} +8 -10
- package/dist/schema-tables-auth-mode.d.ts +2 -0
- package/{src/schema-tables-auth-mode.ts → dist/schema-tables-auth-mode.js} +2 -10
- package/dist/schema-tables-minimal.d.ts +2 -0
- package/{src/schema-tables-minimal.ts → dist/schema-tables-minimal.js} +2 -11
- package/dist/setup-test-stack-from-features.d.ts +11 -0
- package/dist/setup-test-stack-from-features.js +42 -0
- package/dist/welcome-banner.d.ts +15 -0
- package/dist/welcome-banner.js +27 -0
- package/package.json +71 -24
- package/src/changes.json +6 -0
- package/src/__tests__/build-prod-bundle.integration.test.ts +0 -282
- package/src/__tests__/build-server-bundle.test.ts +0 -228
- package/src/__tests__/classify-change.test.ts +0 -87
- package/src/__tests__/compose-stacks.test.ts +0 -273
- package/src/__tests__/config-resolver-default.integration.test.ts +0 -165
- package/src/__tests__/crash-tracker.test.ts +0 -89
- package/src/__tests__/create-kumiko-server-errors.test.ts +0 -54
- package/src/__tests__/create-kumiko-server-options.test.ts +0 -33
- package/src/__tests__/create-kumiko-server.integration.test.ts +0 -1007
- package/src/__tests__/discover-format.test.ts +0 -32
- package/src/__tests__/env-schema.integration.test.ts +0 -248
- package/src/__tests__/env-schema.test.ts +0 -132
- package/src/__tests__/few-shot-corpus.test.ts +0 -316
- package/src/__tests__/framework-version.test.ts +0 -45
- package/src/__tests__/init-deploy-cli.test.ts +0 -113
- package/src/__tests__/kumiko-schema-check.test.ts +0 -56
- package/src/__tests__/merge-extra-context.test.ts +0 -67
- package/src/__tests__/resolve-stylesheet.test.ts +0 -132
- package/src/__tests__/run-dev-app-boot-validation.test.ts +0 -38
- package/src/__tests__/run-dev-app.integration.test.ts +0 -290
- package/src/__tests__/runtime-classification.test.ts +0 -26
- package/src/__tests__/saas-identity-wire.integration.test.ts +0 -427
- package/src/__tests__/scaffold-app-feature.test.ts +0 -216
- package/src/__tests__/scaffold-app.test.ts +0 -630
- package/src/__tests__/scaffold-demo-tasks.test.ts +0 -47
- package/src/__tests__/scaffold-deploy.test.ts +0 -464
- package/src/__tests__/scaffold-dev-cycle.integration.test.ts +0 -109
- package/src/__tests__/scaffold-feature.test.ts +0 -143
- package/src/__tests__/schema-apply.integration.test.ts +0 -191
- package/src/__tests__/setup-test-stack-from-features.integration.test.ts +0 -30
- package/src/__tests__/setup-test-stack-job-run-logger.integration.test.ts +0 -72
- package/src/__tests__/walkthrough.integration.test.ts +0 -140
- package/src/__tests__/welcome-banner.test.ts +0 -42
- package/src/build-server-bundle.ts +0 -346
- package/src/build.ts +0 -28
- package/src/codegen/__tests__/render-codegen.test.ts +0 -100
- package/src/codegen/__tests__/run-codegen.test.ts +0 -665
- package/src/codegen/__tests__/strict-mode-diagnostics.test.ts +0 -347
- package/src/codegen/__tests__/watch.test.ts +0 -222
- package/src/codegen/index.ts +0 -19
- package/src/codegen/render.ts +0 -305
- package/src/codegen/run-codegen.ts +0 -278
- package/src/codegen/scan-events.ts +0 -565
- package/src/codegen/watch.ts +0 -140
- package/src/compose-stacks.ts +0 -193
- package/src/crash-tracker.ts +0 -56
- package/src/create-kumiko-server.ts +0 -1397
- package/src/env-schema.ts +0 -72
- package/src/few-shot-corpus.ts +0 -370
- package/src/framework-version.ts +0 -39
- package/src/index.ts +0 -105
- package/src/init-deploy-cli.ts +0 -148
- package/src/run-dev-app.ts +0 -694
- package/src/scaffold-app-feature.ts +0 -283
- package/src/scaffold-app.ts +0 -1033
- package/src/scaffold-deploy.ts +0 -393
- package/src/scaffold-feature.ts +0 -226
- package/src/schema-apply.ts +0 -123
- package/src/setup-test-stack-from-features.ts +0 -80
- package/src/welcome-banner.ts +0 -43
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
// scanEvents — finds every `r.defineEvent(...)` call across the app's
|
|
2
|
+
// feature files and resolves both the event-name (string literal) AND
|
|
3
|
+
// the schema source needed to derive a TS-Type. The codegen pipeline
|
|
4
|
+
// turns these into a `KumikoEventTypeMap` augmentation, the "single
|
|
5
|
+
// source of truth" the local defineWriteHandler-wrapper binds against
|
|
6
|
+
// for cross-package strict-checking.
|
|
7
|
+
//
|
|
8
|
+
// Output is a flat list of "event entries" — qualified-name + everything
|
|
9
|
+
// needed to emit a `z.infer<typeof Schema>` line in the augmentation.
|
|
10
|
+
//
|
|
11
|
+
// Patterns we resolve to a strict-checked type:
|
|
12
|
+
// 1. r.defineEvent("name", schemaIdentifier)
|
|
13
|
+
// Position-form, named import. The cleanest case — Schema lives
|
|
14
|
+
// in events.ts, gets re-imported by the augmentation as `import
|
|
15
|
+
// type` and threaded through `z.infer<typeof Schema>`.
|
|
16
|
+
//
|
|
17
|
+
// 2. r.defineEvent({ name, schema })
|
|
18
|
+
// Object-form, otherwise identical to (1).
|
|
19
|
+
//
|
|
20
|
+
// 3. r.defineEvent(NAME_CONST.member, schema...)
|
|
21
|
+
// Computed name via `as const` object. We follow the property-
|
|
22
|
+
// access through ts-morph's symbol-resolver, find the literal
|
|
23
|
+
// member, treat it as the string from (1)/(2). Keeps recipes that
|
|
24
|
+
// centralise event-names in a constants module strict-able.
|
|
25
|
+
//
|
|
26
|
+
// 4. r.defineEvent(name..., z.object({ ... }))
|
|
27
|
+
// Inline schema (call-expression instead of an identifier). We
|
|
28
|
+
// extract the call-source-text and emit it into a co-generated
|
|
29
|
+
// `schemas.generated.ts` as a named `export const`, then point
|
|
30
|
+
// the augmentation at that named export. The original feature
|
|
31
|
+
// file keeps its inline schema for runtime validation; the
|
|
32
|
+
// generated schemas-file exists ONLY for type-inference (consumed
|
|
33
|
+
// via `import type`, erased at build).
|
|
34
|
+
//
|
|
35
|
+
// What we still skip with a warning:
|
|
36
|
+
// - Default- or namespace-imports of the schema identifier
|
|
37
|
+
// (`import S from "..."`, `import * as M from "..."`). Rare in
|
|
38
|
+
// real apps; signal-to-noise of supporting them isn't worth it.
|
|
39
|
+
// - Computed names whose const cannot be statically resolved
|
|
40
|
+
// (cross-package re-exports, dynamic property access).
|
|
41
|
+
import { readdirSync, statSync } from "node:fs";
|
|
42
|
+
import { join, relative, resolve, sep } from "node:path";
|
|
43
|
+
import { toKebab } from "@cosmicdrift/kumiko-framework/engine";
|
|
44
|
+
import { Project, SyntaxKind, } from "ts-morph";
|
|
45
|
+
/** Renders a `ScanWarning` for terminal output; omits the location prefix when the warning has no source line. */
|
|
46
|
+
export function formatScanWarning(warning) {
|
|
47
|
+
return warning.file !== undefined
|
|
48
|
+
? `${warning.file}:${warning.line} — ${warning.message}`
|
|
49
|
+
: warning.message;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Scant `appRoot/src/**` nach `r.defineEvent(...)`-Aufrufen und liefert
|
|
53
|
+
* eine deduplizierte Liste aus ScannedEvent. Doppelte qualifiedName
|
|
54
|
+
* landen mit einer Warnung in der Result — Codegen schreibt nur den
|
|
55
|
+
* ERSTEN, sodass das generated File compile-stabil bleibt.
|
|
56
|
+
*/
|
|
57
|
+
export function scanEvents(opts) {
|
|
58
|
+
const project = new Project({
|
|
59
|
+
skipAddingFilesFromTsConfig: true,
|
|
60
|
+
skipFileDependencyResolution: true,
|
|
61
|
+
});
|
|
62
|
+
const filesToScan = [];
|
|
63
|
+
collectTsFiles(join(opts.appRoot, "src"), filesToScan);
|
|
64
|
+
// Add ALL files to the project up-front. ts-morph's symbol-resolver
|
|
65
|
+
// needs to see the file that declares the const-object before it can
|
|
66
|
+
// follow `INVOICE_EVENTS.sent` to its string literal.
|
|
67
|
+
for (const filePath of filesToScan) {
|
|
68
|
+
project.addSourceFileAtPath(filePath);
|
|
69
|
+
}
|
|
70
|
+
const events = [];
|
|
71
|
+
const warnings = [];
|
|
72
|
+
for (const filePath of filesToScan) {
|
|
73
|
+
const sourceFile = project.getSourceFile(filePath);
|
|
74
|
+
if (!sourceFile)
|
|
75
|
+
continue;
|
|
76
|
+
scanFile(sourceFile, filePath, events, warnings);
|
|
77
|
+
}
|
|
78
|
+
return { events: dedupe(events, warnings), warnings };
|
|
79
|
+
}
|
|
80
|
+
// ============================================================================
|
|
81
|
+
// Internal — directory walk
|
|
82
|
+
// ============================================================================
|
|
83
|
+
const SKIP_SEGMENTS = new Set(["node_modules", ".kumiko", "dist", "dist-server", "__tests__"]);
|
|
84
|
+
function collectTsFiles(dir, out) {
|
|
85
|
+
let entries;
|
|
86
|
+
try {
|
|
87
|
+
entries = readdirSync(dir);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// skip: directory unreadable, nothing to scan there
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
for (const entry of entries) {
|
|
94
|
+
if (entry.startsWith("."))
|
|
95
|
+
continue;
|
|
96
|
+
if (SKIP_SEGMENTS.has(entry))
|
|
97
|
+
continue;
|
|
98
|
+
const full = join(dir, entry);
|
|
99
|
+
let stat;
|
|
100
|
+
try {
|
|
101
|
+
stat = statSync(full);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (stat.isDirectory()) {
|
|
107
|
+
collectTsFiles(full, out);
|
|
108
|
+
}
|
|
109
|
+
else if (stat.isFile() &&
|
|
110
|
+
(entry.endsWith(".ts") || entry.endsWith(".tsx")) &&
|
|
111
|
+
!entry.endsWith(".d.ts") &&
|
|
112
|
+
!entry.endsWith(".test.ts") &&
|
|
113
|
+
!entry.endsWith(".test.tsx")) {
|
|
114
|
+
out.push(full);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// ============================================================================
|
|
119
|
+
// Internal — per-file scan
|
|
120
|
+
// ============================================================================
|
|
121
|
+
function scanFile(sourceFile, filePath, events, warnings) {
|
|
122
|
+
// Find every defineFeature(...) — there may be multiple in factory-style
|
|
123
|
+
// packages (one factory function per feature). Each carries its own
|
|
124
|
+
// featureName (1st arg) + setup-arrow-function (2nd arg).
|
|
125
|
+
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
126
|
+
if (call.getExpression().getText() !== "defineFeature")
|
|
127
|
+
continue;
|
|
128
|
+
const featureName = readStringLiteral(call.getArguments()[0]);
|
|
129
|
+
if (!featureName)
|
|
130
|
+
continue;
|
|
131
|
+
const setup = call.getArguments()[1]?.asKind(SyntaxKind.ArrowFunction);
|
|
132
|
+
if (!setup)
|
|
133
|
+
continue;
|
|
134
|
+
const registrarParam = setup.getParameters()[0]?.getName();
|
|
135
|
+
if (!registrarParam)
|
|
136
|
+
continue;
|
|
137
|
+
for (const defCall of setup.getBody().getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
138
|
+
const expr = defCall.getExpression().asKind(SyntaxKind.PropertyAccessExpression);
|
|
139
|
+
if (!expr)
|
|
140
|
+
continue;
|
|
141
|
+
if (expr.getExpression().getText() !== registrarParam)
|
|
142
|
+
continue;
|
|
143
|
+
if (expr.getName() !== "defineEvent")
|
|
144
|
+
continue;
|
|
145
|
+
collectFromDefineEvent(defCall, sourceFile, featureName, filePath, events, warnings);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function collectFromDefineEvent(call, sourceFile, featureName, filePath, events, warnings) {
|
|
150
|
+
const parsed = parseDefineEventCall(call);
|
|
151
|
+
if (!parsed) {
|
|
152
|
+
warnings.push({
|
|
153
|
+
file: filePath,
|
|
154
|
+
line: call.getStartLineNumber(),
|
|
155
|
+
message: "r.defineEvent: cannot read event-name + schema statically",
|
|
156
|
+
});
|
|
157
|
+
// skip: defineEvent call shape not statically parseable, already recorded as warning
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
// Mirror the framework's `qn(toKebab(feature), "event", toKebab(name))`
|
|
161
|
+
// — the augmentation key MUST match what `r.defineEvent` writes at
|
|
162
|
+
// runtime. Without the kebab-step, `defineFeature("driverOrders")`
|
|
163
|
+
// would augment `driverOrders:event:*` while the runtime stream
|
|
164
|
+
// carries `driver-orders:event:*`, and strict-mode would reject every
|
|
165
|
+
// correct call.
|
|
166
|
+
const qualifiedName = `${toKebab(featureName)}:event:${toKebab(parsed.eventName)}`;
|
|
167
|
+
const schema = resolveSchemaSource(parsed.schemaNode, sourceFile, qualifiedName);
|
|
168
|
+
if (!schema) {
|
|
169
|
+
warnings.push({
|
|
170
|
+
file: filePath,
|
|
171
|
+
line: call.getStartLineNumber(),
|
|
172
|
+
message: `r.defineEvent("${parsed.eventName}"): schema "${parsed.schemaNode.getText()}" — not a named import nor an inline z.* call, skipped`,
|
|
173
|
+
});
|
|
174
|
+
// skip: schema source not resolvable, already recorded as warning
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
events.push({
|
|
178
|
+
qualifiedName,
|
|
179
|
+
schemaSource: schema,
|
|
180
|
+
featureFilePath: filePath,
|
|
181
|
+
source: { file: filePath, line: call.getStartLineNumber() },
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
function parseDefineEventCall(call) {
|
|
185
|
+
const args = call.getArguments();
|
|
186
|
+
const first = args[0];
|
|
187
|
+
if (!first)
|
|
188
|
+
return undefined;
|
|
189
|
+
// Object-form: r.defineEvent({ name, schema, version? })
|
|
190
|
+
const obj = first.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
191
|
+
if (obj && args.length === 1) {
|
|
192
|
+
const nameProp = obj
|
|
193
|
+
.getProperty("name")
|
|
194
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
195
|
+
?.getInitializer();
|
|
196
|
+
const eventName = nameProp ? resolveStringLiteralOrConst(nameProp) : undefined;
|
|
197
|
+
if (!eventName)
|
|
198
|
+
return undefined;
|
|
199
|
+
const schemaNode = obj
|
|
200
|
+
.getProperty("schema")
|
|
201
|
+
?.asKind(SyntaxKind.PropertyAssignment)
|
|
202
|
+
?.getInitializer();
|
|
203
|
+
if (!schemaNode)
|
|
204
|
+
return undefined;
|
|
205
|
+
return { eventName, schemaNode };
|
|
206
|
+
}
|
|
207
|
+
// Position-form: r.defineEvent(<name-source>, <schema-node>, ...)
|
|
208
|
+
const eventName = resolveStringLiteralOrConst(first);
|
|
209
|
+
const schemaNode = args[1];
|
|
210
|
+
if (!eventName || !schemaNode)
|
|
211
|
+
return undefined;
|
|
212
|
+
return { eventName, schemaNode };
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Resolves a node to a string literal — either it IS a string-literal,
|
|
216
|
+
* or it's a property-access on a `const X = { ... } as const` object
|
|
217
|
+
* whose member resolves to one. Returns undefined for anything else
|
|
218
|
+
* (template literals, dynamic property access, function calls).
|
|
219
|
+
*
|
|
220
|
+
* Cross-file resolution: we follow the named import that brings the
|
|
221
|
+
* receiver-identifier into scope, load the target file from the same
|
|
222
|
+
* ts-morph project, and look up the const-declaration there. This
|
|
223
|
+
* works WITHOUT a TypeChecker — the import-graph + AST is enough for
|
|
224
|
+
* the patterns the recipes/showcases use (`as const` objects with
|
|
225
|
+
* string-literal members).
|
|
226
|
+
*/
|
|
227
|
+
function resolveStringLiteralOrConst(node) {
|
|
228
|
+
// Direct string-literal — fast path.
|
|
229
|
+
const direct = node.asKind(SyntaxKind.StringLiteral);
|
|
230
|
+
if (direct)
|
|
231
|
+
return direct.getLiteralValue();
|
|
232
|
+
// PropertyAccessExpression: `INVOICE_EVENTS.sent` form.
|
|
233
|
+
const propAccess = node.asKind(SyntaxKind.PropertyAccessExpression);
|
|
234
|
+
if (propAccess)
|
|
235
|
+
return resolvePropertyAccessLiteral(propAccess);
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
function resolvePropertyAccessLiteral(propAccess) {
|
|
239
|
+
const receiver = propAccess.getExpression().asKind(SyntaxKind.Identifier);
|
|
240
|
+
if (!receiver)
|
|
241
|
+
return undefined;
|
|
242
|
+
const memberName = propAccess.getName();
|
|
243
|
+
const callerFile = propAccess.getSourceFile();
|
|
244
|
+
// Two paths to find the const-declaration:
|
|
245
|
+
// 1. Local: declared in the same file before the call.
|
|
246
|
+
// 2. Imported: a named import points at another file in the project;
|
|
247
|
+
// the const lives there as an `export const`.
|
|
248
|
+
const local = findConstObject(callerFile, receiver.getText());
|
|
249
|
+
if (local)
|
|
250
|
+
return readMemberLiteral(local, memberName);
|
|
251
|
+
for (const importDecl of callerFile.getImportDeclarations()) {
|
|
252
|
+
if (!matchesNamedImport(importDecl, receiver.getText()))
|
|
253
|
+
continue;
|
|
254
|
+
const targetFile = resolveImportedSourceFile(importDecl, callerFile);
|
|
255
|
+
if (!targetFile)
|
|
256
|
+
continue;
|
|
257
|
+
const remote = findConstObject(targetFile, receiver.getText());
|
|
258
|
+
if (remote) {
|
|
259
|
+
const literal = readMemberLiteral(remote, memberName);
|
|
260
|
+
if (literal !== undefined)
|
|
261
|
+
return literal;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Walk top-level statements for `const RECEIVER = { ... } [as const]`.
|
|
268
|
+
* Returns the inner object-literal if found.
|
|
269
|
+
*/
|
|
270
|
+
function findConstObject(sourceFile, receiverName) {
|
|
271
|
+
for (const stmt of sourceFile.getVariableStatements()) {
|
|
272
|
+
for (const decl of stmt.getDeclarations()) {
|
|
273
|
+
if (decl.getName() !== receiverName)
|
|
274
|
+
continue;
|
|
275
|
+
const init = unwrapAsConst(decl.getInitializer());
|
|
276
|
+
const objLit = init?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
277
|
+
if (objLit)
|
|
278
|
+
return objLit;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
function readMemberLiteral(objLitNode, memberName) {
|
|
284
|
+
const objLit = objLitNode.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
285
|
+
if (!objLit)
|
|
286
|
+
return undefined;
|
|
287
|
+
const prop = objLit.getProperty(memberName)?.asKind(SyntaxKind.PropertyAssignment);
|
|
288
|
+
const initLit = prop?.getInitializer()?.asKind(SyntaxKind.StringLiteral);
|
|
289
|
+
return initLit?.getLiteralValue();
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Resolve a relative import-specifier to a SourceFile already loaded
|
|
293
|
+
* in the project. Tries `<base>.ts` and `<base>/index.ts`. Returns
|
|
294
|
+
* undefined for npm-package specifiers (no local resolution needed —
|
|
295
|
+
* we don't follow anything beyond the app's own files).
|
|
296
|
+
*/
|
|
297
|
+
function resolveImportedSourceFile(importDecl, fromFile) {
|
|
298
|
+
const spec = importDecl.getModuleSpecifierValue();
|
|
299
|
+
if (!spec.startsWith("."))
|
|
300
|
+
return undefined;
|
|
301
|
+
const fromDir = fromFile.getDirectoryPath();
|
|
302
|
+
const project = fromFile.getProject();
|
|
303
|
+
const candidates = [`${spec}.ts`, `${spec}.tsx`, `${spec}/index.ts`, `${spec}/index.tsx`];
|
|
304
|
+
for (const cand of candidates) {
|
|
305
|
+
const abs = resolve(fromDir, cand);
|
|
306
|
+
const sf = project.getSourceFile(abs);
|
|
307
|
+
if (sf)
|
|
308
|
+
return sf;
|
|
309
|
+
}
|
|
310
|
+
return undefined;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* `{ x: 1 } as const` parses as `AsExpression > ObjectLiteralExpression`.
|
|
314
|
+
* Strip the AsExpression so the caller can hit the inner literal directly.
|
|
315
|
+
*/
|
|
316
|
+
function unwrapAsConst(node) {
|
|
317
|
+
if (!node)
|
|
318
|
+
return undefined;
|
|
319
|
+
const asExpr = node.asKind(SyntaxKind.AsExpression);
|
|
320
|
+
return asExpr ? asExpr.getExpression() : node;
|
|
321
|
+
}
|
|
322
|
+
function readStringLiteral(node) {
|
|
323
|
+
return node?.asKind(SyntaxKind.StringLiteral)?.getLiteralValue();
|
|
324
|
+
}
|
|
325
|
+
// ============================================================================
|
|
326
|
+
// Internal — resolve schema node to its source kind
|
|
327
|
+
// ============================================================================
|
|
328
|
+
function resolveSchemaSource(schemaNode, sourceFile, qualifiedName) {
|
|
329
|
+
// Named identifier: schema lives in another file as a named export.
|
|
330
|
+
const ident = schemaNode.asKind(SyntaxKind.Identifier);
|
|
331
|
+
if (ident) {
|
|
332
|
+
const importInfo = resolveSchemaImport(sourceFile, ident.getText());
|
|
333
|
+
if (!importInfo)
|
|
334
|
+
return undefined;
|
|
335
|
+
return {
|
|
336
|
+
kind: "imported",
|
|
337
|
+
schemaIdentifier: ident.getText(),
|
|
338
|
+
schemaModulePath: importInfo.moduleSpecifier,
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
// Inline call: r.defineEvent("name", z.object({...})).
|
|
342
|
+
// We accept any call-expression that LOOKS like a zod schema (cheap
|
|
343
|
+
// structural check on the `z.*` head). The exact ".object/.string/etc"
|
|
344
|
+
// doesn't matter — the codegen will replay the source text in the
|
|
345
|
+
// schemas-file, where TS resolves z.infer correctly.
|
|
346
|
+
const callExpr = schemaNode.asKind(SyntaxKind.CallExpression);
|
|
347
|
+
if (callExpr && looksLikeZodCall(callExpr)) {
|
|
348
|
+
return {
|
|
349
|
+
kind: "inline",
|
|
350
|
+
schemaSource: callExpr.getText(),
|
|
351
|
+
generatedConstName: qualifiedNameToConstName(qualifiedName),
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
return undefined;
|
|
355
|
+
}
|
|
356
|
+
function looksLikeZodCall(call) {
|
|
357
|
+
// Walk the callee head — `z.something(...)` or `z.something.foo(...)`,
|
|
358
|
+
// anything that traces back to an Identifier `z`. Conservative: we
|
|
359
|
+
// don't try to verify it's the actual zod-import (the runtime check
|
|
360
|
+
// happens through the schemas-file's `import { z } from "zod"` anyway,
|
|
361
|
+
// which fails loudly if the user's `z` is something else).
|
|
362
|
+
let cur = call.getExpression();
|
|
363
|
+
while (cur.asKind(SyntaxKind.PropertyAccessExpression) || cur.asKind(SyntaxKind.CallExpression)) {
|
|
364
|
+
const prop = cur.asKind(SyntaxKind.PropertyAccessExpression);
|
|
365
|
+
if (prop) {
|
|
366
|
+
cur = prop.getExpression();
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
const innerCall = cur.asKind(SyntaxKind.CallExpression);
|
|
370
|
+
if (innerCall) {
|
|
371
|
+
cur = innerCall.getExpression();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return cur.asKind(SyntaxKind.Identifier)?.getText() === "z";
|
|
375
|
+
}
|
|
376
|
+
function resolveSchemaImport(sourceFile, identifier) {
|
|
377
|
+
for (const importDecl of sourceFile.getImportDeclarations()) {
|
|
378
|
+
if (matchesNamedImport(importDecl, identifier)) {
|
|
379
|
+
return { moduleSpecifier: importDecl.getModuleSpecifierValue() };
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
384
|
+
function matchesNamedImport(importDecl, identifier) {
|
|
385
|
+
for (const named of importDecl.getNamedImports()) {
|
|
386
|
+
// Handles both `import { x }` and `import { x as y }` — we want the
|
|
387
|
+
// LOCAL alias (what's used in the call site), which matches the
|
|
388
|
+
// identifier the scanner extracted.
|
|
389
|
+
const localName = named.getAliasNode()?.getText() ?? named.getNameNode().getText();
|
|
390
|
+
if (localName === identifier)
|
|
391
|
+
return true;
|
|
392
|
+
}
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Stable identifier-safe rewrite of a qualifiedName like
|
|
397
|
+
* `pubsubOrders:event:order-placed` → `_kg_pubsubOrders__orderPlaced`.
|
|
398
|
+
* Used for the `export const` name in `schemas.generated.ts`. Same
|
|
399
|
+
* transform in scan + render keeps the two sides in sync.
|
|
400
|
+
*/
|
|
401
|
+
export function qualifiedNameToConstName(qualifiedName) {
|
|
402
|
+
// Drop the ":event:" infix — every entry has it, no information.
|
|
403
|
+
const withoutEventInfix = qualifiedName.replace(/:event:/g, "__");
|
|
404
|
+
// Replace remaining colons + dashes with `_` and camel-case after `_`
|
|
405
|
+
// so the output is identifier-legal AND visually parseable.
|
|
406
|
+
const sanitised = withoutEventInfix.replace(/[^A-Za-z0-9_]+(.?)/g, (_match, next) => next.toUpperCase());
|
|
407
|
+
return `_kg_${sanitised}`;
|
|
408
|
+
}
|
|
409
|
+
// ============================================================================
|
|
410
|
+
// Internal — module-specifier rewriting + dedup
|
|
411
|
+
// ============================================================================
|
|
412
|
+
/**
|
|
413
|
+
* Resolves the relative schema-import path from the feature-file's
|
|
414
|
+
* point-of-view to a path relative to `.kumiko/` (which is where the
|
|
415
|
+
* generated d.ts file lives). Workspace-package specifiers
|
|
416
|
+
* (`@cosmicdrift/kumiko-...`) are returned as-is.
|
|
417
|
+
*/
|
|
418
|
+
export function rewriteImportPath(schemaModulePath, featureFilePath, outputDirAbs) {
|
|
419
|
+
// Workspace / npm specifiers — pass through. Codegen output imports
|
|
420
|
+
// them by name.
|
|
421
|
+
if (!schemaModulePath.startsWith("."))
|
|
422
|
+
return schemaModulePath;
|
|
423
|
+
const featureDir = featureFilePath.substring(0, featureFilePath.lastIndexOf(sep));
|
|
424
|
+
const absoluteSchemaPath = resolve(featureDir, schemaModulePath);
|
|
425
|
+
const fromOutput = relative(outputDirAbs, absoluteSchemaPath);
|
|
426
|
+
// POSIX-Slash für TS-imports (auch auf Windows).
|
|
427
|
+
const normalised = fromOutput.split(sep).join("/");
|
|
428
|
+
// Strip .ts/.tsx — TS module specifiers don't carry the extension;
|
|
429
|
+
// resolve() didn't add one but a hand-written ".ts" should be removed.
|
|
430
|
+
return normalised.replace(/\.tsx?$/, "");
|
|
431
|
+
}
|
|
432
|
+
function dedupe(events, warnings) {
|
|
433
|
+
const seen = new Map();
|
|
434
|
+
for (const ev of events) {
|
|
435
|
+
const existing = seen.get(ev.qualifiedName);
|
|
436
|
+
if (existing) {
|
|
437
|
+
warnings.push({
|
|
438
|
+
file: ev.source.file,
|
|
439
|
+
line: ev.source.line,
|
|
440
|
+
message: `duplicate r.defineEvent("${ev.qualifiedName}") — first declared at ${existing.source.file}:${existing.source.line}, ignored here`,
|
|
441
|
+
});
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
seen.set(ev.qualifiedName, ev);
|
|
445
|
+
}
|
|
446
|
+
return [...seen.values()];
|
|
447
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { runCodegen } from "./run-codegen.js";
|
|
2
|
+
export type WatchOptions = {
|
|
3
|
+
/** App-Wurzel — gleiche Bedeutung wie für `runCodegen`. */
|
|
4
|
+
readonly appRoot: string;
|
|
5
|
+
/** Optional: alle registrierten Write-Handler-QNs. Durchgereicht an
|
|
6
|
+
* `runCodegen` für die Generierung der `WriteHandlerQn`-Union.
|
|
7
|
+
* Der Dev-Server extrahiert sie beim Boot aus den Features und
|
|
8
|
+
* reicht sie hier rein — der Watcher reicht sie bei jedem Re-Run
|
|
9
|
+
* an `runCodegen` weiter, damit der generated-Type nicht verschwindet. */
|
|
10
|
+
readonly handlerQns?: readonly string[];
|
|
11
|
+
/** Wartet diese Millisekunden zusätzliche Events ab, bevor codegen
|
|
12
|
+
* einmal fährt. 50ms catched typische save-bursts (Editor-Saves
|
|
13
|
+
* feuern oft 2-3 Events: temp-file → rename → cleanup), bleibt aber
|
|
14
|
+
* unmerklich für den User. */
|
|
15
|
+
readonly debounceMs?: number;
|
|
16
|
+
/** Callback nach jedem erfolgreichen Codegen-Pass. Default: stderr-
|
|
17
|
+
* Log mit event-count + warnings. Wer den Output strukturiert
|
|
18
|
+
* konsumieren will (Dev-Server-UI, IDE-Plugin), gibt einen eigenen
|
|
19
|
+
* Handler. */
|
|
20
|
+
readonly onResult?: (result: ReturnType<typeof runCodegen>) => void;
|
|
21
|
+
/** Callback bei runCodegen-Fehlern. Default: stderr-Warning. */
|
|
22
|
+
readonly onError?: (err: unknown) => void;
|
|
23
|
+
};
|
|
24
|
+
export type WatchHandle = {
|
|
25
|
+
/** Beendet den Watcher. Idempotent — mehrfacher Aufruf ist no-op. */
|
|
26
|
+
readonly close: () => void;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Startet den Watcher. Beim Boot fährt einmalig `runCodegen` (sodass
|
|
30
|
+
* die generated Files sofort frisch sind), dann hängt sich an `fs.watch`
|
|
31
|
+
* und re-runs bei file-changes mit Debounce.
|
|
32
|
+
*/
|
|
33
|
+
export declare function watchAndRegenerate(opts: WatchOptions): WatchHandle;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// watchAndRegenerate — file-watcher der bei jeder TS-Änderung unter
|
|
2
|
+
// `<appRoot>/src/**` `runCodegen` neu fährt. Lebendige IDE-DX: User
|
|
3
|
+
// editiert ein r.defineEvent, drückt save, das `.kumiko/types.generated.d.ts`
|
|
4
|
+
// ist innerhalb von ~50ms aktualisiert, der TS-Sprachserver merkt es,
|
|
5
|
+
// neue Auto-Complete-Vorschläge erscheinen ohne Server-Restart.
|
|
6
|
+
//
|
|
7
|
+
// Implementation: node:fs.watch (recursive) auf `<appRoot>/src/`.
|
|
8
|
+
// Debounced damit ein batch-edit (z.B. find+replace via sed) nicht
|
|
9
|
+
// 50× hintereinander feuert. Idempotent — runCodegen schreibt nur bei
|
|
10
|
+
// echter Änderung, der Watcher kann ohne Schaden über-ruft werden.
|
|
11
|
+
//
|
|
12
|
+
// Nicht-Ziele: kein Watcher auf `node_modules`, `.kumiko`, `dist*`,
|
|
13
|
+
// `__tests__` (gleicher SKIP-Set wie scan-events). fs.watch's
|
|
14
|
+
// recursive-Mode liefert events für jede Subdir; wir filtern im
|
|
15
|
+
// Callback.
|
|
16
|
+
import { watch } from "node:fs";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { runCodegen } from "./run-codegen.js";
|
|
19
|
+
import { formatScanWarning } from "./scan-events.js";
|
|
20
|
+
const SKIP_SUBSTRINGS = ["/node_modules/", "/.kumiko/", "/dist/", "/dist-server/", "/__tests__/"];
|
|
21
|
+
const DEFAULT_DEBOUNCE_MS = 50;
|
|
22
|
+
/**
|
|
23
|
+
* Startet den Watcher. Beim Boot fährt einmalig `runCodegen` (sodass
|
|
24
|
+
* die generated Files sofort frisch sind), dann hängt sich an `fs.watch`
|
|
25
|
+
* und re-runs bei file-changes mit Debounce.
|
|
26
|
+
*/
|
|
27
|
+
export function watchAndRegenerate(opts) {
|
|
28
|
+
const debounceMs = opts.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
29
|
+
const srcDir = join(opts.appRoot, "src");
|
|
30
|
+
let timer;
|
|
31
|
+
let watcher;
|
|
32
|
+
let closed = false;
|
|
33
|
+
const fire = () => {
|
|
34
|
+
try {
|
|
35
|
+
const result = runCodegen({
|
|
36
|
+
appRoot: opts.appRoot,
|
|
37
|
+
...(opts.handlerQns !== undefined && { handlerQns: opts.handlerQns }),
|
|
38
|
+
});
|
|
39
|
+
if (opts.onResult) {
|
|
40
|
+
opts.onResult(result);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
if (result.warnings.length > 0) {
|
|
44
|
+
for (const w of result.warnings) {
|
|
45
|
+
// biome-ignore lint/suspicious/noConsole: codegen-watcher logs to terminal
|
|
46
|
+
console.warn(`[codegen] ${formatScanWarning(w)}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
if (opts.onError)
|
|
53
|
+
opts.onError(err);
|
|
54
|
+
else {
|
|
55
|
+
// biome-ignore lint/suspicious/noConsole: codegen-watcher logs to terminal
|
|
56
|
+
console.warn(`[codegen] regenerate failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
// Initial run — gleiches Verhalten wie wenn ein file-change kam, aber
|
|
61
|
+
// ohne Debounce (User wartet auf den ersten codegen).
|
|
62
|
+
fire();
|
|
63
|
+
try {
|
|
64
|
+
watcher = watch(srcDir, { recursive: true }, (_eventType, filename) => {
|
|
65
|
+
// skip: watcher closed or no filename reported, nothing to act on
|
|
66
|
+
if (closed || !filename)
|
|
67
|
+
return;
|
|
68
|
+
// node liefert filename relativ zu srcDir, kann aber posix oder
|
|
69
|
+
// windows-style separators haben. Wir prüfen substring-tolerant.
|
|
70
|
+
const normalised = `/${filename.toString().replace(/\\/g, "/")}`;
|
|
71
|
+
// skip: path matches an excluded segment (node_modules/dist/etc.)
|
|
72
|
+
if (SKIP_SUBSTRINGS.some((seg) => normalised.includes(seg)))
|
|
73
|
+
return;
|
|
74
|
+
// skip: Nur .ts/.tsx interessieren — alles andere (CSS, MD, JSON) hat
|
|
75
|
+
// keinen Einfluss auf r.defineEvent-Calls.
|
|
76
|
+
if (!normalised.endsWith(".ts") && !normalised.endsWith(".tsx"))
|
|
77
|
+
return;
|
|
78
|
+
// skip: .d.ts ausgenommen — die kommen meistens vom codegen selbst.
|
|
79
|
+
if (normalised.endsWith(".d.ts"))
|
|
80
|
+
return;
|
|
81
|
+
// skip: .test.ts/.test.tsx ausgenommen — Tests definieren keine
|
|
82
|
+
// Production-Features.
|
|
83
|
+
if (normalised.endsWith(".test.ts") || normalised.endsWith(".test.tsx"))
|
|
84
|
+
return;
|
|
85
|
+
if (timer)
|
|
86
|
+
clearTimeout(timer);
|
|
87
|
+
timer = setTimeout(fire, debounceMs);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
// Watch failed (z.B. fs nicht recursive-fähig auf Linux ohne
|
|
92
|
+
// patches) — degraded mode: codegen läuft nur beim initial-call.
|
|
93
|
+
// User kriegt keine live-updates, aber das ist nicht fatal.
|
|
94
|
+
if (opts.onError)
|
|
95
|
+
opts.onError(err);
|
|
96
|
+
else {
|
|
97
|
+
// biome-ignore lint/suspicious/noConsole: codegen-watcher logs to terminal
|
|
98
|
+
console.warn(`[codegen] watcher failed to start (live-updates disabled): ${err instanceof Error ? err.message : String(err)}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
close: () => {
|
|
103
|
+
// skip: already closed, avoid double-close
|
|
104
|
+
if (closed)
|
|
105
|
+
return;
|
|
106
|
+
closed = true;
|
|
107
|
+
if (timer)
|
|
108
|
+
clearTimeout(timer);
|
|
109
|
+
watcher?.close();
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type AuthMfaFeatureOptions } from "@cosmicdrift/kumiko-bundled-features/auth-mfa";
|
|
2
|
+
import { type LegalPagesOptions } from "@cosmicdrift/kumiko-bundled-features/legal-pages";
|
|
3
|
+
import { type UserDataRightsOptions } from "@cosmicdrift/kumiko-bundled-features/user-data-rights";
|
|
4
|
+
import type { FeatureDefinition } from "@cosmicdrift/kumiko-framework/engine";
|
|
5
|
+
export type FileProviderKind = "inmemory" | "s3" | "s3-env";
|
|
6
|
+
export type MailTransportKind = "inmemory" | "smtp";
|
|
7
|
+
export type GdprStackOrder = "retention-first" | "compliance-first";
|
|
8
|
+
export type FileStackOptions = {
|
|
9
|
+
readonly providers: readonly FileProviderKind[];
|
|
10
|
+
readonly includeFilesFeature?: boolean;
|
|
11
|
+
};
|
|
12
|
+
export type MailStackOptions = {
|
|
13
|
+
readonly transports: readonly MailTransportKind[];
|
|
14
|
+
};
|
|
15
|
+
export type PagesStackOptions = {
|
|
16
|
+
readonly wrapLayout?: LegalPagesOptions["wrapLayout"];
|
|
17
|
+
};
|
|
18
|
+
export type GdprStackOptions = {
|
|
19
|
+
readonly order?: GdprStackOrder;
|
|
20
|
+
readonly sessions?: boolean;
|
|
21
|
+
readonly tenantLifecycle?: boolean;
|
|
22
|
+
/** Optional tokenVerifier provider(s) (e.g. PAT) when `sessions` is on.
|
|
23
|
+
* Session-only stacks need none — sessions registers sessionStore, which
|
|
24
|
+
* satisfies auth-foundation's boot check (#1570). Pass providers when the
|
|
25
|
+
* app also wants Bearer/PAT auth. */
|
|
26
|
+
readonly providers?: readonly FeatureDefinition[];
|
|
27
|
+
};
|
|
28
|
+
export type UserDataRightsStackOptions = {
|
|
29
|
+
readonly userDataRights?: UserDataRightsOptions;
|
|
30
|
+
readonly includeDefaults?: boolean;
|
|
31
|
+
};
|
|
32
|
+
export type OpsStackOptions = {
|
|
33
|
+
readonly delivery?: boolean;
|
|
34
|
+
readonly audit?: boolean;
|
|
35
|
+
readonly jobs?: boolean;
|
|
36
|
+
readonly rateLimiting?: boolean;
|
|
37
|
+
};
|
|
38
|
+
/** sessions (+ optional auth-mfa). config/user/tenant/auth-email-password stay
|
|
39
|
+
* on composeFeatures(includeBundled). Pass `mfa` options to mount TOTP.
|
|
40
|
+
* When `sessions` is on (the default), auth-foundation is mounted alongside it —
|
|
41
|
+
* the framework's registry now hard-requires it (sessions.requires("auth-foundation")).
|
|
42
|
+
* Session-only is enough for boot (#1570); optional Bearer/PAT still needs a
|
|
43
|
+
* tokenVerifier provider from the caller (e.g. createPersonalAccessTokensFeature
|
|
44
|
+
* ({ scopes })) — PAT scopes are app-specific, so they stay caller-owned. */
|
|
45
|
+
export type IdentityStackOptions = {
|
|
46
|
+
readonly sessions?: boolean;
|
|
47
|
+
readonly mfa?: AuthMfaFeatureOptions;
|
|
48
|
+
};
|
|
49
|
+
export declare function stackFeatureNames(features: readonly FeatureDefinition[]): string[];
|
|
50
|
+
export declare function composePagesStack(options?: PagesStackOptions): FeatureDefinition[];
|
|
51
|
+
export declare function composeRendererStack(): FeatureDefinition[];
|
|
52
|
+
export declare function composeMailStack(options: MailStackOptions): FeatureDefinition[];
|
|
53
|
+
export declare function composeFileStack(options: FileStackOptions): FeatureDefinition[];
|
|
54
|
+
/** Retention + compliance (+ optional lifecycle/sessions). UDR/files mount separately — order varies per app. */
|
|
55
|
+
export declare function composeGdprStack(options?: GdprStackOptions): FeatureDefinition[];
|
|
56
|
+
export declare function composeUserDataRightsStack(options?: UserDataRightsStackOptions): FeatureDefinition[];
|
|
57
|
+
export declare function composeOpsStack(options?: OpsStackOptions): FeatureDefinition[];
|
|
58
|
+
/** Identity opt-ins: sessions by default; auth-mfa when `mfa` options given.
|
|
59
|
+
* Do not also pass `sessions: true` to composeGdprStack — duplicate feature. */
|
|
60
|
+
export declare function composeIdentityStack(options?: IdentityStackOptions): FeatureDefinition[];
|