@mantajs/cli 0.2.0-beta.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/bin/manta.mjs +20 -0
- package/bin/manta.ts +13 -0
- package/dist/admin/generate-admin-config.d.ts +8 -0
- package/dist/admin/generate-admin-config.d.ts.map +1 -0
- package/dist/admin/generate-admin-config.js +127 -0
- package/dist/admin/generate-admin-config.js.map +1 -0
- package/dist/ai/chat-handler.d.ts +10 -0
- package/dist/ai/chat-handler.d.ts.map +1 -0
- package/dist/ai/chat-handler.js +1353 -0
- package/dist/ai/chat-handler.js.map +1 -0
- package/dist/bootstrap/boot.d.ts +25 -0
- package/dist/bootstrap/boot.d.ts.map +1 -0
- package/dist/bootstrap/boot.js +344 -0
- package/dist/bootstrap/boot.js.map +1 -0
- package/dist/bootstrap/bootstrap-app.d.ts +71 -0
- package/dist/bootstrap/bootstrap-app.d.ts.map +1 -0
- package/dist/bootstrap/bootstrap-app.js +308 -0
- package/dist/bootstrap/bootstrap-app.js.map +1 -0
- package/dist/bootstrap/bootstrap-context.d.ts +76 -0
- package/dist/bootstrap/bootstrap-context.d.ts.map +1 -0
- package/dist/bootstrap/bootstrap-context.js +4 -0
- package/dist/bootstrap/bootstrap-context.js.map +1 -0
- package/dist/bootstrap/bootstrap-helpers.d.ts +71 -0
- package/dist/bootstrap/bootstrap-helpers.d.ts.map +1 -0
- package/dist/bootstrap/bootstrap-helpers.js +373 -0
- package/dist/bootstrap/bootstrap-helpers.js.map +1 -0
- package/dist/bootstrap/generate-types.d.ts +7 -0
- package/dist/bootstrap/generate-types.d.ts.map +1 -0
- package/dist/bootstrap/generate-types.js +832 -0
- package/dist/bootstrap/generate-types.js.map +1 -0
- package/dist/bootstrap/phases/assemble/index.d.ts +5 -0
- package/dist/bootstrap/phases/assemble/index.d.ts.map +1 -0
- package/dist/bootstrap/phases/assemble/index.js +5 -0
- package/dist/bootstrap/phases/assemble/index.js.map +1 -0
- package/dist/bootstrap/phases/assemble/load-links.d.ts +3 -0
- package/dist/bootstrap/phases/assemble/load-links.d.ts.map +1 -0
- package/dist/bootstrap/phases/assemble/load-links.js +160 -0
- package/dist/bootstrap/phases/assemble/load-links.js.map +1 -0
- package/dist/bootstrap/phases/assemble/load-modules.d.ts +3 -0
- package/dist/bootstrap/phases/assemble/load-modules.d.ts.map +1 -0
- package/dist/bootstrap/phases/assemble/load-modules.js +163 -0
- package/dist/bootstrap/phases/assemble/load-modules.js.map +1 -0
- package/dist/bootstrap/phases/assemble/load-resources.d.ts +3 -0
- package/dist/bootstrap/phases/assemble/load-resources.d.ts.map +1 -0
- package/dist/bootstrap/phases/assemble/load-resources.js +270 -0
- package/dist/bootstrap/phases/assemble/load-resources.js.map +1 -0
- package/dist/bootstrap/phases/assemble/wire-commands.d.ts +3 -0
- package/dist/bootstrap/phases/assemble/wire-commands.d.ts.map +1 -0
- package/dist/bootstrap/phases/assemble/wire-commands.js +408 -0
- package/dist/bootstrap/phases/assemble/wire-commands.js.map +1 -0
- package/dist/bootstrap/phases/assemble-modules.d.ts +3 -0
- package/dist/bootstrap/phases/assemble-modules.d.ts.map +1 -0
- package/dist/bootstrap/phases/assemble-modules.js +14 -0
- package/dist/bootstrap/phases/assemble-modules.js.map +1 -0
- package/dist/bootstrap/phases/build-app.d.ts +3 -0
- package/dist/bootstrap/phases/build-app.d.ts.map +1 -0
- package/dist/bootstrap/phases/build-app.js +15 -0
- package/dist/bootstrap/phases/build-app.js.map +1 -0
- package/dist/bootstrap/phases/discover-resources.d.ts +3 -0
- package/dist/bootstrap/phases/discover-resources.d.ts.map +1 -0
- package/dist/bootstrap/phases/discover-resources.js +60 -0
- package/dist/bootstrap/phases/discover-resources.js.map +1 -0
- package/dist/bootstrap/phases/index.d.ts +7 -0
- package/dist/bootstrap/phases/index.d.ts.map +1 -0
- package/dist/bootstrap/phases/index.js +7 -0
- package/dist/bootstrap/phases/index.js.map +1 -0
- package/dist/bootstrap/phases/init-infra.d.ts +3 -0
- package/dist/bootstrap/phases/init-infra.d.ts.map +1 -0
- package/dist/bootstrap/phases/init-infra.js +193 -0
- package/dist/bootstrap/phases/init-infra.js.map +1 -0
- package/dist/bootstrap/phases/seed-dev-users.d.ts +3 -0
- package/dist/bootstrap/phases/seed-dev-users.d.ts.map +1 -0
- package/dist/bootstrap/phases/seed-dev-users.js +93 -0
- package/dist/bootstrap/phases/seed-dev-users.js.map +1 -0
- package/dist/bootstrap/phases/wire/auth-helpers.d.ts +12 -0
- package/dist/bootstrap/phases/wire/auth-helpers.d.ts.map +1 -0
- package/dist/bootstrap/phases/wire/auth-helpers.js +25 -0
- package/dist/bootstrap/phases/wire/auth-helpers.js.map +1 -0
- package/dist/bootstrap/phases/wire/contexts/context-registry.d.ts +4 -0
- package/dist/bootstrap/phases/wire/contexts/context-registry.d.ts.map +1 -0
- package/dist/bootstrap/phases/wire/contexts/context-registry.js +96 -0
- package/dist/bootstrap/phases/wire/contexts/context-registry.js.map +1 -0
- package/dist/bootstrap/phases/wire/contexts/cqrs-routes.d.ts +3 -0
- package/dist/bootstrap/phases/wire/contexts/cqrs-routes.d.ts.map +1 -0
- package/dist/bootstrap/phases/wire/contexts/cqrs-routes.js +138 -0
- package/dist/bootstrap/phases/wire/contexts/cqrs-routes.js.map +1 -0
- package/dist/bootstrap/phases/wire/contexts/index.d.ts +6 -0
- package/dist/bootstrap/phases/wire/contexts/index.d.ts.map +1 -0
- package/dist/bootstrap/phases/wire/contexts/index.js +6 -0
- package/dist/bootstrap/phases/wire/contexts/index.js.map +1 -0
- package/dist/bootstrap/phases/wire/contexts/query-endpoints.d.ts +3 -0
- package/dist/bootstrap/phases/wire/contexts/query-endpoints.d.ts.map +1 -0
- package/dist/bootstrap/phases/wire/contexts/query-endpoints.js +116 -0
- package/dist/bootstrap/phases/wire/contexts/query-endpoints.js.map +1 -0
- package/dist/bootstrap/phases/wire/contexts/spa-warnings.d.ts +3 -0
- package/dist/bootstrap/phases/wire/contexts/spa-warnings.d.ts.map +1 -0
- package/dist/bootstrap/phases/wire/contexts/spa-warnings.js +17 -0
- package/dist/bootstrap/phases/wire/contexts/spa-warnings.js.map +1 -0
- package/dist/bootstrap/phases/wire/contexts/user-routes.d.ts +3 -0
- package/dist/bootstrap/phases/wire/contexts/user-routes.d.ts.map +1 -0
- package/dist/bootstrap/phases/wire/contexts/user-routes.js +83 -0
- package/dist/bootstrap/phases/wire/contexts/user-routes.js.map +1 -0
- package/dist/bootstrap/phases/wire/index.d.ts +5 -0
- package/dist/bootstrap/phases/wire/index.d.ts.map +1 -0
- package/dist/bootstrap/phases/wire/index.js +5 -0
- package/dist/bootstrap/phases/wire/index.js.map +1 -0
- package/dist/bootstrap/phases/wire/wire-adapter.d.ts +12 -0
- package/dist/bootstrap/phases/wire/wire-adapter.d.ts.map +1 -0
- package/dist/bootstrap/phases/wire/wire-adapter.js +156 -0
- package/dist/bootstrap/phases/wire/wire-adapter.js.map +1 -0
- package/dist/bootstrap/phases/wire/wire-auth.d.ts +3 -0
- package/dist/bootstrap/phases/wire/wire-auth.d.ts.map +1 -0
- package/dist/bootstrap/phases/wire/wire-auth.js +46 -0
- package/dist/bootstrap/phases/wire/wire-auth.js.map +1 -0
- package/dist/bootstrap/phases/wire/wire-contexts.d.ts +3 -0
- package/dist/bootstrap/phases/wire/wire-contexts.d.ts.map +1 -0
- package/dist/bootstrap/phases/wire/wire-contexts.js +15 -0
- package/dist/bootstrap/phases/wire/wire-contexts.js.map +1 -0
- package/dist/bootstrap/phases/wire/wire-cron-routes.d.ts +3 -0
- package/dist/bootstrap/phases/wire/wire-cron-routes.d.ts.map +1 -0
- package/dist/bootstrap/phases/wire/wire-cron-routes.js +102 -0
- package/dist/bootstrap/phases/wire/wire-cron-routes.js.map +1 -0
- package/dist/bootstrap/phases/wire/wire-extras.d.ts +3 -0
- package/dist/bootstrap/phases/wire/wire-extras.d.ts.map +1 -0
- package/dist/bootstrap/phases/wire/wire-extras.js +305 -0
- package/dist/bootstrap/phases/wire/wire-extras.js.map +1 -0
- package/dist/bootstrap/phases/wire/wire-workflow-routes.d.ts +3 -0
- package/dist/bootstrap/phases/wire/wire-workflow-routes.d.ts.map +1 -0
- package/dist/bootstrap/phases/wire/wire-workflow-routes.js +212 -0
- package/dist/bootstrap/phases/wire/wire-workflow-routes.js.map +1 -0
- package/dist/bootstrap/phases/wire-http.d.ts +3 -0
- package/dist/bootstrap/phases/wire-http.d.ts.map +1 -0
- package/dist/bootstrap/phases/wire-http.js +10 -0
- package/dist/bootstrap/phases/wire-http.js.map +1 -0
- package/dist/bootstrap/validate-generated-ts.d.ts +6 -0
- package/dist/bootstrap/validate-generated-ts.d.ts.map +1 -0
- package/dist/bootstrap/validate-generated-ts.js +26 -0
- package/dist/bootstrap/validate-generated-ts.js.map +1 -0
- package/dist/build/generate-manifest.d.ts +10 -0
- package/dist/build/generate-manifest.d.ts.map +1 -0
- package/dist/build/generate-manifest.js +138 -0
- package/dist/build/generate-manifest.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +421 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/build.d.ts +21 -0
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/build.js +399 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/db/create.d.ts +17 -0
- package/dist/commands/db/create.d.ts.map +1 -0
- package/dist/commands/db/create.js +94 -0
- package/dist/commands/db/create.js.map +1 -0
- package/dist/commands/db/diff.d.ts +39 -0
- package/dist/commands/db/diff.d.ts.map +1 -0
- package/dist/commands/db/diff.js +81 -0
- package/dist/commands/db/diff.js.map +1 -0
- package/dist/commands/db/generate.d.ts +58 -0
- package/dist/commands/db/generate.d.ts.map +1 -0
- package/dist/commands/db/generate.js +138 -0
- package/dist/commands/db/generate.js.map +1 -0
- package/dist/commands/db/migrate.d.ts +29 -0
- package/dist/commands/db/migrate.d.ts.map +1 -0
- package/dist/commands/db/migrate.js +118 -0
- package/dist/commands/db/migrate.js.map +1 -0
- package/dist/commands/db/pg-deps.d.ts +30 -0
- package/dist/commands/db/pg-deps.d.ts.map +1 -0
- package/dist/commands/db/pg-deps.js +178 -0
- package/dist/commands/db/pg-deps.js.map +1 -0
- package/dist/commands/db/rollback.d.ts +21 -0
- package/dist/commands/db/rollback.d.ts.map +1 -0
- package/dist/commands/db/rollback.js +85 -0
- package/dist/commands/db/rollback.js.map +1 -0
- package/dist/commands/db/types.d.ts +113 -0
- package/dist/commands/db/types.d.ts.map +1 -0
- package/dist/commands/db/types.js +4 -0
- package/dist/commands/db/types.js.map +1 -0
- package/dist/commands/dev.d.ts +12 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +79 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/exec.d.ts +21 -0
- package/dist/commands/exec.d.ts.map +1 -0
- package/dist/commands/exec.js +148 -0
- package/dist/commands/exec.js.map +1 -0
- package/dist/commands/generate.d.ts +11 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +19 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/init.d.ts +14 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +476 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/start.d.ts +15 -0
- package/dist/commands/start.d.ts.map +1 -0
- package/dist/commands/start.js +121 -0
- package/dist/commands/start.js.map +1 -0
- package/dist/commands/user.d.ts +19 -0
- package/dist/commands/user.d.ts.map +1 -0
- package/dist/commands/user.js +125 -0
- package/dist/commands/user.js.map +1 -0
- package/dist/config/load-config.d.ts +23 -0
- package/dist/config/load-config.d.ts.map +1 -0
- package/dist/config/load-config.js +105 -0
- package/dist/config/load-config.js.map +1 -0
- package/dist/config/load-env.d.ts +11 -0
- package/dist/config/load-env.d.ts.map +1 -0
- package/dist/config/load-env.js +61 -0
- package/dist/config/load-env.js.map +1 -0
- package/dist/config/resolve-adapters.d.ts +23 -0
- package/dist/config/resolve-adapters.d.ts.map +1 -0
- package/dist/config/resolve-adapters.js +96 -0
- package/dist/config/resolve-adapters.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/jiti.d.ts +2 -0
- package/dist/jiti.d.ts.map +1 -0
- package/dist/jiti.js +2 -0
- package/dist/jiti.js.map +1 -0
- package/dist/openapi/generate-spec.d.ts +56 -0
- package/dist/openapi/generate-spec.d.ts.map +1 -0
- package/dist/openapi/generate-spec.js +491 -0
- package/dist/openapi/generate-spec.js.map +1 -0
- package/dist/openapi/index.d.ts +4 -0
- package/dist/openapi/index.d.ts.map +1 -0
- package/dist/openapi/index.js +3 -0
- package/dist/openapi/index.js.map +1 -0
- package/dist/openapi/swagger-html.d.ts +2 -0
- package/dist/openapi/swagger-html.d.ts.map +1 -0
- package/dist/openapi/swagger-html.js +29 -0
- package/dist/openapi/swagger-html.js.map +1 -0
- package/dist/plugins/merge-resources.d.ts +18 -0
- package/dist/plugins/merge-resources.d.ts.map +1 -0
- package/dist/plugins/merge-resources.js +76 -0
- package/dist/plugins/merge-resources.js.map +1 -0
- package/dist/plugins/resolve-plugins.d.ts +14 -0
- package/dist/plugins/resolve-plugins.d.ts.map +1 -0
- package/dist/plugins/resolve-plugins.js +73 -0
- package/dist/plugins/resolve-plugins.js.map +1 -0
- package/dist/resource-loader.d.ts +151 -0
- package/dist/resource-loader.d.ts.map +1 -0
- package/dist/resource-loader.js +456 -0
- package/dist/resource-loader.js.map +1 -0
- package/dist/route-discovery.d.ts +33 -0
- package/dist/route-discovery.d.ts.map +1 -0
- package/dist/route-discovery.js +69 -0
- package/dist/route-discovery.js.map +1 -0
- package/dist/server-bootstrap.d.ts +38 -0
- package/dist/server-bootstrap.d.ts.map +1 -0
- package/dist/server-bootstrap.js +21 -0
- package/dist/server-bootstrap.js.map +1 -0
- package/dist/spa/generate-spa.d.ts +15 -0
- package/dist/spa/generate-spa.d.ts.map +1 -0
- package/dist/spa/generate-spa.js +357 -0
- package/dist/spa/generate-spa.js.map +1 -0
- package/dist/templates/agent/nextjs.md +129 -0
- package/dist/templates/agent/nuxt.md +98 -0
- package/dist/templates/agent/standalone.md +498 -0
- package/dist/types.d.ts +145 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/colors.d.ts +7 -0
- package/dist/utils/colors.d.ts.map +1 -0
- package/dist/utils/colors.js +8 -0
- package/dist/utils/colors.js.map +1 -0
- package/dist/utils/logger.d.ts +10 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +27 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/prompts.d.ts +6 -0
- package/dist/utils/prompts.d.ts.map +1 -0
- package/dist/utils/prompts.js +28 -0
- package/dist/utils/prompts.js.map +1 -0
- package/dist/utils/spinner.d.ts +7 -0
- package/dist/utils/spinner.d.ts.map +1 -0
- package/dist/utils/spinner.js +20 -0
- package/dist/utils/spinner.js.map +1 -0
- package/package.json +80 -0
|
@@ -0,0 +1,832 @@
|
|
|
1
|
+
// Generate .manta/generated.d.ts from discovered DML entities, services, commands, and subscribers.
|
|
2
|
+
// Runs in the CLI process (before Nitro starts), not in the Nitro worker.
|
|
3
|
+
//
|
|
4
|
+
// Generates a SINGLE file:
|
|
5
|
+
// .manta/generated.d.ts — all type declarations merged
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { relative, resolve } from 'node:path';
|
|
8
|
+
import { MantaError } from '@mantajs/core';
|
|
9
|
+
import { discoverResources } from '../resource-loader';
|
|
10
|
+
import { validateGeneratedTypeScript } from './validate-generated-ts';
|
|
11
|
+
// ── Identifier sanitization ──────────────────────────────────────────
|
|
12
|
+
// SPEC TS-04 — Reject invalid identifiers at the source with clear
|
|
13
|
+
// MantaError messages. Pairs with the output validator (belt + braces).
|
|
14
|
+
function assertSafeIdentifierComponent(value, kind, where) {
|
|
15
|
+
const pattern = kind === 'PascalCase' ? /^[A-Z][A-Za-z0-9]*$/ : /^[a-z][A-Za-z0-9]*$/;
|
|
16
|
+
if (!pattern.test(value)) {
|
|
17
|
+
throw new MantaError('INVALID_DATA', `Invalid ${kind} identifier "${value}" in ${where}. Must match ${pattern}.`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
// ── Inject globals so model imports work in standalone codegen ────────
|
|
21
|
+
async function injectGlobals() {
|
|
22
|
+
const g = globalThis;
|
|
23
|
+
if (g.defineModel)
|
|
24
|
+
return; // Already injected
|
|
25
|
+
const core = await import('@mantajs/core');
|
|
26
|
+
g.defineModel = core.defineModel;
|
|
27
|
+
g.defineService = core.defineService;
|
|
28
|
+
g.defineLink = core.defineLink;
|
|
29
|
+
g.defineCommand = core.defineCommand;
|
|
30
|
+
g.defineAgent = core.defineAgent;
|
|
31
|
+
g.defineSubscriber = core.defineSubscriber;
|
|
32
|
+
g.defineJob = core.defineJob;
|
|
33
|
+
g.defineUserModel = core.defineUserModel;
|
|
34
|
+
g.defineQuery = core.defineQuery;
|
|
35
|
+
g.defineQueryGraph = core.defineQueryGraph;
|
|
36
|
+
g.extendQueryGraph = core.extendQueryGraph;
|
|
37
|
+
g.defineWorkflow = core.defineWorkflow;
|
|
38
|
+
g.defineConfig = core.defineConfig;
|
|
39
|
+
g.definePreset = core.definePreset;
|
|
40
|
+
g.defineMiddleware = core.defineMiddleware;
|
|
41
|
+
g.field = core.field;
|
|
42
|
+
g.many = core.many;
|
|
43
|
+
const { z } = await import('zod');
|
|
44
|
+
g.z = z;
|
|
45
|
+
}
|
|
46
|
+
// ── Pluralization (must match instantiate.ts) ────────────────────────
|
|
47
|
+
function pluralize(name) {
|
|
48
|
+
if (name.endsWith('s') || name.endsWith('x') || name.endsWith('ch') || name.endsWith('sh'))
|
|
49
|
+
return `${name}es`;
|
|
50
|
+
if (name.endsWith('y') && !/[aeiou]y$/i.test(name))
|
|
51
|
+
return `${name.slice(0, -1)}ies`;
|
|
52
|
+
return `${name}s`;
|
|
53
|
+
}
|
|
54
|
+
async function collectModuleEntries(resources) {
|
|
55
|
+
const entries = [];
|
|
56
|
+
// Also include user models (defineUserModel) as module entities
|
|
57
|
+
for (const userInfo of resources.users) {
|
|
58
|
+
try {
|
|
59
|
+
const mod = (await import(`${userInfo.path}?t=${Date.now()}`));
|
|
60
|
+
const def = (mod.default ?? mod);
|
|
61
|
+
if (def?.__type === 'user' && def?.model) {
|
|
62
|
+
const model = def.model;
|
|
63
|
+
const contextName = def.contextName;
|
|
64
|
+
assertSafeIdentifierComponent(model.name, 'PascalCase', `defineUserModel in ${userInfo.path}`);
|
|
65
|
+
// Find the module this user belongs to
|
|
66
|
+
const parentModule = resources.modules.find((m) => m.entities.some((e) => e.modelPath === userInfo.path)) ??
|
|
67
|
+
resources.modules.find((m) => m.name === contextName);
|
|
68
|
+
const moduleName = parentModule?.name ?? contextName;
|
|
69
|
+
// Use a unique export alias for the user model
|
|
70
|
+
const alias = `${model.name}UserDef`;
|
|
71
|
+
entries.push({
|
|
72
|
+
moduleName,
|
|
73
|
+
entityName: model.name,
|
|
74
|
+
entityExportName: alias,
|
|
75
|
+
modulePath: userInfo.path,
|
|
76
|
+
customMethods: [],
|
|
77
|
+
isUserModel: true,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
// Propagate validation errors (bad identifiers); swallow only true import failures.
|
|
83
|
+
if (MantaError.is(err))
|
|
84
|
+
throw err;
|
|
85
|
+
// User model not importable
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
for (const modInfo of resources.modules) {
|
|
89
|
+
for (const entity of modInfo.entities) {
|
|
90
|
+
try {
|
|
91
|
+
const mod = (await import(`${entity.modelPath}?t=${Date.now()}`));
|
|
92
|
+
// Find the DML entity export — support both DmlEntity and UserDefinition
|
|
93
|
+
let entityExportName = '';
|
|
94
|
+
let entityName = '';
|
|
95
|
+
let isExternal = false;
|
|
96
|
+
for (const [exportName, value] of Object.entries(mod)) {
|
|
97
|
+
// Standard DmlEntity
|
|
98
|
+
if (typeof value === 'object' &&
|
|
99
|
+
value !== null &&
|
|
100
|
+
'name' in value &&
|
|
101
|
+
'schema' in value &&
|
|
102
|
+
typeof value.name === 'string' &&
|
|
103
|
+
typeof value.getOptions === 'function') {
|
|
104
|
+
const v = value;
|
|
105
|
+
entityExportName = exportName;
|
|
106
|
+
entityName = v.name;
|
|
107
|
+
const opts = v.getOptions?.();
|
|
108
|
+
isExternal = opts?.external === true;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
// UserDefinition — skip in entity loop, handled by user model loop above
|
|
112
|
+
if (typeof value === 'object' && value !== null && value.__type === 'user') {
|
|
113
|
+
break; // Skip — already added by user model loop
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (!entityExportName)
|
|
117
|
+
continue;
|
|
118
|
+
assertSafeIdentifierComponent(entityName, 'PascalCase', `defineModel in ${entity.modelPath}`);
|
|
119
|
+
// Fix: 'default' is a reserved word — alias it to the entity name
|
|
120
|
+
let isDefaultExport = false;
|
|
121
|
+
if (entityExportName === 'default') {
|
|
122
|
+
entityExportName = `${entityName}Model`;
|
|
123
|
+
isDefaultExport = true;
|
|
124
|
+
}
|
|
125
|
+
// Find custom compensable methods from the service.ts (if it exists)
|
|
126
|
+
const customMethods = [];
|
|
127
|
+
if (entity.servicePath) {
|
|
128
|
+
try {
|
|
129
|
+
const serviceMod = (await import(`${entity.servicePath}?t=${Date.now()}`));
|
|
130
|
+
const defaultExport = serviceMod.default;
|
|
131
|
+
if (defaultExport?.__type && typeof defaultExport.factory === 'function') {
|
|
132
|
+
const fakeRepo = new Proxy({}, {
|
|
133
|
+
get: () => async () => [],
|
|
134
|
+
});
|
|
135
|
+
const methods = defaultExport.factory(fakeRepo);
|
|
136
|
+
for (const [methodName, fn] of Object.entries(methods)) {
|
|
137
|
+
if (typeof fn === 'function') {
|
|
138
|
+
customMethods.push(methodName);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// Service may fail with fake repo — skip custom methods
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
entries.push({
|
|
148
|
+
moduleName: modInfo.name,
|
|
149
|
+
entityName,
|
|
150
|
+
entityExportName,
|
|
151
|
+
modulePath: entity.modelPath,
|
|
152
|
+
customMethods,
|
|
153
|
+
isDefaultExport,
|
|
154
|
+
isExternal,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
// Propagate validation errors (bad identifiers); swallow only true import failures.
|
|
159
|
+
if (MantaError.is(err))
|
|
160
|
+
throw err;
|
|
161
|
+
// Entity model may not be importable — skip
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return entries;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Try to extract the object literal from a step.emit() / app.emit() call.
|
|
169
|
+
* Best-effort: parses `{ key: value, key2: value2 }` after the event name.
|
|
170
|
+
* Returns field names with inferred TS types, or undefined if unparseable.
|
|
171
|
+
*/
|
|
172
|
+
function extractEmitDataShape(content, eventName) {
|
|
173
|
+
// Match .emit('eventName', { ... })
|
|
174
|
+
const escaped = eventName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
175
|
+
const pattern = new RegExp(`\\.emit\\(\\s*['"]${escaped}['"]\\s*,\\s*\\{([^}]+)\\}`, 's');
|
|
176
|
+
const match = pattern.exec(content);
|
|
177
|
+
if (!match)
|
|
178
|
+
return undefined;
|
|
179
|
+
const body = match[1];
|
|
180
|
+
const fields = {};
|
|
181
|
+
// Match property patterns: `key: expr` or `key,` (shorthand)
|
|
182
|
+
const propPattern = /(\w+)\s*(?::\s*([^,\n}]+?))?(?:\s*[,\n}])/g;
|
|
183
|
+
let propMatch = null;
|
|
184
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: regex iteration
|
|
185
|
+
while ((propMatch = propPattern.exec(body)) !== null) {
|
|
186
|
+
const key = propMatch[1];
|
|
187
|
+
const value = propMatch[2]?.trim();
|
|
188
|
+
// Infer type from the value expression
|
|
189
|
+
if (!value || value.startsWith('input.')) {
|
|
190
|
+
fields[key] = 'unknown';
|
|
191
|
+
}
|
|
192
|
+
else if (/^['"]/.test(value)) {
|
|
193
|
+
fields[key] = 'string';
|
|
194
|
+
}
|
|
195
|
+
else if (/^\d+/.test(value)) {
|
|
196
|
+
fields[key] = 'number';
|
|
197
|
+
}
|
|
198
|
+
else if (value === 'true' || value === 'false') {
|
|
199
|
+
fields[key] = 'boolean';
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
fields[key] = 'unknown';
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return Object.keys(fields).length > 0 ? fields : undefined;
|
|
206
|
+
}
|
|
207
|
+
async function collectEvents(resources) {
|
|
208
|
+
const events = [];
|
|
209
|
+
const seen = new Set();
|
|
210
|
+
// Extract event names from subscriber definitions
|
|
211
|
+
for (const subInfo of resources.subscribers) {
|
|
212
|
+
try {
|
|
213
|
+
const mod = (await import(`${subInfo.path}?t=${Date.now()}`));
|
|
214
|
+
const sub = mod.default;
|
|
215
|
+
if (sub?.event) {
|
|
216
|
+
const eventNames = Array.isArray(sub.event) ? sub.event : [sub.event];
|
|
217
|
+
for (const name of eventNames) {
|
|
218
|
+
const eventName = name;
|
|
219
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9.-]*$/.test(eventName)) {
|
|
220
|
+
throw new MantaError('INVALID_DATA', `Invalid subscriber event name "${eventName}" in ${subInfo.path}. Must be alphanumeric with dots/hyphens.`);
|
|
221
|
+
}
|
|
222
|
+
if (!seen.has(eventName)) {
|
|
223
|
+
seen.add(eventName);
|
|
224
|
+
events.push({ eventName, sourceFile: subInfo.path, sourceType: 'subscriber' });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
catch (err) {
|
|
230
|
+
if (MantaError.is(err))
|
|
231
|
+
throw err;
|
|
232
|
+
// Skip
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// Extract event names + data shapes from command files (step.emit calls)
|
|
236
|
+
for (const cmdInfo of resources.commands) {
|
|
237
|
+
try {
|
|
238
|
+
const content = readFileSync(cmdInfo.path, 'utf-8');
|
|
239
|
+
// Match step.emit('event.name', ...) and app.emit('event.name', ...)
|
|
240
|
+
const emitPattern = /\.emit\(\s*['"]([a-z][a-z0-9.-]+)['"]/g;
|
|
241
|
+
let match = null;
|
|
242
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: regex iteration
|
|
243
|
+
while ((match = emitPattern.exec(content)) !== null) {
|
|
244
|
+
const name = match[1];
|
|
245
|
+
if (!seen.has(name)) {
|
|
246
|
+
seen.add(name);
|
|
247
|
+
const dataFields = extractEmitDataShape(content, name);
|
|
248
|
+
events.push({ eventName: name, sourceFile: cmdInfo.path, sourceType: 'command', dataFields });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
// Skip
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// Also scan subscriber files for app.emit() calls (cascading events)
|
|
257
|
+
for (const subInfo of resources.subscribers) {
|
|
258
|
+
try {
|
|
259
|
+
const content = readFileSync(subInfo.path, 'utf-8');
|
|
260
|
+
const emitPattern = /\.emit\(\s*['"]([a-z][a-z0-9.-]+)['"]/g;
|
|
261
|
+
let match = null;
|
|
262
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: regex iteration
|
|
263
|
+
while ((match = emitPattern.exec(content)) !== null) {
|
|
264
|
+
const name = match[1];
|
|
265
|
+
if (!seen.has(name)) {
|
|
266
|
+
seen.add(name);
|
|
267
|
+
const dataFields = extractEmitDataShape(content, name);
|
|
268
|
+
events.push({ eventName: name, sourceFile: subInfo.path, sourceType: 'subscriber', dataFields });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
// Skip
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return events;
|
|
277
|
+
}
|
|
278
|
+
// ── Main entry point ─────────────────────────────────────────────────
|
|
279
|
+
/**
|
|
280
|
+
* Scan src/ for modules, commands, subscribers and generate typed declarations.
|
|
281
|
+
*
|
|
282
|
+
* Generates a single file: .manta/generated.d.ts
|
|
283
|
+
*/
|
|
284
|
+
export async function generateTypesFromModules(cwd) {
|
|
285
|
+
const mantaDir = resolve(cwd, '.manta');
|
|
286
|
+
if (!existsSync(mantaDir))
|
|
287
|
+
mkdirSync(mantaDir, { recursive: true });
|
|
288
|
+
// Clean up old codegen directories if they exist
|
|
289
|
+
for (const old of ['types', '../.manta-types']) {
|
|
290
|
+
const oldDir = resolve(mantaDir, old);
|
|
291
|
+
if (existsSync(oldDir))
|
|
292
|
+
rmSync(oldDir, { recursive: true, force: true });
|
|
293
|
+
}
|
|
294
|
+
// Inject globals (defineModel, defineService, etc.) so model imports work standalone
|
|
295
|
+
await injectGlobals();
|
|
296
|
+
// Load config + resolve plugins, so plugin-contributed modules/entities show up in codegen
|
|
297
|
+
let resources = await discoverResources(cwd);
|
|
298
|
+
try {
|
|
299
|
+
const { loadConfig } = await import('../config/load-config');
|
|
300
|
+
const { resolvePlugins } = await import('../plugins/resolve-plugins');
|
|
301
|
+
const { mergePluginResources } = await import('../plugins/merge-resources');
|
|
302
|
+
const config = await loadConfig(cwd);
|
|
303
|
+
if (config) {
|
|
304
|
+
const plugins = resolvePlugins(config, cwd);
|
|
305
|
+
if (plugins.length > 0) {
|
|
306
|
+
resources = await mergePluginResources(plugins, resources);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
// Config may not exist yet (e.g. during postinstall of the CLI itself) — ignore
|
|
312
|
+
}
|
|
313
|
+
const entries = await collectModuleEntries(resources);
|
|
314
|
+
const events = await collectEvents(resources);
|
|
315
|
+
if (entries.length === 0)
|
|
316
|
+
return;
|
|
317
|
+
const outPath = resolve(mantaDir, 'generated.d.ts');
|
|
318
|
+
const lines = [
|
|
319
|
+
'// Auto-generated by manta dev — DO NOT EDIT',
|
|
320
|
+
'// Provides typed app.modules.*, step.catalog.*, event.data, step.command.*, step.agent.*, etc.',
|
|
321
|
+
'// Regenerated on every boot when modules/commands/subscribers/agents change.',
|
|
322
|
+
'',
|
|
323
|
+
];
|
|
324
|
+
// ── Imports ─────────────────────────────────────────────────────────
|
|
325
|
+
lines.push("import type { InferEntity } from '@mantajs/core'");
|
|
326
|
+
lines.push("import type { ServiceConfig } from '@mantajs/core'");
|
|
327
|
+
// Entity imports (deduplicated by export name)
|
|
328
|
+
const seenImports = new Set();
|
|
329
|
+
for (const entry of entries) {
|
|
330
|
+
const key = `${entry.entityExportName}@${entry.modulePath}`;
|
|
331
|
+
if (seenImports.has(key))
|
|
332
|
+
continue;
|
|
333
|
+
seenImports.add(key);
|
|
334
|
+
const relPath = relative(mantaDir, entry.modulePath).replace(/\.ts$/, '');
|
|
335
|
+
const isUser = entry.isUserModel;
|
|
336
|
+
if (isUser || entry.isDefaultExport) {
|
|
337
|
+
// Default export: use default import with alias
|
|
338
|
+
lines.push(`import type ${entry.entityExportName} from '${relPath}'`);
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
lines.push(`import type { ${entry.entityExportName} } from '${relPath}'`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
// Agent imports
|
|
345
|
+
const agentEntries = resources.agents.map((a) => {
|
|
346
|
+
const camel = a.id.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
347
|
+
assertSafeIdentifierComponent(camel, 'camelCase', `defineAgent in ${a.path}`);
|
|
348
|
+
return {
|
|
349
|
+
kebab: a.id,
|
|
350
|
+
camel,
|
|
351
|
+
path: a.path,
|
|
352
|
+
};
|
|
353
|
+
});
|
|
354
|
+
if (agentEntries.length > 0) {
|
|
355
|
+
lines.push("import type { z } from 'zod'");
|
|
356
|
+
for (const agent of agentEntries) {
|
|
357
|
+
const relPath = relative(mantaDir, agent.path).replace(/\.ts$/, '');
|
|
358
|
+
lines.push(`import type ${agent.camel}Def from '${relPath}'`);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
// ── Module-scoped entity types ──────────────────────────────────────
|
|
362
|
+
lines.push('');
|
|
363
|
+
// Group entries by module to fix the duplicate interface issue
|
|
364
|
+
const moduleMap = new Map();
|
|
365
|
+
for (const entry of entries) {
|
|
366
|
+
const existing = moduleMap.get(entry.moduleName) ?? [];
|
|
367
|
+
existing.push(entry);
|
|
368
|
+
moduleMap.set(entry.moduleName, existing);
|
|
369
|
+
}
|
|
370
|
+
// Generate entity types and per-module interfaces
|
|
371
|
+
for (const [moduleName, moduleEntries] of moduleMap) {
|
|
372
|
+
for (const entry of moduleEntries) {
|
|
373
|
+
const E = entry.entityExportName;
|
|
374
|
+
const name = entry.entityName;
|
|
375
|
+
const isUser = entry.isUserModel;
|
|
376
|
+
if (isUser) {
|
|
377
|
+
// For user models, generate the type manually from the DML schema
|
|
378
|
+
// because InferEntity can't resolve the generic UserDefinition.model type
|
|
379
|
+
try {
|
|
380
|
+
const mod = (await import(`${entry.modulePath}?t=${Date.now()}`));
|
|
381
|
+
const def = (mod.default ?? mod);
|
|
382
|
+
const schema = def?.model?.schema;
|
|
383
|
+
if (schema) {
|
|
384
|
+
const fields = [];
|
|
385
|
+
for (const [key, prop] of Object.entries(schema)) {
|
|
386
|
+
const meta = typeof prop.parse === 'function' ? prop.parse(key) : null;
|
|
387
|
+
if (!meta)
|
|
388
|
+
continue;
|
|
389
|
+
const nullable = meta.nullable;
|
|
390
|
+
const dt = meta.dataType?.name ?? 'unknown';
|
|
391
|
+
let tsType = 'unknown';
|
|
392
|
+
switch (dt) {
|
|
393
|
+
case 'text':
|
|
394
|
+
tsType = 'string';
|
|
395
|
+
break;
|
|
396
|
+
case 'number':
|
|
397
|
+
case 'bigNumber':
|
|
398
|
+
case 'float':
|
|
399
|
+
case 'autoincrement':
|
|
400
|
+
tsType = 'number';
|
|
401
|
+
break;
|
|
402
|
+
case 'boolean':
|
|
403
|
+
tsType = 'boolean';
|
|
404
|
+
break;
|
|
405
|
+
case 'dateTime':
|
|
406
|
+
tsType = 'Date';
|
|
407
|
+
break;
|
|
408
|
+
case 'json':
|
|
409
|
+
tsType = 'any';
|
|
410
|
+
break;
|
|
411
|
+
case 'enum':
|
|
412
|
+
tsType = meta.values ? meta.values.map((v) => `'${v}'`).join(' | ') : 'string';
|
|
413
|
+
break;
|
|
414
|
+
case 'array':
|
|
415
|
+
tsType = 'unknown[]';
|
|
416
|
+
break;
|
|
417
|
+
default:
|
|
418
|
+
tsType = 'unknown';
|
|
419
|
+
}
|
|
420
|
+
fields.push(` ${key}${nullable ? '?' : ''}: ${tsType}${nullable ? ' | null' : ''}`);
|
|
421
|
+
}
|
|
422
|
+
lines.push(`type ${name}Entity = { ${fields.join('; ')} } & { id: string; created_at: Date; updated_at: Date }`);
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
lines.push(`type ${name}Entity = Record<string, unknown> & { id: string; created_at: Date; updated_at: Date }`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
catch {
|
|
429
|
+
lines.push(`type ${name}Entity = Record<string, unknown> & { id: string; created_at: Date; updated_at: Date }`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
lines.push(`type ${name}Entity = InferEntity<typeof ${E}> & { id: string; created_at: Date; updated_at: Date }`);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
lines.push('');
|
|
437
|
+
// Single interface per module with ALL entity methods
|
|
438
|
+
lines.push(`interface ${moduleName}Module {`);
|
|
439
|
+
for (const entry of moduleEntries) {
|
|
440
|
+
const name = entry.entityName;
|
|
441
|
+
const plural = pluralize(name);
|
|
442
|
+
// External entities: no local service, only query.graph() routing via extendQueryGraph.
|
|
443
|
+
// Skip CRUD method generation — they would fail at runtime (no table).
|
|
444
|
+
if (entry.isExternal) {
|
|
445
|
+
lines.push(` // ${name} is external — queried via query.graph({ entity: '${name.charAt(0).toLowerCase() + name.slice(1)}', ... })`);
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
lines.push(` retrieve${name}(id: string, config?: ServiceConfig): Promise<${name}Entity>`);
|
|
449
|
+
lines.push(` list${plural}(filters?: Partial<${name}Entity>, config?: ServiceConfig): Promise<${name}Entity[]>`);
|
|
450
|
+
lines.push(` listAndCount${plural}(filters?: Partial<${name}Entity>, config?: ServiceConfig): Promise<[${name}Entity[], number]>`);
|
|
451
|
+
lines.push(` create${plural}(data: Partial<${name}Entity> | Partial<${name}Entity>[]): Promise<${name}Entity | ${name}Entity[]>`);
|
|
452
|
+
lines.push(` update${plural}(data: (Partial<${name}Entity> & { id: string }) | (Partial<${name}Entity> & { id: string })[]): Promise<${name}Entity | ${name}Entity[]>`);
|
|
453
|
+
lines.push(` delete${plural}(ids: string | string[]): Promise<void>`);
|
|
454
|
+
lines.push(` softDelete${plural}(ids: string | string[]): Promise<Record<string, string[]>>`);
|
|
455
|
+
lines.push(` restore${plural}(ids: string | string[]): Promise<void>`);
|
|
456
|
+
lines.push(` list(): Promise<${name}Entity[]>`);
|
|
457
|
+
lines.push(` findById(id: string): Promise<${name}Entity | null>`);
|
|
458
|
+
for (const method of entry.customMethods) {
|
|
459
|
+
lines.push(` ${method}(...args: unknown[]): Promise<unknown>`);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
// Add shorthand methods for step.service proxy (create, update, delete)
|
|
463
|
+
// These match what createModuleProxy exposes at runtime
|
|
464
|
+
if (moduleEntries.length > 0) {
|
|
465
|
+
const first = moduleEntries[0];
|
|
466
|
+
const name = first.entityName;
|
|
467
|
+
lines.push(` /** Shorthand: step.service.${moduleName}.create(data) */`);
|
|
468
|
+
lines.push(` create(data: Partial<${name}Entity>): Promise<${name}Entity>`);
|
|
469
|
+
lines.push(` /** Shorthand: step.service.${moduleName}.update(id, data) */`);
|
|
470
|
+
lines.push(` update(id: string, data?: Partial<${name}Entity>): Promise<${name}Entity>`);
|
|
471
|
+
lines.push(` /** Shorthand: step.service.${moduleName}.delete(id) */`);
|
|
472
|
+
lines.push(` delete(id: string): Promise<void>`);
|
|
473
|
+
}
|
|
474
|
+
lines.push('}');
|
|
475
|
+
lines.push('');
|
|
476
|
+
}
|
|
477
|
+
// ── Build event map ─────────────────────────────────────────────────
|
|
478
|
+
const eventMap = new Map();
|
|
479
|
+
// Auto-generated CRUD events from entities (skip external — they have no local CRUD)
|
|
480
|
+
for (const entry of entries) {
|
|
481
|
+
if (entry.isExternal)
|
|
482
|
+
continue;
|
|
483
|
+
const lower = entry.entityName.toLowerCase();
|
|
484
|
+
eventMap.set(`${lower}.created`, '{ id: string }');
|
|
485
|
+
eventMap.set(`${lower}.updated`, '{ id: string }');
|
|
486
|
+
eventMap.set(`${lower}.deleted`, '{ id: string }');
|
|
487
|
+
}
|
|
488
|
+
// Explicit events from commands/subscribers
|
|
489
|
+
for (const event of events) {
|
|
490
|
+
if (!eventMap.has(event.eventName)) {
|
|
491
|
+
if (event.dataFields && Object.keys(event.dataFields).length > 0) {
|
|
492
|
+
const fields = Object.entries(event.dataFields)
|
|
493
|
+
.map(([k, t]) => `${k}: ${t}`)
|
|
494
|
+
.join('; ');
|
|
495
|
+
eventMap.set(event.eventName, `{ ${fields} }`);
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
eventMap.set(event.eventName, 'Record<string, unknown>');
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
const sortedEvents = [...eventMap.keys()].sort();
|
|
503
|
+
// ── Collect commands ────────────────────────────────────────────────
|
|
504
|
+
const commandNames = resources.commands.map((c) => c.id).sort();
|
|
505
|
+
const commandEntries = [];
|
|
506
|
+
for (const cmd of resources.commands) {
|
|
507
|
+
const camel = cmd.id.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
508
|
+
assertSafeIdentifierComponent(camel, 'camelCase', `defineCommand in ${cmd.path}`);
|
|
509
|
+
commandEntries.push({ kebab: cmd.id, camel });
|
|
510
|
+
}
|
|
511
|
+
// ── Collect actors ──────────────────────────────────────────────────
|
|
512
|
+
const actorSet = new Set(['user']);
|
|
513
|
+
for (const ctxInfo of resources.contexts) {
|
|
514
|
+
try {
|
|
515
|
+
const ctxMod = (await import(`${ctxInfo.path}?t=${Date.now()}`));
|
|
516
|
+
const def = ctxMod.default;
|
|
517
|
+
if (def?.actors) {
|
|
518
|
+
const actors = Array.isArray(def.actors) ? def.actors : [def.actors];
|
|
519
|
+
for (const a of actors) {
|
|
520
|
+
assertSafeIdentifierComponent(a, 'camelCase', `defineContext actor in ${ctxInfo.path}`);
|
|
521
|
+
actorSet.add(a);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
catch (err) {
|
|
526
|
+
if (MantaError.is(err))
|
|
527
|
+
throw err;
|
|
528
|
+
console.warn(` [codegen] Warning: failed to import context '${ctxInfo.id}': ${err.message}`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
const sortedActors = [...actorSet].sort();
|
|
532
|
+
// ── Deduplicated module names ───────────────────────────────────────
|
|
533
|
+
const moduleNames = [...new Set(entries.map((e) => e.moduleName))].sort();
|
|
534
|
+
// ── declare global block ────────────────────────────────────────────
|
|
535
|
+
lines.push('declare global {');
|
|
536
|
+
// MantaGeneratedEntities — camelCase keys only
|
|
537
|
+
lines.push(' interface MantaGeneratedEntities {');
|
|
538
|
+
const seenEntityKeys = new Set();
|
|
539
|
+
const entityTypeRef = (entry) => {
|
|
540
|
+
const isUser = entry.isUserModel;
|
|
541
|
+
return isUser ? `typeof ${entry.entityExportName}['model']` : `typeof ${entry.entityExportName}`;
|
|
542
|
+
};
|
|
543
|
+
// Entity keys: camelCase derived from PascalCase entity name
|
|
544
|
+
for (const entry of entries) {
|
|
545
|
+
const camel = entry.entityName.charAt(0).toLowerCase() + entry.entityName.slice(1);
|
|
546
|
+
if (!seenEntityKeys.has(camel)) {
|
|
547
|
+
lines.push(` ${camel}: ${entityTypeRef(entry)}`);
|
|
548
|
+
seenEntityKeys.add(camel);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
// Module name aliases (only if different from entity camelCase)
|
|
552
|
+
for (const [modName, modEntries] of moduleMap) {
|
|
553
|
+
if (!seenEntityKeys.has(modName)) {
|
|
554
|
+
lines.push(` ${modName}: ${entityTypeRef(modEntries[0])}`);
|
|
555
|
+
seenEntityKeys.add(modName);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
lines.push(' }');
|
|
559
|
+
lines.push('');
|
|
560
|
+
// MantaGeneratedEntityRegistry — camelCase keys only
|
|
561
|
+
lines.push(' interface MantaGeneratedEntityRegistry {');
|
|
562
|
+
for (const entry of entries) {
|
|
563
|
+
const camel = entry.entityName.charAt(0).toLowerCase() + entry.entityName.slice(1);
|
|
564
|
+
lines.push(` ${camel}: ${entityTypeRef(entry)}`);
|
|
565
|
+
}
|
|
566
|
+
lines.push(' }');
|
|
567
|
+
lines.push('');
|
|
568
|
+
// MantaGeneratedCommands — includes explicit commands + auto-generated CRUD + link/unlink
|
|
569
|
+
// All names are camelCase for ergonomic step.command.xxx() usage.
|
|
570
|
+
{
|
|
571
|
+
/** Convert any casing to camelCase: 'CustomerGroup' → 'customerGroup', 'customer_group' → 'customerGroup' */
|
|
572
|
+
const toCamel = (name) => name
|
|
573
|
+
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
|
574
|
+
.replace(/[\s_]+/g, '-')
|
|
575
|
+
.toLowerCase()
|
|
576
|
+
.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
577
|
+
const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
|
578
|
+
lines.push(' interface MantaGeneratedCommands {');
|
|
579
|
+
// Explicit commands (from src/commands/)
|
|
580
|
+
for (const cmd of commandEntries) {
|
|
581
|
+
if (cmd.kebab === 'graph')
|
|
582
|
+
continue;
|
|
583
|
+
lines.push(` ${cmd.camel}(input: unknown): Promise<unknown>`);
|
|
584
|
+
}
|
|
585
|
+
// Auto-generated CRUD commands per entity (camelCase)
|
|
586
|
+
for (const entry of entries) {
|
|
587
|
+
const camel = toCamel(entry.entityName);
|
|
588
|
+
const cap = capitalize(camel);
|
|
589
|
+
const capPlural = pluralize(cap);
|
|
590
|
+
lines.push(` create${cap}(input: Record<string, unknown>): Promise<unknown>`);
|
|
591
|
+
lines.push(` update${cap}(input: { id: string } & Record<string, unknown>): Promise<unknown>`);
|
|
592
|
+
lines.push(` delete${cap}(input: { id: string }): Promise<{ id: string; deleted: true }>`);
|
|
593
|
+
lines.push(` retrieve${cap}(input: { id: string }): Promise<unknown>`);
|
|
594
|
+
lines.push(` list${capPlural}(input?: { filters?: Record<string, unknown>; limit?: number; offset?: number }): Promise<unknown[]>`);
|
|
595
|
+
}
|
|
596
|
+
// Auto-generated link/unlink commands (camelCase)
|
|
597
|
+
const emitLinkTypes = (link) => {
|
|
598
|
+
if (!link?.leftEntity || !link?.rightEntity)
|
|
599
|
+
return;
|
|
600
|
+
const leftCap = capitalize(toCamel(link.leftEntity));
|
|
601
|
+
const rightCap = capitalize(toCamel(link.rightEntity));
|
|
602
|
+
const leftFk = link.leftFk ?? `${toCamel(link.leftEntity)}_id`;
|
|
603
|
+
const rightFk = link.rightFk ?? `${toCamel(link.rightEntity)}_id`;
|
|
604
|
+
lines.push(` link${leftCap}${rightCap}(input: { ${leftFk}: string; ${rightFk}: string }): Promise<{ success: true }>`);
|
|
605
|
+
lines.push(` unlink${leftCap}${rightCap}(input: { ${leftFk}: string; ${rightFk}: string }): Promise<{ success: true }>`);
|
|
606
|
+
};
|
|
607
|
+
for (const linkInfo of resources.links) {
|
|
608
|
+
try {
|
|
609
|
+
const mod = (await import(`${linkInfo.path}?t=${Date.now()}`));
|
|
610
|
+
const link = (mod.default ?? mod);
|
|
611
|
+
if (!link?.isDirectFk)
|
|
612
|
+
emitLinkTypes(link);
|
|
613
|
+
}
|
|
614
|
+
catch {
|
|
615
|
+
/* skip */
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
for (const modInfo of resources.modules) {
|
|
619
|
+
for (const linkInfo of modInfo.intraLinks) {
|
|
620
|
+
try {
|
|
621
|
+
const mod = (await import(`${linkInfo.path}?t=${Date.now()}`));
|
|
622
|
+
const link = (mod.default ?? mod);
|
|
623
|
+
if (link?.cardinality === 'M:N' && !link?.isDirectFk)
|
|
624
|
+
emitLinkTypes(link);
|
|
625
|
+
}
|
|
626
|
+
catch {
|
|
627
|
+
/* skip */
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
lines.push(' }');
|
|
632
|
+
lines.push('');
|
|
633
|
+
}
|
|
634
|
+
// MantaGeneratedQueries
|
|
635
|
+
if (resources.queries.length > 0) {
|
|
636
|
+
lines.push(' interface MantaGeneratedQueries {');
|
|
637
|
+
for (const q of resources.queries) {
|
|
638
|
+
if (q.id === 'graph')
|
|
639
|
+
continue; // skip defineQueryGraph entries
|
|
640
|
+
const _camel = q.id.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
641
|
+
lines.push(` '${q.id}': (params?: Record<string, unknown>) => Promise<unknown>`);
|
|
642
|
+
}
|
|
643
|
+
lines.push(' }');
|
|
644
|
+
lines.push('');
|
|
645
|
+
}
|
|
646
|
+
// MantaGeneratedAgents
|
|
647
|
+
if (agentEntries.length > 0) {
|
|
648
|
+
lines.push(' interface MantaGeneratedAgents {');
|
|
649
|
+
for (const agent of agentEntries) {
|
|
650
|
+
lines.push(` ${agent.camel}(input: z.infer<typeof ${agent.camel}Def.input>): Promise<z.infer<typeof ${agent.camel}Def.output>>`);
|
|
651
|
+
}
|
|
652
|
+
lines.push(' }');
|
|
653
|
+
lines.push('');
|
|
654
|
+
}
|
|
655
|
+
// MantaGeneratedEventMap
|
|
656
|
+
lines.push(' /** Event data map — typed event.data for defineSubscriber() and step.emit(). */');
|
|
657
|
+
lines.push(' interface MantaGeneratedEventMap {');
|
|
658
|
+
for (const name of sortedEvents) {
|
|
659
|
+
lines.push(` '${name}': ${eventMap.get(name)}`);
|
|
660
|
+
}
|
|
661
|
+
lines.push(' }');
|
|
662
|
+
lines.push('');
|
|
663
|
+
// DefineSubscriberFn — merge event-specific overloads for autocomplete
|
|
664
|
+
// Interface call signatures with explicit string literals give IDE autocomplete.
|
|
665
|
+
if (sortedEvents.length > 0) {
|
|
666
|
+
lines.push(' /** Codegen overloads — gives autocomplete for event names in defineSubscriber(). */');
|
|
667
|
+
lines.push(' interface DefineSubscriberFn {');
|
|
668
|
+
for (const name of sortedEvents) {
|
|
669
|
+
const dataType = eventMap.get(name);
|
|
670
|
+
lines.push(` (event: '${name}', handler: (event: import('@mantajs/core').Message<${dataType}>, scope: import('@mantajs/core').SubscriberScope) => void | Promise<void>): import('@mantajs/core').SubscriberDefinition<${dataType}> & { __type: 'subscriber' }`);
|
|
671
|
+
}
|
|
672
|
+
lines.push(' }');
|
|
673
|
+
lines.push('');
|
|
674
|
+
}
|
|
675
|
+
// MantaGeneratedAppModules (deduplicated — one entry per module)
|
|
676
|
+
lines.push(' interface MantaGeneratedAppModules {');
|
|
677
|
+
for (const moduleName of moduleNames) {
|
|
678
|
+
lines.push(` ${moduleName}: ${moduleName}Module`);
|
|
679
|
+
}
|
|
680
|
+
lines.push(' }');
|
|
681
|
+
lines.push('');
|
|
682
|
+
// MantaGeneratedRegistry (deduplicated module names)
|
|
683
|
+
lines.push(' interface MantaGeneratedRegistry {');
|
|
684
|
+
if (moduleNames.length > 0) {
|
|
685
|
+
lines.push(' modules: {');
|
|
686
|
+
for (const name of moduleNames) {
|
|
687
|
+
lines.push(` ${name}: true`);
|
|
688
|
+
}
|
|
689
|
+
lines.push(' }');
|
|
690
|
+
}
|
|
691
|
+
if (commandNames.length > 0) {
|
|
692
|
+
lines.push(' commands: {');
|
|
693
|
+
for (const name of commandNames) {
|
|
694
|
+
lines.push(` '${name}': true`);
|
|
695
|
+
}
|
|
696
|
+
lines.push(' }');
|
|
697
|
+
}
|
|
698
|
+
if (sortedActors.length > 0) {
|
|
699
|
+
lines.push(' actors: {');
|
|
700
|
+
for (const actor of sortedActors) {
|
|
701
|
+
lines.push(` ${actor}: true`);
|
|
702
|
+
}
|
|
703
|
+
lines.push(' }');
|
|
704
|
+
}
|
|
705
|
+
lines.push(' }');
|
|
706
|
+
lines.push('}');
|
|
707
|
+
lines.push('');
|
|
708
|
+
lines.push('export {}');
|
|
709
|
+
lines.push('');
|
|
710
|
+
const source = lines.join('\n');
|
|
711
|
+
validateGeneratedTypeScript(source, 'generated.d.ts');
|
|
712
|
+
writeFileSync(outPath, source);
|
|
713
|
+
// ── Generate command schemas for frontend (runtime metadata) ────────
|
|
714
|
+
await generateCommandSchemas(cwd, resources, mantaDir);
|
|
715
|
+
console.log(` [codegen] .manta/generated.d.ts (${entries.length} entities, ${commandNames.length} commands, ${agentEntries.length} agents, ${sortedActors.length} actors, ${sortedEvents.length} events)`);
|
|
716
|
+
}
|
|
717
|
+
/**
|
|
718
|
+
* Extract field metadata from a Zod schema (introspects _def).
|
|
719
|
+
*/
|
|
720
|
+
function extractZodFields(schema) {
|
|
721
|
+
const fields = [];
|
|
722
|
+
// biome-ignore lint/suspicious/noExplicitAny: Zod internal structure
|
|
723
|
+
const s = schema;
|
|
724
|
+
if (!s?._def?.typeName)
|
|
725
|
+
return fields;
|
|
726
|
+
// Must be a ZodObject
|
|
727
|
+
if (s._def.typeName !== 'ZodObject')
|
|
728
|
+
return fields;
|
|
729
|
+
const shape = s._def.shape?.() ?? s._def.shape ?? {};
|
|
730
|
+
for (const [key, fieldSchema] of Object.entries(shape)) {
|
|
731
|
+
// biome-ignore lint/suspicious/noExplicitAny: Zod internal
|
|
732
|
+
let inner = fieldSchema;
|
|
733
|
+
let required = true;
|
|
734
|
+
// Unwrap ZodOptional / ZodDefault
|
|
735
|
+
while (inner?._def) {
|
|
736
|
+
if (inner._def.typeName === 'ZodOptional' || inner._def.typeName === 'ZodNullable') {
|
|
737
|
+
required = false;
|
|
738
|
+
inner = inner._def.innerType;
|
|
739
|
+
}
|
|
740
|
+
else if (inner._def.typeName === 'ZodDefault') {
|
|
741
|
+
required = false;
|
|
742
|
+
inner = inner._def.innerType;
|
|
743
|
+
}
|
|
744
|
+
else {
|
|
745
|
+
break;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
const typeName = inner?._def?.typeName ?? '';
|
|
749
|
+
let type = 'unknown';
|
|
750
|
+
const checks = [];
|
|
751
|
+
let options;
|
|
752
|
+
switch (typeName) {
|
|
753
|
+
case 'ZodString':
|
|
754
|
+
type = 'string';
|
|
755
|
+
// Extract string checks (email, url, min, max, etc.)
|
|
756
|
+
for (const check of inner._def.checks ?? []) {
|
|
757
|
+
if (check.kind === 'email')
|
|
758
|
+
checks.push('email');
|
|
759
|
+
else if (check.kind === 'url')
|
|
760
|
+
checks.push('url');
|
|
761
|
+
else if (check.kind === 'min')
|
|
762
|
+
checks.push(`min:${check.value}`);
|
|
763
|
+
else if (check.kind === 'max')
|
|
764
|
+
checks.push(`max:${check.value}`);
|
|
765
|
+
}
|
|
766
|
+
break;
|
|
767
|
+
case 'ZodNumber':
|
|
768
|
+
type = 'number';
|
|
769
|
+
for (const check of inner._def.checks ?? []) {
|
|
770
|
+
if (check.kind === 'min')
|
|
771
|
+
checks.push(`min:${check.value}`);
|
|
772
|
+
else if (check.kind === 'max')
|
|
773
|
+
checks.push(`max:${check.value}`);
|
|
774
|
+
else if (check.kind === 'int')
|
|
775
|
+
checks.push('int');
|
|
776
|
+
}
|
|
777
|
+
break;
|
|
778
|
+
case 'ZodBoolean':
|
|
779
|
+
type = 'boolean';
|
|
780
|
+
break;
|
|
781
|
+
case 'ZodDate':
|
|
782
|
+
type = 'date';
|
|
783
|
+
break;
|
|
784
|
+
case 'ZodEnum':
|
|
785
|
+
case 'ZodNativeEnum':
|
|
786
|
+
type = 'enum';
|
|
787
|
+
options = inner._def.values;
|
|
788
|
+
break;
|
|
789
|
+
case 'ZodArray':
|
|
790
|
+
type = 'array';
|
|
791
|
+
break;
|
|
792
|
+
default:
|
|
793
|
+
type = 'unknown';
|
|
794
|
+
}
|
|
795
|
+
fields.push({ key, required, type, checks: checks.length > 0 ? checks : undefined, options });
|
|
796
|
+
}
|
|
797
|
+
return fields;
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Generate .manta/command-schemas.ts — runtime metadata for frontend form validation.
|
|
801
|
+
*/
|
|
802
|
+
async function generateCommandSchemas(_cwd, resources, mantaDir) {
|
|
803
|
+
const schemas = {};
|
|
804
|
+
for (const cmdInfo of resources.commands) {
|
|
805
|
+
try {
|
|
806
|
+
const mod = (await import(`${cmdInfo.path}?t=${Date.now()}`));
|
|
807
|
+
const cmd = mod.default;
|
|
808
|
+
if (!cmd?.name || !cmd?.input)
|
|
809
|
+
continue;
|
|
810
|
+
const fields = extractZodFields(cmd.input);
|
|
811
|
+
if (fields.length > 0) {
|
|
812
|
+
schemas[cmd.name] = fields;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
catch {
|
|
816
|
+
// Command may not be importable — skip
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
if (Object.keys(schemas).length === 0)
|
|
820
|
+
return;
|
|
821
|
+
const outPath = resolve(mantaDir, 'command-schemas.ts');
|
|
822
|
+
const content = [
|
|
823
|
+
'// Auto-generated by manta dev — DO NOT EDIT',
|
|
824
|
+
'// Command input schemas for frontend form validation.',
|
|
825
|
+
'',
|
|
826
|
+
`export const commandSchemas: Record<string, Array<{ key: string; required: boolean; type: string; checks?: string[]; options?: string[] }>> = ${JSON.stringify(schemas, null, 2)}`,
|
|
827
|
+
'',
|
|
828
|
+
].join('\n');
|
|
829
|
+
validateGeneratedTypeScript(content, 'command-schemas.ts');
|
|
830
|
+
writeFileSync(outPath, content);
|
|
831
|
+
}
|
|
832
|
+
//# sourceMappingURL=generate-types.js.map
|