@ismaelsoilet/synapsejs 1.1.1 → 1.3.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/synapse.ts +5 -1
- package/package.json +2 -1
- package/src/client/hooks.ts +25 -4
- package/src/client.ts +1 -0
- package/src/compiler/client-bundler.ts +114 -3
- package/src/compiler/migration-runner.ts +18 -1
- package/src/compiler/oracle-runner.ts +38 -4
- package/src/compiler/scaffolder.ts +2 -2
- package/src/compiler/slice-templates.ts +7 -7
- package/src/core/index.ts +1 -0
- package/src/core/validation.ts +57 -0
- package/src/mcp/server.ts +95 -1
- package/src/runtime/client-entry.ts +58 -15
- package/src/runtime/server.ts +62 -11
- package/templates/starter/.jev.json +6 -0
- package/templates/starter/package.json +1 -0
package/bin/synapse.ts
CHANGED
|
@@ -534,7 +534,11 @@ async function main() {
|
|
|
534
534
|
}
|
|
535
535
|
|
|
536
536
|
case 'test': {
|
|
537
|
-
const
|
|
537
|
+
const enableGate =
|
|
538
|
+
process.argv.includes('--gate') ||
|
|
539
|
+
fs.existsSync(path.join(root, '.jev.json')) ||
|
|
540
|
+
Boolean(process.env.JEV_ACTIVE || process.env.JEV_PROVIDER || process.env.TYPESAFE_API_KEY);
|
|
541
|
+
const report = await runSliceOracles(root, { enableJevTriage: enableGate });
|
|
538
542
|
|
|
539
543
|
process.stdout.write(`${JSON.stringify({ operation: 'PBT_ORACLE_TEST_SUITE', ...report }, null, 2)}\n`);
|
|
540
544
|
process.exit(report.status === 'PASS' ? 0 : 1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ismaelsoilet/synapsejs",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Bun-only fullstack framework where a feature is a single contiguous file",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"access": "public"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
+
"@ismaelsoilet/jev-harness": "^0.2.1",
|
|
53
54
|
"@sinclair/typebox": "^0.34.40",
|
|
54
55
|
"fast-check": "^3.23.2",
|
|
55
56
|
"postgres": "^3.4.9",
|
package/src/client/hooks.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
2
|
import type { Result } from '../core/machine-types';
|
|
3
|
+
import { useSynapseContext } from './context';
|
|
3
4
|
|
|
4
5
|
export interface UseSubscriptionOptions {
|
|
5
6
|
enabled?: boolean;
|
|
@@ -86,17 +87,33 @@ export interface UseActionResult<TPayload, TData, TError> {
|
|
|
86
87
|
reset: () => void;
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
export interface UseActionOptions<TData, TError> {
|
|
91
|
+
onSuccess?: (data: TData) => void;
|
|
92
|
+
onError?: (error: TError) => void;
|
|
93
|
+
}
|
|
94
|
+
|
|
89
95
|
/**
|
|
90
96
|
* Hook for executing server actions from client UI with reactive state management.
|
|
97
|
+
* If actionFn is not provided, it automatically resolves the slice's action from SynapseContext.
|
|
91
98
|
*/
|
|
92
99
|
export function useAction<TPayload = unknown, TData = unknown, TError = string>(
|
|
93
|
-
actionFn?: (payload: TPayload) => Promise<Result<TData, TError>>
|
|
100
|
+
actionFn?: ((payload: TPayload) => Promise<Result<TData, TError>>) | null,
|
|
101
|
+
options?: UseActionOptions<TData, TError>
|
|
94
102
|
): UseActionResult<TPayload, TData, TError> {
|
|
95
103
|
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
|
96
104
|
const [data, setData] = useState<TData | null>(null);
|
|
97
105
|
const [error, setError] = useState<TError | null>(null);
|
|
98
106
|
const [isSuccess, setIsSuccess] = useState<boolean>(false);
|
|
99
107
|
|
|
108
|
+
const ctx = useSynapseContext();
|
|
109
|
+
const resolvedAction =
|
|
110
|
+
actionFn ??
|
|
111
|
+
(ctx.props?.action as ((payload: TPayload) => Promise<Result<TData, TError>>) | undefined) ??
|
|
112
|
+
(ctx.props?.onSubmitAction as ((payload: TPayload) => Promise<Result<TData, TError>>) | undefined);
|
|
113
|
+
|
|
114
|
+
const optionsRef = useRef(options);
|
|
115
|
+
optionsRef.current = options;
|
|
116
|
+
|
|
100
117
|
const reset = useCallback(() => {
|
|
101
118
|
setIsSubmitting(false);
|
|
102
119
|
setData(null);
|
|
@@ -106,9 +123,10 @@ export function useAction<TPayload = unknown, TData = unknown, TError = string>(
|
|
|
106
123
|
|
|
107
124
|
const execute = useCallback(
|
|
108
125
|
async (payload: TPayload): Promise<Result<TData, TError>> => {
|
|
109
|
-
if (!
|
|
126
|
+
if (!resolvedAction) {
|
|
110
127
|
const errVal = 'NO_ACTION_FUNCTION' as unknown as TError;
|
|
111
128
|
setError(errVal);
|
|
129
|
+
optionsRef.current?.onError?.(errVal);
|
|
112
130
|
return { ok: false, error: errVal };
|
|
113
131
|
}
|
|
114
132
|
|
|
@@ -117,26 +135,29 @@ export function useAction<TPayload = unknown, TData = unknown, TError = string>(
|
|
|
117
135
|
setIsSuccess(false);
|
|
118
136
|
|
|
119
137
|
try {
|
|
120
|
-
const result = await
|
|
138
|
+
const result = await resolvedAction(payload);
|
|
121
139
|
if (result.ok) {
|
|
122
140
|
setData(result.value);
|
|
123
141
|
setIsSuccess(true);
|
|
124
142
|
setError(null);
|
|
143
|
+
optionsRef.current?.onSuccess?.(result.value);
|
|
125
144
|
} else {
|
|
126
145
|
setError(result.error);
|
|
127
146
|
setIsSuccess(false);
|
|
147
|
+
optionsRef.current?.onError?.(result.error);
|
|
128
148
|
}
|
|
129
149
|
return result;
|
|
130
150
|
} catch (err: unknown) {
|
|
131
151
|
const failure = (err instanceof Error ? err.message : String(err)) as unknown as TError;
|
|
132
152
|
setError(failure);
|
|
133
153
|
setIsSuccess(false);
|
|
154
|
+
optionsRef.current?.onError?.(failure);
|
|
134
155
|
return { ok: false, error: failure };
|
|
135
156
|
} finally {
|
|
136
157
|
setIsSubmitting(false);
|
|
137
158
|
}
|
|
138
159
|
},
|
|
139
|
-
[
|
|
160
|
+
[resolvedAction]
|
|
140
161
|
);
|
|
141
162
|
|
|
142
163
|
return {
|
package/src/client.ts
CHANGED
|
@@ -71,13 +71,77 @@ export async function buildVendorBundle(baseDir: string): Promise<Result<BuiltBu
|
|
|
71
71
|
`import * as ReactDOM from 'react-dom';`,
|
|
72
72
|
`import * as ReactDOMClient from 'react-dom/client';`,
|
|
73
73
|
`import * as SynapseClient from 'synapsejs/client';`,
|
|
74
|
+
`import * as jsxRuntime from 'react/jsx-runtime';`,
|
|
75
|
+
`import * as jsxDevRuntime from 'react/jsx-dev-runtime';`,
|
|
74
76
|
``,
|
|
75
77
|
`export default React;`,
|
|
76
78
|
`export * from 'react';`,
|
|
77
79
|
`export * from 'react-dom';`,
|
|
78
80
|
`export * from 'react-dom/client';`,
|
|
79
81
|
`export * from 'synapsejs/client';`,
|
|
80
|
-
`export
|
|
82
|
+
`export * from 'react/jsx-runtime';`,
|
|
83
|
+
`export * from 'react/jsx-dev-runtime';`,
|
|
84
|
+
`const { hydrateRoot, createRoot } = ReactDOMClient;`,
|
|
85
|
+
`const { jsx, jsxs } = (jsxRuntime as unknown as Record<string, unknown>);`,
|
|
86
|
+
`const { jsxDEV } = (jsxDevRuntime as unknown as Record<string, unknown>);`,
|
|
87
|
+
`const {`,
|
|
88
|
+
` useState,`,
|
|
89
|
+
` useEffect,`,
|
|
90
|
+
` useCallback,`,
|
|
91
|
+
` useMemo,`,
|
|
92
|
+
` useRef,`,
|
|
93
|
+
` useContext,`,
|
|
94
|
+
` useReducer,`,
|
|
95
|
+
` useId,`,
|
|
96
|
+
` useLayoutEffect,`,
|
|
97
|
+
` useTransition,`,
|
|
98
|
+
` useDeferredValue,`,
|
|
99
|
+
` createContext,`,
|
|
100
|
+
` createElement,`,
|
|
101
|
+
` cloneElement,`,
|
|
102
|
+
` isValidElement,`,
|
|
103
|
+
` Fragment,`,
|
|
104
|
+
` Component,`,
|
|
105
|
+
` PureComponent,`,
|
|
106
|
+
` memo,`,
|
|
107
|
+
` forwardRef,`,
|
|
108
|
+
` Children,`,
|
|
109
|
+
` startTransition`,
|
|
110
|
+
`} = (React as unknown as Record<string, unknown>);`,
|
|
111
|
+
`export {`,
|
|
112
|
+
` ReactDOM,`,
|
|
113
|
+
` ReactDOMClient,`,
|
|
114
|
+
` SynapseClient,`,
|
|
115
|
+
` jsxRuntime,`,
|
|
116
|
+
` jsxDevRuntime,`,
|
|
117
|
+
` hydrateRoot,`,
|
|
118
|
+
` createRoot,`,
|
|
119
|
+
` jsx,`,
|
|
120
|
+
` jsxs,`,
|
|
121
|
+
` jsxDEV,`,
|
|
122
|
+
` useState,`,
|
|
123
|
+
` useEffect,`,
|
|
124
|
+
` useCallback,`,
|
|
125
|
+
` useMemo,`,
|
|
126
|
+
` useRef,`,
|
|
127
|
+
` useContext,`,
|
|
128
|
+
` useReducer,`,
|
|
129
|
+
` useId,`,
|
|
130
|
+
` useLayoutEffect,`,
|
|
131
|
+
` useTransition,`,
|
|
132
|
+
` useDeferredValue,`,
|
|
133
|
+
` createContext,`,
|
|
134
|
+
` createElement,`,
|
|
135
|
+
` cloneElement,`,
|
|
136
|
+
` isValidElement,`,
|
|
137
|
+
` Fragment,`,
|
|
138
|
+
` Component,`,
|
|
139
|
+
` PureComponent,`,
|
|
140
|
+
` memo,`,
|
|
141
|
+
` forwardRef,`,
|
|
142
|
+
` Children,`,
|
|
143
|
+
` startTransition`,
|
|
144
|
+
`};`,
|
|
81
145
|
``
|
|
82
146
|
].join('\n');
|
|
83
147
|
|
|
@@ -194,12 +258,51 @@ export async function buildClientBundle(
|
|
|
194
258
|
const entryPath = path.join(entryDir, `${target.domain}-${target.name}.tsx`);
|
|
195
259
|
const modulePath = path.relative(entryDir, path.join(outDir, clientArtifact.fileName)).split(path.sep).join('/');
|
|
196
260
|
|
|
261
|
+
// Detect optional root and domain layouts for isomorphic hydration
|
|
262
|
+
let rootLayoutPath: string | undefined;
|
|
263
|
+
let domainLayoutPath: string | undefined;
|
|
264
|
+
|
|
265
|
+
let currentDir = path.dirname(target.filePath);
|
|
266
|
+
let slicesDir: string | null = null;
|
|
267
|
+
while (currentDir && currentDir !== baseDir && currentDir !== path.dirname(currentDir)) {
|
|
268
|
+
if (path.basename(currentDir) === 'slices') {
|
|
269
|
+
slicesDir = currentDir;
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
currentDir = path.dirname(currentDir);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (slicesDir) {
|
|
276
|
+
const rootCandidates = [path.join(slicesDir, '_layout.tsx'), path.join(slicesDir, 'layout.tsx')];
|
|
277
|
+
for (const cand of rootCandidates) {
|
|
278
|
+
if (fs.existsSync(cand)) {
|
|
279
|
+
const rel = path.relative(entryDir, cand).split(path.sep).join('/');
|
|
280
|
+
rootLayoutPath = rel.startsWith('.') ? rel : `./${rel}`;
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const domainDir = path.dirname(target.filePath);
|
|
286
|
+
if (domainDir !== slicesDir) {
|
|
287
|
+
const domainCandidates = [path.join(domainDir, '_layout.tsx'), path.join(domainDir, 'layout.tsx')];
|
|
288
|
+
for (const cand of domainCandidates) {
|
|
289
|
+
if (fs.existsSync(cand)) {
|
|
290
|
+
const rel = path.relative(entryDir, cand).split(path.sep).join('/');
|
|
291
|
+
domainLayoutPath = rel.startsWith('.') ? rel : `./${rel}`;
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
197
298
|
fs.writeFileSync(
|
|
198
299
|
entryPath,
|
|
199
300
|
clientEntrySource({
|
|
200
301
|
componentName: componentExport,
|
|
201
302
|
clientModulePath: `./${modulePath}`,
|
|
202
|
-
rpcPath: target.rpcPath
|
|
303
|
+
rpcPath: target.rpcPath,
|
|
304
|
+
rootLayoutPath,
|
|
305
|
+
domainLayoutPath
|
|
203
306
|
}),
|
|
204
307
|
'utf-8'
|
|
205
308
|
);
|
|
@@ -224,7 +327,15 @@ export async function buildClientBundle(
|
|
|
224
327
|
naming: '[name].js',
|
|
225
328
|
minify: production,
|
|
226
329
|
external: vendorSplit
|
|
227
|
-
? [
|
|
330
|
+
? [
|
|
331
|
+
'react',
|
|
332
|
+
'react/jsx-runtime',
|
|
333
|
+
'react/jsx-dev-runtime',
|
|
334
|
+
'react-dom',
|
|
335
|
+
'react-dom/client',
|
|
336
|
+
'synapsejs/client',
|
|
337
|
+
'@ismaelsoilet/synapsejs/client'
|
|
338
|
+
]
|
|
228
339
|
: [],
|
|
229
340
|
define: { 'process.env.NODE_ENV': production ? '"production"' : '"development"' }
|
|
230
341
|
});
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* Preserves Locality of Behavior (LoB): each slice owns its own table contracts!
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { type AbortGateResult, shouldAbortTrajectory } from '@ismaelsoilet/jev-harness';
|
|
9
10
|
import * as fs from 'fs';
|
|
10
11
|
import * as path from 'path';
|
|
11
12
|
import * as ts from 'typescript';
|
|
@@ -34,6 +35,7 @@ export interface MigrationReport {
|
|
|
34
35
|
code?: SlicesDirErrorCode;
|
|
35
36
|
message?: string;
|
|
36
37
|
candidates?: string[];
|
|
38
|
+
abortSignal?: AbortGateResult;
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
export interface BidirectionalDdl {
|
|
@@ -404,13 +406,28 @@ export async function runSliceMigrations(
|
|
|
404
406
|
}
|
|
405
407
|
}
|
|
406
408
|
|
|
409
|
+
let abortSignal: AbortGateResult | undefined;
|
|
410
|
+
if (hasFailure) {
|
|
411
|
+
try {
|
|
412
|
+
const failureDetails = migrations
|
|
413
|
+
.filter((m) => m.status === 'FAILED')
|
|
414
|
+
.map((m) => `Slice ${m.slice}: ${m.error}`)
|
|
415
|
+
.join('\n');
|
|
416
|
+
abortSignal = await shouldAbortTrajectory(
|
|
417
|
+
`Apply sliceSchema migrations across discovered slices in ${baseDir}`,
|
|
418
|
+
failureDetails
|
|
419
|
+
);
|
|
420
|
+
} catch {}
|
|
421
|
+
}
|
|
422
|
+
|
|
407
423
|
return {
|
|
408
424
|
status: hasFailure ? 'FAIL' : 'PASS',
|
|
409
425
|
totalDiscovered: migrations.length,
|
|
410
426
|
appliedCount,
|
|
411
427
|
skippedCount,
|
|
412
428
|
statementsApplied: statementsAppliedTotal,
|
|
413
|
-
migrations
|
|
429
|
+
migrations,
|
|
430
|
+
abortSignal
|
|
414
431
|
};
|
|
415
432
|
} finally {
|
|
416
433
|
if (isPostgres) {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* Nothing here may report PASS when no invariant was executed.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import { type TestTriageResult, triageTestFailure } from '@ismaelsoilet/jev-harness';
|
|
12
13
|
import * as fs from 'fs';
|
|
13
14
|
import * as path from 'path';
|
|
14
15
|
import { findSliceFiles, resolveSlicesDir, type SlicesDirErrorCode } from './slice-discovery';
|
|
@@ -42,6 +43,7 @@ export interface OracleReport {
|
|
|
42
43
|
code?: OracleErrorCode;
|
|
43
44
|
message?: string;
|
|
44
45
|
candidates?: string[];
|
|
46
|
+
triage?: TestTriageResult;
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
export interface JunitSuite {
|
|
@@ -154,6 +156,8 @@ export function oracleWrapperSource(importPath: string, sliceName: string): stri
|
|
|
154
156
|
export interface OracleRunOptions {
|
|
155
157
|
/** Injected by tests: runs a command and returns its exit code and output. */
|
|
156
158
|
spawn?: (args: string[], cwd: string) => Promise<{ exitCode: number; output: string }>;
|
|
159
|
+
/** Enable Jev System One semantic test triage on failures. Default: true */
|
|
160
|
+
enableJevTriage?: boolean;
|
|
157
161
|
}
|
|
158
162
|
|
|
159
163
|
async function defaultSpawn(args: string[], cwd: string): Promise<{ exitCode: number; output: string }> {
|
|
@@ -241,6 +245,12 @@ export async function runSliceOracles(
|
|
|
241
245
|
);
|
|
242
246
|
|
|
243
247
|
if (!fs.existsSync(reportPath)) {
|
|
248
|
+
let triageResult: TestTriageResult | undefined;
|
|
249
|
+
if (options.enableJevTriage !== false) {
|
|
250
|
+
try {
|
|
251
|
+
triageResult = await triageTestFailure(output.trim() || `O runner Bun test falhou com exit ${exitCode}`);
|
|
252
|
+
} catch {}
|
|
253
|
+
}
|
|
244
254
|
return {
|
|
245
255
|
status: 'FAIL',
|
|
246
256
|
totalSlices: wrappers.size,
|
|
@@ -249,7 +259,8 @@ export async function runSliceOracles(
|
|
|
249
259
|
passedCases: 0,
|
|
250
260
|
results: [],
|
|
251
261
|
code: 'RUNNER_FAILED',
|
|
252
|
-
message: `O runner não produziu relatório (exit ${exitCode}). ${output.trim().slice(-400)}`.trim()
|
|
262
|
+
message: `O runner não produziu relatório (exit ${exitCode}). ${output.trim().slice(-400)}`.trim(),
|
|
263
|
+
triage: triageResult
|
|
253
264
|
};
|
|
254
265
|
}
|
|
255
266
|
|
|
@@ -268,6 +279,12 @@ export async function runSliceOracles(
|
|
|
268
279
|
const passedSlices = results.filter((item) => item.passed).length;
|
|
269
280
|
|
|
270
281
|
if (totalCases === 0) {
|
|
282
|
+
let triageResult: TestTriageResult | undefined;
|
|
283
|
+
if (options.enableJevTriage !== false) {
|
|
284
|
+
try {
|
|
285
|
+
triageResult = await triageTestFailure(`Nenhum invariante foi executado em ${results.length} fatia(s).`);
|
|
286
|
+
} catch {}
|
|
287
|
+
}
|
|
271
288
|
return {
|
|
272
289
|
status: 'FAIL',
|
|
273
290
|
slicesDir: path.relative(root, slicesDir),
|
|
@@ -277,17 +294,34 @@ export async function runSliceOracles(
|
|
|
277
294
|
passedCases: 0,
|
|
278
295
|
results,
|
|
279
296
|
code: 'NO_CASES',
|
|
280
|
-
message: `Nenhum invariante foi executado em ${results.length} fatia(s)
|
|
297
|
+
message: `Nenhum invariante foi executado em ${results.length} fatia(s).`,
|
|
298
|
+
triage: triageResult
|
|
281
299
|
};
|
|
282
300
|
}
|
|
283
301
|
|
|
302
|
+
const isPass = passedSlices === results.length;
|
|
303
|
+
let triageResult: TestTriageResult | undefined;
|
|
304
|
+
if (!isPass && options.enableJevTriage !== false) {
|
|
305
|
+
try {
|
|
306
|
+
const failedSummaries = results
|
|
307
|
+
.filter((r) => !r.passed)
|
|
308
|
+
.flatMap((r) =>
|
|
309
|
+
r.cases.filter((c) => !c.passed).map((c) => `[${r.slice}] ${c.name}: ${c.message || 'assertion failed'}`)
|
|
310
|
+
)
|
|
311
|
+
.join('\n');
|
|
312
|
+
const failureTrace = failedSummaries || output.trim() || 'Slice oracle invariants failed';
|
|
313
|
+
triageResult = await triageTestFailure(failureTrace);
|
|
314
|
+
} catch {}
|
|
315
|
+
}
|
|
316
|
+
|
|
284
317
|
return {
|
|
285
|
-
status:
|
|
318
|
+
status: isPass ? 'PASS' : 'FAIL',
|
|
286
319
|
slicesDir: path.relative(root, slicesDir),
|
|
287
320
|
totalSlices: results.length,
|
|
288
321
|
passedSlices,
|
|
289
322
|
totalCases,
|
|
290
323
|
passedCases,
|
|
291
|
-
results
|
|
324
|
+
results,
|
|
325
|
+
triage: triageResult
|
|
292
326
|
};
|
|
293
327
|
}
|
|
@@ -61,7 +61,7 @@ import {
|
|
|
61
61
|
// ============================================================================
|
|
62
62
|
export const ${inputSchemaName} = Type.Object({
|
|
63
63
|
${tbProps}
|
|
64
|
-
});
|
|
64
|
+
}, { additionalProperties: true });
|
|
65
65
|
export type ${inputTypeName} = Static<typeof ${inputSchemaName}>;
|
|
66
66
|
|
|
67
67
|
// DDL Schema Declarativo da Fatia (Auto-Migrado pelo Synapse)
|
|
@@ -211,7 +211,7 @@ export const ${inputSchemaName} = Type.Object({
|
|
|
211
211
|
name: Type.String({ minLength: 2, maxLength: 100 }),
|
|
212
212
|
email: Type.String({ format: 'email' }),
|
|
213
213
|
metadata: Type.Optional(Type.String())
|
|
214
|
-
});
|
|
214
|
+
}, { additionalProperties: true });
|
|
215
215
|
export type ${inputTypeName} = Static<typeof ${inputSchemaName}>;
|
|
216
216
|
|
|
217
217
|
// DDL Schema Declarativo da Fatia (Auto-Migrado pelo Synapse)
|
|
@@ -88,7 +88,7 @@ export const ${names.schemaName} = Type.Object({
|
|
|
88
88
|
query: Type.Optional(Type.String({ maxLength: 60 })),
|
|
89
89
|
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 100 })),
|
|
90
90
|
offset: Type.Optional(Type.Integer({ minimum: 0 }))
|
|
91
|
-
});
|
|
91
|
+
}, { additionalProperties: true });
|
|
92
92
|
export type ${names.inputName} = Static<typeof ${names.schemaName}>;
|
|
93
93
|
|
|
94
94
|
// ============================================================================
|
|
@@ -236,7 +236,7 @@ export const ${names.schemaName} = Type.Object({
|
|
|
236
236
|
query: Type.Optional(Type.String({ maxLength: 60 })),
|
|
237
237
|
limit: Type.Optional(Type.Integer({ minimum: 1, maximum: 100 })),
|
|
238
238
|
offset: Type.Optional(Type.Integer({ minimum: 0 }))
|
|
239
|
-
});
|
|
239
|
+
}, { additionalProperties: true });
|
|
240
240
|
export type ${names.inputName} = Static<typeof ${names.schemaName}>;
|
|
241
241
|
|
|
242
242
|
// A tabela pertence a outra fatia desta mesma feature.
|
|
@@ -450,7 +450,7 @@ function updateTemplate(names: TemplateNames): string {
|
|
|
450
450
|
export const ${names.schemaName} = Type.Object({
|
|
451
451
|
id: Type.String({ minLength: 1 }),
|
|
452
452
|
name: Type.Optional(Type.String({ minLength: 2, maxLength: 120 }))
|
|
453
|
-
});
|
|
453
|
+
}, { additionalProperties: true });
|
|
454
454
|
export type ${names.inputName} = Static<typeof ${names.schemaName}>;
|
|
455
455
|
|
|
456
456
|
// ============================================================================
|
|
@@ -657,7 +657,7 @@ function deleteTemplate(names: TemplateNames): string {
|
|
|
657
657
|
// ============================================================================
|
|
658
658
|
export const ${names.schemaName} = Type.Object({
|
|
659
659
|
id: Type.String({ minLength: 1 })
|
|
660
|
-
});
|
|
660
|
+
}, { additionalProperties: true });
|
|
661
661
|
export type ${names.inputName} = Static<typeof ${names.schemaName}>;
|
|
662
662
|
|
|
663
663
|
// ============================================================================
|
|
@@ -806,7 +806,7 @@ import { storeSession } from 'synapsejs/client';
|
|
|
806
806
|
export const ${names.schemaName} = Type.Object({
|
|
807
807
|
email: Type.String({ minLength: 3, maxLength: 200 }),
|
|
808
808
|
password: Type.String({ minLength: 8, maxLength: 200 })
|
|
809
|
-
});
|
|
809
|
+
}, { additionalProperties: true });
|
|
810
810
|
export type ${names.inputName} = Static<typeof ${names.schemaName}>;
|
|
811
811
|
|
|
812
812
|
// ============================================================================
|
|
@@ -1075,7 +1075,7 @@ import {
|
|
|
1075
1075
|
export const ${names.schemaName} = Type.Object({
|
|
1076
1076
|
code: Type.String({ minLength: 1, maxLength: 500 }),
|
|
1077
1077
|
state: Type.Optional(Type.String({ maxLength: 500 }))
|
|
1078
|
-
});
|
|
1078
|
+
}, { additionalProperties: true });
|
|
1079
1079
|
export type ${names.inputName} = Static<typeof ${names.schemaName}>;
|
|
1080
1080
|
|
|
1081
1081
|
// ============================================================================
|
|
@@ -1353,7 +1353,7 @@ import { signSessionToken } from 'synapsejs';
|
|
|
1353
1353
|
export const ${names.schemaName} = Type.Object({
|
|
1354
1354
|
userId: Type.String({ minLength: 1 }),
|
|
1355
1355
|
code: Type.String({ minLength: 6, maxLength: 6 })
|
|
1356
|
-
});
|
|
1356
|
+
}, { additionalProperties: true });
|
|
1357
1357
|
export type ${names.inputName} = Static<typeof ${names.schemaName}>;
|
|
1358
1358
|
|
|
1359
1359
|
// ============================================================================
|
package/src/core/index.ts
CHANGED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SynapseJS - Schema Validation Utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides structured, human-readable validation for TypeBox contracts.
|
|
5
|
+
* Rather than returning an opaque 'INVALID_SCHEMA', it extracts the exact
|
|
6
|
+
* paths and failure reasons to provide actionable machine and developer diagnostics.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Static, TSchema } from '@sinclair/typebox';
|
|
10
|
+
import { Value } from '@sinclair/typebox/value';
|
|
11
|
+
import { Err, Ok, type Result } from './machine-types';
|
|
12
|
+
|
|
13
|
+
export interface ValidationErrorDetail {
|
|
14
|
+
path: string;
|
|
15
|
+
message: string;
|
|
16
|
+
value?: unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ValidationFailure {
|
|
20
|
+
code: 'INVALID_SCHEMA';
|
|
21
|
+
message: string;
|
|
22
|
+
errors: ValidationErrorDetail[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Validates a payload against a TypeBox schema with detailed error reporting.
|
|
27
|
+
* Returns Ok(validatedValue) or Err with formatted diagnosis.
|
|
28
|
+
*/
|
|
29
|
+
export function validateSchema<T extends TSchema>(schema: T, payload: unknown): Result<Static<T>, string> {
|
|
30
|
+
if (Value.Check(schema, payload)) {
|
|
31
|
+
return Ok(payload as Static<T>);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const errors: ValidationErrorDetail[] = [];
|
|
35
|
+
try {
|
|
36
|
+
for (const err of Value.Errors(schema, payload)) {
|
|
37
|
+
errors.push({
|
|
38
|
+
path: err.path || '/',
|
|
39
|
+
message: err.message,
|
|
40
|
+
value: err.value
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
// Se a extração de erros falhar, mantém fallback seguro
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (errors.length === 0) {
|
|
48
|
+
return Err('INVALID_SCHEMA: Falha na validação do contrato de entrada.');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const formattedDetails = errors
|
|
52
|
+
.slice(0, 3)
|
|
53
|
+
.map((e) => `campo "${e.path}": ${e.message}`)
|
|
54
|
+
.join('; ');
|
|
55
|
+
|
|
56
|
+
return Err(`INVALID_SCHEMA: ${formattedDetails}`);
|
|
57
|
+
}
|
package/src/mcp/server.ts
CHANGED
|
@@ -6,6 +6,16 @@
|
|
|
6
6
|
* directly to AI Agents (Cursor, Claude Code, Windsurf, Antigravity) via JSON-RPC 2.0 over stdio.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import {
|
|
10
|
+
type AbortGateResult,
|
|
11
|
+
modulateReasoningEffort,
|
|
12
|
+
type ReasoningEffortResult,
|
|
13
|
+
shouldAbortTrajectory,
|
|
14
|
+
type TestTriageResult,
|
|
15
|
+
triageTestFailure,
|
|
16
|
+
type VerificationResult,
|
|
17
|
+
verifyStepCompletion
|
|
18
|
+
} from '@ismaelsoilet/jev-harness';
|
|
9
19
|
import * as fs from 'fs';
|
|
10
20
|
import * as path from 'path';
|
|
11
21
|
import * as readline from 'readline';
|
|
@@ -80,7 +90,7 @@ export class SynapseMcpServer {
|
|
|
80
90
|
},
|
|
81
91
|
serverInfo: {
|
|
82
92
|
name: 'synapse-mcp',
|
|
83
|
-
version: '1.
|
|
93
|
+
version: '1.2.0'
|
|
84
94
|
}
|
|
85
95
|
}
|
|
86
96
|
};
|
|
@@ -201,6 +211,67 @@ export class SynapseMcpServer {
|
|
|
201
211
|
steps: { type: 'number', description: 'Number of recent migrations to rollback (default: 1)' }
|
|
202
212
|
}
|
|
203
213
|
}
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
name: 'synapse_test_gate',
|
|
217
|
+
description:
|
|
218
|
+
'Execute Fast-Check PBT oracles and immediately triage failures with Jev System One semantic test-gate to prevent LLM token waste',
|
|
219
|
+
inputSchema: {
|
|
220
|
+
type: 'object',
|
|
221
|
+
properties: {}
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
name: 'synapse_abort_check',
|
|
226
|
+
description:
|
|
227
|
+
'Evaluate plan and error history with Jev System One to detect doomed trajectories, circular loops, or high-risk refactor dead ends',
|
|
228
|
+
inputSchema: {
|
|
229
|
+
type: 'object',
|
|
230
|
+
required: ['plan', 'history'],
|
|
231
|
+
properties: {
|
|
232
|
+
plan: { type: 'string', description: 'Proposed implementation plan or architectural change' },
|
|
233
|
+
history: { type: 'string', description: 'Recent attempt history, error logs, or failure reasons' }
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
name: 'synapse_verify_completion',
|
|
239
|
+
description:
|
|
240
|
+
'Verify slice implementation and test outputs against acceptance criteria using Jev System One before committing work',
|
|
241
|
+
inputSchema: {
|
|
242
|
+
type: 'object',
|
|
243
|
+
required: ['criteria', 'output'],
|
|
244
|
+
properties: {
|
|
245
|
+
criteria: { type: 'string', description: 'Acceptance criteria or requirement specifications' },
|
|
246
|
+
output: {
|
|
247
|
+
type: 'string',
|
|
248
|
+
description: 'Actual implementation summary, test output, or slice behavior'
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
name: 'synapse_reasoning_effort',
|
|
255
|
+
description:
|
|
256
|
+
'Dynamically modulate agent reasoning effort (Astra-Jev) to low/medium/high based on step context (lowering effort for mechanical commands like split/migrate/skeleton)',
|
|
257
|
+
inputSchema: {
|
|
258
|
+
type: 'object',
|
|
259
|
+
required: ['context'],
|
|
260
|
+
properties: {
|
|
261
|
+
context: {
|
|
262
|
+
type: 'string',
|
|
263
|
+
description: 'Immediate command or task context, e.g. "synapse split", "git commit"'
|
|
264
|
+
},
|
|
265
|
+
provider: {
|
|
266
|
+
type: 'string',
|
|
267
|
+
description: 'Target LLM provider, e.g. "deepseek", "anthropic", "openai"'
|
|
268
|
+
},
|
|
269
|
+
sessionContextTokens: {
|
|
270
|
+
type: 'number',
|
|
271
|
+
description: 'Estimated session context token count to protect prompt cache'
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
204
275
|
}
|
|
205
276
|
]
|
|
206
277
|
}
|
|
@@ -503,6 +574,29 @@ export class SynapseMcpServer {
|
|
|
503
574
|
steps: args.steps
|
|
504
575
|
});
|
|
505
576
|
contentText = JSON.stringify(report, null, 2);
|
|
577
|
+
} else if (toolName === 'synapse_test_gate') {
|
|
578
|
+
const report = await runSliceOracles(this.root, { enableJevTriage: true });
|
|
579
|
+
contentText = JSON.stringify(report, null, 2);
|
|
580
|
+
} else if (toolName === 'synapse_abort_check') {
|
|
581
|
+
const plan = String(args.plan || '');
|
|
582
|
+
const history = String(args.history || '');
|
|
583
|
+
const result: AbortGateResult = await shouldAbortTrajectory(plan, history);
|
|
584
|
+
contentText = JSON.stringify(result, null, 2);
|
|
585
|
+
} else if (toolName === 'synapse_verify_completion') {
|
|
586
|
+
const criteria = String(args.criteria || '');
|
|
587
|
+
const output = String(args.output || '');
|
|
588
|
+
const result: VerificationResult = await verifyStepCompletion(criteria, output);
|
|
589
|
+
contentText = JSON.stringify(result, null, 2);
|
|
590
|
+
} else if (toolName === 'synapse_reasoning_effort') {
|
|
591
|
+
const context = String(args.context || '');
|
|
592
|
+
const provider = args.provider ? String(args.provider) : undefined;
|
|
593
|
+
const sessionContextTokens =
|
|
594
|
+
typeof args.sessionContextTokens === 'number' ? args.sessionContextTokens : undefined;
|
|
595
|
+
const result: ReasoningEffortResult = await modulateReasoningEffort(context, {
|
|
596
|
+
provider,
|
|
597
|
+
sessionContextTokens
|
|
598
|
+
});
|
|
599
|
+
contentText = JSON.stringify(result, null, 2);
|
|
506
600
|
} else {
|
|
507
601
|
return {
|
|
508
602
|
jsonrpc: '2.0',
|
|
@@ -17,6 +17,8 @@ export interface ClientEntryOptions {
|
|
|
17
17
|
rpcPath: string;
|
|
18
18
|
rootId?: string;
|
|
19
19
|
actionPropNames?: string[];
|
|
20
|
+
rootLayoutPath?: string;
|
|
21
|
+
domainLayoutPath?: string;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
export const CLIENT_ROOT_ID = 'synapse-root';
|
|
@@ -26,14 +28,25 @@ export function clientEntrySource(options: ClientEntryOptions): string {
|
|
|
26
28
|
const rootId = options.rootId ?? CLIENT_ROOT_ID;
|
|
27
29
|
const propNames = options.actionPropNames ?? ['onSubmitAction', 'action'];
|
|
28
30
|
const actionWires = propNames
|
|
29
|
-
.map((name) => `
|
|
31
|
+
.map((name) => ` ${name}: (payload: unknown) => rpcCall(${JSON.stringify(options.rpcPath)}, payload)`)
|
|
30
32
|
.join(',\n');
|
|
31
33
|
|
|
32
|
-
|
|
34
|
+
const layoutImports: string[] = [];
|
|
35
|
+
if (options.rootLayoutPath) {
|
|
36
|
+
layoutImports.push(`import * as RootLayoutModule from ${JSON.stringify(options.rootLayoutPath)};`);
|
|
37
|
+
}
|
|
38
|
+
if (options.domainLayoutPath) {
|
|
39
|
+
layoutImports.push(`import * as DomainLayoutModule from ${JSON.stringify(options.domainLayoutPath)};`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const hasLayouts = Boolean(options.rootLayoutPath || options.domainLayoutPath);
|
|
43
|
+
|
|
44
|
+
const lines = [
|
|
33
45
|
`// [SYNAPSE-JS GENERATED CLIENT ENTRY] hydrates the slice in the browser`,
|
|
34
46
|
`import React from 'react';`,
|
|
35
47
|
`import { hydrateRoot } from 'react-dom/client';`,
|
|
36
|
-
`import { rpcCall } from 'synapsejs/client';`,
|
|
48
|
+
`import { rpcCall, SynapseProvider } from 'synapsejs/client';`,
|
|
49
|
+
...layoutImports,
|
|
37
50
|
`import { ${options.componentName} } from ${JSON.stringify(options.clientModulePath)};`,
|
|
38
51
|
``,
|
|
39
52
|
`export function hydrate() {`,
|
|
@@ -41,19 +54,49 @@ export function clientEntrySource(options: ClientEntryOptions): string {
|
|
|
41
54
|
` const props = (scope[${JSON.stringify(CLIENT_PROPS_GLOBAL)}] ?? {}) as Record<string, unknown>;`,
|
|
42
55
|
` const root = document.getElementById(${JSON.stringify(rootId)});`,
|
|
43
56
|
` if (root) {`,
|
|
44
|
-
`
|
|
45
|
-
`
|
|
46
|
-
` React.createElement(${options.componentName}, {`,
|
|
47
|
-
` ...props,`,
|
|
57
|
+
` const sliceElement = React.createElement(${options.componentName}, {`,
|
|
58
|
+
` ...props,`,
|
|
48
59
|
`${actionWires}`,
|
|
49
|
-
`
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
60
|
+
` });`
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
if (hasLayouts) {
|
|
64
|
+
if (options.domainLayoutPath) {
|
|
65
|
+
lines.push(
|
|
66
|
+
` const DomainLayout = (DomainLayoutModule as any).default || (DomainLayoutModule as any).Layout || null;`
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
if (options.rootLayoutPath) {
|
|
70
|
+
lines.push(
|
|
71
|
+
` const RootLayout = (RootLayoutModule as any).default || (RootLayoutModule as any).Layout || null;`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
let treeExpr = 'sliceElement';
|
|
75
|
+
if (options.domainLayoutPath) {
|
|
76
|
+
treeExpr = `(typeof DomainLayout === 'function' ? React.createElement(DomainLayout, { ...props }, ${treeExpr}) : ${treeExpr})`;
|
|
77
|
+
}
|
|
78
|
+
if (options.rootLayoutPath) {
|
|
79
|
+
treeExpr = `(typeof RootLayout === 'function' ? React.createElement(RootLayout, { ...props }, ${treeExpr}) : ${treeExpr})`;
|
|
80
|
+
}
|
|
81
|
+
lines.push(` const treeWithLayout = ${treeExpr};`);
|
|
82
|
+
lines.push(` hydrateRoot(`);
|
|
83
|
+
lines.push(` root,`);
|
|
84
|
+
lines.push(` React.createElement(SynapseProvider, { props, session: props.session as any }, treeWithLayout)`);
|
|
85
|
+
lines.push(` );`);
|
|
86
|
+
} else {
|
|
87
|
+
lines.push(` hydrateRoot(`);
|
|
88
|
+
lines.push(` root,`);
|
|
89
|
+
lines.push(` React.createElement(SynapseProvider, { props, session: props.session as any }, sliceElement)`);
|
|
90
|
+
lines.push(` );`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
lines.push(` }`);
|
|
94
|
+
lines.push(`}`);
|
|
95
|
+
lines.push(``);
|
|
96
|
+
lines.push(`hydrate();`);
|
|
97
|
+
lines.push(``);
|
|
98
|
+
|
|
99
|
+
return lines.join('\n');
|
|
57
100
|
}
|
|
58
101
|
|
|
59
102
|
/**
|
package/src/runtime/server.ts
CHANGED
|
@@ -356,7 +356,8 @@ export class SynapseServer {
|
|
|
356
356
|
for (const cand of layoutCandidates) {
|
|
357
357
|
if (fs.existsSync(cand)) {
|
|
358
358
|
try {
|
|
359
|
-
const
|
|
359
|
+
const candStat = fs.statSync(cand);
|
|
360
|
+
const layoutMod = await import(`${cand}?t=${candStat.mtimeMs}`);
|
|
360
361
|
this.layoutComponent = layoutMod.default || layoutMod.Layout || null;
|
|
361
362
|
break;
|
|
362
363
|
} catch (e) {
|
|
@@ -380,7 +381,8 @@ export class SynapseServer {
|
|
|
380
381
|
for (const cand of domainCandidates) {
|
|
381
382
|
if (fs.existsSync(cand)) {
|
|
382
383
|
try {
|
|
383
|
-
const
|
|
384
|
+
const candStat = fs.statSync(cand);
|
|
385
|
+
const domainMod = await import(`${cand}?t=${candStat.mtimeMs}`);
|
|
384
386
|
const domainComp = domainMod.default || domainMod.Layout || null;
|
|
385
387
|
if (domainComp) {
|
|
386
388
|
this.domainLayouts.set(domain, domainComp);
|
|
@@ -399,7 +401,8 @@ export class SynapseServer {
|
|
|
399
401
|
}
|
|
400
402
|
|
|
401
403
|
try {
|
|
402
|
-
const
|
|
404
|
+
const fileStat = fs.existsSync(file) ? fs.statSync(file) : null;
|
|
405
|
+
const mod = await import(fileStat ? `${file}?t=${fileStat.mtimeMs}` : file);
|
|
403
406
|
let actionFn: any = null;
|
|
404
407
|
let componentFn: any = null;
|
|
405
408
|
let componentExport: string | undefined;
|
|
@@ -624,10 +627,12 @@ export class SynapseServer {
|
|
|
624
627
|
return null;
|
|
625
628
|
}
|
|
626
629
|
|
|
627
|
-
const
|
|
630
|
+
const stat = fs.existsSync(slice.filePath) ? fs.statSync(slice.filePath) : null;
|
|
631
|
+
const signature = stat ? String(stat.mtimeMs) : 'missing';
|
|
628
632
|
const cached = this.clientBundles.get(slice.key);
|
|
629
633
|
if (cached?.builtFor === signature) {
|
|
630
|
-
|
|
634
|
+
const version = stat ? `?v=${Math.floor(stat.mtimeMs)}` : '';
|
|
635
|
+
return `${cached.url}${version}`;
|
|
631
636
|
}
|
|
632
637
|
|
|
633
638
|
// `synapse build` já empacotou esta fatia e o arquivo não mudou desde então:
|
|
@@ -639,7 +644,8 @@ export class SynapseServer {
|
|
|
639
644
|
const prebuilt = freshBundleFrom(this.clientManifest, slice.key, Number(signature));
|
|
640
645
|
if (prebuilt) {
|
|
641
646
|
this.clientBundles.set(slice.key, { url: prebuilt, builtFor: signature });
|
|
642
|
-
|
|
647
|
+
const version = stat ? `?v=${Math.floor(stat.mtimeMs)}` : '';
|
|
648
|
+
return `${prebuilt}${version}`;
|
|
643
649
|
}
|
|
644
650
|
|
|
645
651
|
const built = await buildClientBundle(
|
|
@@ -660,7 +666,8 @@ export class SynapseServer {
|
|
|
660
666
|
|
|
661
667
|
this.clientBundles.set(slice.key, { url: built.value.url, builtFor: signature });
|
|
662
668
|
|
|
663
|
-
|
|
669
|
+
const version = stat ? `?v=${Math.floor(stat.mtimeMs)}` : '';
|
|
670
|
+
return `${built.value.url}${version}`;
|
|
664
671
|
}
|
|
665
672
|
|
|
666
673
|
/**
|
|
@@ -696,7 +703,8 @@ export class SynapseServer {
|
|
|
696
703
|
propsJson: string,
|
|
697
704
|
clientUrl: string | null,
|
|
698
705
|
meta?: SliceMetadata,
|
|
699
|
-
locale = 'pt-BR'
|
|
706
|
+
locale = 'pt-BR',
|
|
707
|
+
hasLayout = false
|
|
700
708
|
): string {
|
|
701
709
|
const safeTitle = escapeHtml(title);
|
|
702
710
|
const safeSliceName = escapeHtml(sliceName);
|
|
@@ -729,6 +737,44 @@ export class SynapseServer {
|
|
|
729
737
|
}
|
|
730
738
|
const metaSection = metaTags.length > 0 ? `\n${metaTags.join('\n')}` : '';
|
|
731
739
|
|
|
740
|
+
if (hasLayout) {
|
|
741
|
+
return `<!DOCTYPE html>
|
|
742
|
+
<html lang="${escapeHtml(locale)}" class="min-h-full">
|
|
743
|
+
<head>
|
|
744
|
+
<meta charset="UTF-8">
|
|
745
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
746
|
+
<title>${pageTitle}</title>${metaSection}
|
|
747
|
+
${this.renderStylesheets()}
|
|
748
|
+
<script type="importmap">
|
|
749
|
+
{
|
|
750
|
+
"imports": {
|
|
751
|
+
"react": "/_synapse/client/_vendor.js",
|
|
752
|
+
"react/jsx-runtime": "/_synapse/client/_vendor.js",
|
|
753
|
+
"react/jsx-dev-runtime": "/_synapse/client/_vendor.js",
|
|
754
|
+
"react-dom": "/_synapse/client/_vendor.js",
|
|
755
|
+
"react-dom/client": "/_synapse/client/_vendor.js",
|
|
756
|
+
"synapsejs/client": "/_synapse/client/_vendor.js",
|
|
757
|
+
"@ismaelsoilet/synapsejs/client": "/_synapse/client/_vendor.js"
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
</script>
|
|
761
|
+
<script type="module" src="/_synapse/client/_vendor.js"></script>
|
|
762
|
+
<style>
|
|
763
|
+
body { font-family: 'Inter', sans-serif; }
|
|
764
|
+
code, pre { font-family: 'JetBrains Mono', monospace; }
|
|
765
|
+
</style>
|
|
766
|
+
</head>
|
|
767
|
+
<body class="min-h-full flex flex-col">
|
|
768
|
+
<div id="synapse-root" class="min-h-full flex-1 flex flex-col">${contentHtml}</div>
|
|
769
|
+
|
|
770
|
+
<script data-synapse-props>globalThis.__SYNAPSE_PROPS__ = ${propsJson};</script>
|
|
771
|
+
${clientUrl ? `<script data-synapse-client type="module" src="${clientUrl}"></script>` : '<!-- sem componente hidratavel -->'}
|
|
772
|
+
|
|
773
|
+
<script type="module" src="/_synapse/turbo-router.js"></script>
|
|
774
|
+
</body>
|
|
775
|
+
</html>`;
|
|
776
|
+
}
|
|
777
|
+
|
|
732
778
|
return `<!DOCTYPE html>
|
|
733
779
|
<html lang="${escapeHtml(locale)}" class="h-full bg-slate-950 text-slate-100">
|
|
734
780
|
<head>
|
|
@@ -740,9 +786,12 @@ ${this.renderStylesheets()}
|
|
|
740
786
|
{
|
|
741
787
|
"imports": {
|
|
742
788
|
"react": "/_synapse/client/_vendor.js",
|
|
789
|
+
"react/jsx-runtime": "/_synapse/client/_vendor.js",
|
|
790
|
+
"react/jsx-dev-runtime": "/_synapse/client/_vendor.js",
|
|
743
791
|
"react-dom": "/_synapse/client/_vendor.js",
|
|
744
792
|
"react-dom/client": "/_synapse/client/_vendor.js",
|
|
745
|
-
"synapsejs/client": "/_synapse/client/_vendor.js"
|
|
793
|
+
"synapsejs/client": "/_synapse/client/_vendor.js",
|
|
794
|
+
"@ismaelsoilet/synapsejs/client": "/_synapse/client/_vendor.js"
|
|
746
795
|
}
|
|
747
796
|
}
|
|
748
797
|
</script>
|
|
@@ -993,7 +1042,7 @@ ${this.renderStylesheets()}
|
|
|
993
1042
|
{
|
|
994
1043
|
status,
|
|
995
1044
|
framework: 'SynapseJS',
|
|
996
|
-
version: '1.
|
|
1045
|
+
version: '1.2.0',
|
|
997
1046
|
uptime: process.uptime(),
|
|
998
1047
|
database: dbHealthy ? 'connected' : 'disconnected',
|
|
999
1048
|
...(dbError ? { databaseError: dbError } : {}),
|
|
@@ -1501,6 +1550,7 @@ ${this.renderStylesheets()}
|
|
|
1501
1550
|
|
|
1502
1551
|
this.logRequest(req, pathname, 200, startedAt, session, 'ssr');
|
|
1503
1552
|
|
|
1553
|
+
const hasCustomLayout = Boolean(this.layoutComponent || this.domainLayouts.get(slice.domain));
|
|
1504
1554
|
const html = this.renderHtmlShell(
|
|
1505
1555
|
meta?.title || slice.name,
|
|
1506
1556
|
contentHtml,
|
|
@@ -1508,7 +1558,8 @@ ${this.renderStylesheets()}
|
|
|
1508
1558
|
serializeClientProps(dataProps),
|
|
1509
1559
|
clientUrl,
|
|
1510
1560
|
meta,
|
|
1511
|
-
matchedLocale || 'pt-BR'
|
|
1561
|
+
matchedLocale || 'pt-BR',
|
|
1562
|
+
hasCustomLayout
|
|
1512
1563
|
);
|
|
1513
1564
|
return new Response(html, {
|
|
1514
1565
|
headers: { 'Content-Type': 'text/html; charset=utf-8' }
|