proteum 2.1.3-1 → 2.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +22 -14
- package/README.md +109 -17
- package/agents/project/AGENTS.md +188 -25
- package/agents/project/CODING_STYLE.md +1 -0
- package/agents/project/client/AGENTS.md +13 -8
- package/agents/project/client/pages/AGENTS.md +17 -9
- package/agents/project/diagnostics.md +52 -0
- package/agents/project/optimizations.md +48 -0
- package/agents/project/server/routes/AGENTS.md +9 -6
- package/agents/project/server/services/AGENTS.md +10 -6
- package/agents/project/tests/AGENTS.md +11 -5
- package/cli/app/config.ts +13 -14
- package/cli/app/index.ts +58 -0
- package/cli/commands/connect.ts +45 -0
- package/cli/commands/dev.ts +26 -11
- package/cli/commands/diagnose.ts +286 -0
- package/cli/commands/doctor.ts +18 -5
- package/cli/commands/explain.ts +25 -0
- package/cli/commands/perf.ts +243 -0
- package/cli/commands/trace.ts +9 -1
- package/cli/commands/verify.ts +281 -0
- package/cli/compiler/artifacts/connectedProjects.ts +453 -0
- package/cli/compiler/artifacts/controllers.ts +198 -49
- package/cli/compiler/artifacts/discovery.ts +0 -34
- package/cli/compiler/artifacts/manifest.ts +90 -6
- package/cli/compiler/artifacts/routing.ts +2 -2
- package/cli/compiler/artifacts/services.ts +277 -130
- package/cli/compiler/client/index.ts +3 -0
- package/cli/compiler/common/files/style.ts +52 -0
- package/cli/compiler/common/generatedRouteModules.ts +34 -5
- package/cli/compiler/common/scripts.ts +11 -5
- package/cli/compiler/index.ts +2 -1
- package/cli/compiler/server/index.ts +3 -0
- package/cli/presentation/commands.ts +110 -7
- package/cli/presentation/devSession.ts +32 -7
- package/cli/runtime/commands.ts +165 -6
- package/cli/scaffold/index.ts +14 -25
- package/cli/scaffold/templates.ts +41 -27
- package/cli/utils/agents.ts +4 -2
- package/cli/utils/keyboard.ts +8 -0
- package/client/dev/profiler/ApexChart.tsx +66 -0
- package/client/dev/profiler/index.tsx +2508 -302
- package/client/dev/profiler/runtime.noop.ts +12 -0
- package/client/dev/profiler/runtime.ts +195 -4
- package/client/services/router/request/api.ts +6 -1
- package/common/applicationConfig.ts +173 -0
- package/common/applicationConfigLoader.ts +102 -0
- package/common/connectedProjects.ts +113 -0
- package/common/dev/connect.ts +267 -0
- package/common/dev/console.ts +31 -0
- package/common/dev/contractsDoctor.ts +128 -0
- package/common/dev/diagnostics.ts +59 -15
- package/common/dev/inspection.ts +491 -0
- package/common/dev/performance.ts +809 -0
- package/common/dev/profiler.ts +3 -0
- package/common/dev/proteumManifest.ts +31 -6
- package/common/dev/requestTrace.ts +52 -1
- package/common/env/proteumEnv.ts +176 -50
- package/common/router/index.ts +1 -0
- package/common/router/request/api.ts +2 -0
- package/config.ts +5 -0
- package/docs/dev-commands.md +5 -1
- package/docs/dev-sessions.md +90 -0
- package/docs/diagnostics.md +74 -11
- package/docs/request-tracing.md +50 -3
- package/package.json +1 -1
- package/server/app/container/config.ts +16 -87
- package/server/app/container/console/index.ts +42 -8
- package/server/app/container/index.ts +3 -1
- package/server/app/container/trace/index.ts +105 -0
- package/server/app/devDiagnostics.ts +138 -0
- package/server/app/index.ts +18 -8
- package/server/app/service/container.ts +0 -12
- package/server/app/service/index.ts +0 -2
- package/server/services/prisma/index.ts +121 -4
- package/server/services/router/http/index.ts +266 -0
- package/server/services/router/index.ts +50 -47
- package/server/services/router/request/api.ts +160 -19
- package/server/services/router/request/index.ts +8 -0
- package/server/services/router/response/index.ts +23 -1
- package/server/services/router/response/page/document.tsx +5 -0
- package/server/services/router/response/page/index.tsx +10 -0
- package/agents/framework/AGENTS.md +0 -177
- package/server/services/auth/router/service.json +0 -6
- package/server/services/auth/service.json +0 -6
- package/server/services/cron/service.json +0 -6
- package/server/services/disks/drivers/local/service.json +0 -6
- package/server/services/disks/drivers/s3/service.json +0 -6
- package/server/services/disks/service.json +0 -6
- package/server/services/fetch/service.json +0 -7
- package/server/services/prisma/service.json +0 -6
- package/server/services/router/service.json +0 -6
- package/server/services/schema/router/service.json +0 -6
- package/server/services/schema/service.json +0 -6
- package/server/services/security/encrypt/aes/service.json +0 -6
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import childProcess from 'child_process';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import got from 'got';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
import app from '../../app';
|
|
7
|
+
import cli from '../..';
|
|
8
|
+
import {
|
|
9
|
+
connectedProjectContractVersion,
|
|
10
|
+
connectedProjectSourceKinds,
|
|
11
|
+
getConnectedProjectSlug,
|
|
12
|
+
normalizeConnectedProjectsConfig,
|
|
13
|
+
type TConnectedProjectContract,
|
|
14
|
+
type TConnectedProjectContractController,
|
|
15
|
+
type TConnectedProjectConfig,
|
|
16
|
+
type TConnectedProjectSourceKind,
|
|
17
|
+
type TConnectedProjectTypingMode,
|
|
18
|
+
type TConnectedProjectsConfig,
|
|
19
|
+
} from '../../../common/connectedProjects';
|
|
20
|
+
import { resolveIdentityConfigFilepath, resolveSetupConfigFilepath } from '../../../common/applicationConfigLoader';
|
|
21
|
+
import type { TControllerFileMeta } from '../common/controllers';
|
|
22
|
+
import { printControllerTree } from '../common/controllers';
|
|
23
|
+
import writeIfChanged from '../writeIfChanged';
|
|
24
|
+
import { normalizeAbsolutePath, normalizePath } from './shared';
|
|
25
|
+
|
|
26
|
+
export type TResolvedConnectedProjectContract = {
|
|
27
|
+
namespace: string;
|
|
28
|
+
cachedContractFilepath: string;
|
|
29
|
+
contract: TConnectedProjectContract;
|
|
30
|
+
sourceKind: TConnectedProjectSourceKind;
|
|
31
|
+
sourceValue: string;
|
|
32
|
+
typingMode: TConnectedProjectTypingMode;
|
|
33
|
+
typeImportModuleSpecifier?: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const connectedContractJsonFilename = 'proteum.connected.json';
|
|
37
|
+
const connectedContractDtsFilename = 'proteum.connected.d.ts';
|
|
38
|
+
const connectedContractsCacheDir = path.join(app.paths.proteum, 'connected');
|
|
39
|
+
const connectedTypesPackageScope = '@proteum-connected';
|
|
40
|
+
const connectedRefreshStackEnvKey = 'PROTEUM_CONNECTED_REFRESH_STACK';
|
|
41
|
+
|
|
42
|
+
type TParsedConnectedProjectSource =
|
|
43
|
+
| {
|
|
44
|
+
kind: 'file';
|
|
45
|
+
producerAppRoot: string;
|
|
46
|
+
sourceValue: string;
|
|
47
|
+
}
|
|
48
|
+
| {
|
|
49
|
+
kind: 'github';
|
|
50
|
+
contractPath: string;
|
|
51
|
+
ref: string;
|
|
52
|
+
repo: string;
|
|
53
|
+
sourceValue: string;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const normalizeImportPath = (filepath: string) =>
|
|
57
|
+
normalizePath(filepath).replace(/\.ts$/, '');
|
|
58
|
+
|
|
59
|
+
const normalizeImportSpecifier = (filepath: string) => {
|
|
60
|
+
const importPath = normalizeImportPath(filepath);
|
|
61
|
+
|
|
62
|
+
return importPath.startsWith('.') ? importPath : `./${importPath.replace(/^\.\//, '')}`;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const buildContractControllers = (controllers: TControllerFileMeta[]): TConnectedProjectContractController[] =>
|
|
66
|
+
controllers
|
|
67
|
+
.filter((controller) => controller.importPath.startsWith('@/server/controllers/'))
|
|
68
|
+
.flatMap((controller) =>
|
|
69
|
+
controller.methods.map((method) => ({
|
|
70
|
+
className: controller.className,
|
|
71
|
+
methodName: method.name,
|
|
72
|
+
routeBasePath: controller.routeBasePath,
|
|
73
|
+
routePath: method.routePath,
|
|
74
|
+
httpPath: '/api/' + method.routePath,
|
|
75
|
+
clientAccessor: method.routePath.split('/').join('.'),
|
|
76
|
+
hasInput: method.inputCallsCount > 0,
|
|
77
|
+
inputCallsCount: method.inputCallsCount,
|
|
78
|
+
importPath: normalizeImportPath(path.relative(app.paths.root, controller.filepath)),
|
|
79
|
+
relativeFilepath: normalizePath(path.relative(app.paths.root, controller.filepath)),
|
|
80
|
+
sourceLocation: method.sourceLocation,
|
|
81
|
+
})),
|
|
82
|
+
)
|
|
83
|
+
.sort((left, right) => left.httpPath.localeCompare(right.httpPath));
|
|
84
|
+
|
|
85
|
+
const createControllerTree = (controllers: TConnectedProjectContractController[]) => {
|
|
86
|
+
const root: Record<string, any> = {};
|
|
87
|
+
|
|
88
|
+
for (const controller of controllers) {
|
|
89
|
+
const segments = controller.clientAccessor.split('.');
|
|
90
|
+
let cursor = root;
|
|
91
|
+
|
|
92
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
93
|
+
const segment = segments[index];
|
|
94
|
+
const isLeaf = index === segments.length - 1;
|
|
95
|
+
|
|
96
|
+
if (isLeaf) {
|
|
97
|
+
cursor[segment] = JSON.stringify(controller);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
cursor[segment] = cursor[segment] || {};
|
|
102
|
+
cursor = cursor[segment];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return root;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const buildConnectedContractDts = (contract: TConnectedProjectContract) => {
|
|
110
|
+
const typeImports = contract.controllers
|
|
111
|
+
.map((controller, index) =>
|
|
112
|
+
`import type Controller${index} from ${JSON.stringify(
|
|
113
|
+
normalizeImportSpecifier(path.relative(app.paths.proteum, path.join(app.paths.root, controller.relativeFilepath))),
|
|
114
|
+
)};`,
|
|
115
|
+
)
|
|
116
|
+
.join('\n');
|
|
117
|
+
const controllerIndexByAccessor = new Map(contract.controllers.map((controller, index) => [controller.clientAccessor, index]));
|
|
118
|
+
|
|
119
|
+
const typeLeaf = (leaf: string) => {
|
|
120
|
+
const controller = JSON.parse(leaf) as TConnectedProjectContractController;
|
|
121
|
+
const index = controllerIndexByAccessor.get(controller.clientAccessor);
|
|
122
|
+
if (index === undefined) throw new Error(`Missing connected controller type import for ${controller.clientAccessor}.`);
|
|
123
|
+
|
|
124
|
+
const resultType = `TControllerResult<Controller${index}, ${JSON.stringify(controller.methodName)}>`;
|
|
125
|
+
return controller.hasInput
|
|
126
|
+
? `(data: any) => TFetcher<${resultType}>`
|
|
127
|
+
: `() => TFetcher<${resultType}>`;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
return `/*----------------------------------
|
|
131
|
+
- GENERATED FILE
|
|
132
|
+
----------------------------------*/
|
|
133
|
+
|
|
134
|
+
// This file is generated by Proteum from app controller files.
|
|
135
|
+
// Do not edit it manually.
|
|
136
|
+
|
|
137
|
+
import type { TFetcher } from 'proteum/common/router/request/api';
|
|
138
|
+
${typeImports ? '\n' + typeImports : ''}
|
|
139
|
+
|
|
140
|
+
type TControllerResult<TController, TMethod extends keyof TController> =
|
|
141
|
+
TController[TMethod] extends (...args: any[]) => infer TResult ? Awaited<TResult> : never;
|
|
142
|
+
|
|
143
|
+
export type TConnectedControllers = ${printControllerTree(createControllerTree(contract.controllers), typeLeaf)};
|
|
144
|
+
`;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const validateContract = ({
|
|
148
|
+
contract,
|
|
149
|
+
contractFilepath,
|
|
150
|
+
}: {
|
|
151
|
+
contract: unknown;
|
|
152
|
+
contractFilepath: string;
|
|
153
|
+
}): TConnectedProjectContract => {
|
|
154
|
+
if (!contract || typeof contract !== 'object' || Array.isArray(contract)) {
|
|
155
|
+
throw new Error(`Connected project contract at ${contractFilepath} is invalid. Expected an object.`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const candidate = contract as Partial<TConnectedProjectContract>;
|
|
159
|
+
if (candidate.version !== connectedProjectContractVersion) {
|
|
160
|
+
throw new Error(
|
|
161
|
+
`Connected project contract at ${contractFilepath} uses version ${String(candidate.version)}. Expected ${connectedProjectContractVersion}.`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
if (candidate.packageName !== undefined && (typeof candidate.packageName !== 'string' || candidate.packageName.trim() === '')) {
|
|
165
|
+
throw new Error(`Connected project contract at ${contractFilepath} has invalid package metadata.`);
|
|
166
|
+
}
|
|
167
|
+
if (!candidate.identity || typeof candidate.identity !== 'object') {
|
|
168
|
+
throw new Error(`Connected project contract at ${contractFilepath} is missing identity metadata.`);
|
|
169
|
+
}
|
|
170
|
+
if (!Array.isArray(candidate.controllers)) {
|
|
171
|
+
throw new Error(`Connected project contract at ${contractFilepath} is missing controllers.`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return candidate as TConnectedProjectContract;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const getConnectedRefreshStack = () =>
|
|
178
|
+
(process.env[connectedRefreshStackEnvKey] || '')
|
|
179
|
+
.split(path.delimiter)
|
|
180
|
+
.map((entry) => entry.trim())
|
|
181
|
+
.filter(Boolean)
|
|
182
|
+
.map((entry) => normalizeAbsolutePath(entry));
|
|
183
|
+
|
|
184
|
+
const requireConnectedSourceValue = (namespace: string, config: TConnectedProjectConfig) => {
|
|
185
|
+
const sourceValue = config.source?.trim();
|
|
186
|
+
|
|
187
|
+
if (sourceValue) return sourceValue;
|
|
188
|
+
|
|
189
|
+
throw new Error(
|
|
190
|
+
`Connected project "${namespace}" requires connect.${namespace}.source in ${path.join(app.paths.root, 'proteum.config.ts')}. Set it explicitly in Application.setup(...), for example source: process.env.MY_CONNECTED_SOURCE.`,
|
|
191
|
+
);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const isAbsoluteOrDrivePath = (value: string) => path.isAbsolute(value) || /^[A-Za-z]:[\\/]/.test(value);
|
|
195
|
+
|
|
196
|
+
const parseConnectedProjectSource = (namespace: string, config: TConnectedProjectConfig): TParsedConnectedProjectSource => {
|
|
197
|
+
const sourceValue = requireConnectedSourceValue(namespace, config);
|
|
198
|
+
const separatorIndex = sourceValue.indexOf(':');
|
|
199
|
+
|
|
200
|
+
if (separatorIndex === -1) {
|
|
201
|
+
throw new Error(
|
|
202
|
+
`Invalid connect.${namespace}.source="${sourceValue}". Expected one of ${connectedProjectSourceKinds.join(', ')}: sources.`,
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const kind = sourceValue.substring(0, separatorIndex).trim() as TConnectedProjectSourceKind;
|
|
207
|
+
const rawValue = sourceValue.substring(separatorIndex + 1).trim();
|
|
208
|
+
|
|
209
|
+
if (!connectedProjectSourceKinds.includes(kind)) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
`Invalid connect.${namespace}.source="${sourceValue}". Expected one of ${connectedProjectSourceKinds.join(', ')}: sources.`,
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (kind === 'file') {
|
|
216
|
+
if (!rawValue) {
|
|
217
|
+
throw new Error(`Invalid connect.${namespace}.source="${sourceValue}". Expected a local producer app root after "file:".`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const producerAppRoot = normalizeAbsolutePath(
|
|
221
|
+
isAbsoluteOrDrivePath(rawValue) ? rawValue : path.resolve(app.paths.root, rawValue),
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
kind,
|
|
226
|
+
producerAppRoot,
|
|
227
|
+
sourceValue: producerAppRoot,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const repoAndQueryMatch = rawValue.match(/^([^?]+\/[^?]+)(?:\?(.*))?$/);
|
|
232
|
+
if (!repoAndQueryMatch) {
|
|
233
|
+
throw new Error(
|
|
234
|
+
`Invalid connect.${namespace}.source="${sourceValue}". Expected github:<owner>/<repo>?ref=<ref>&path=proteum.connected.json.`,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const [, repo, rawQuery = ''] = repoAndQueryMatch;
|
|
239
|
+
const params = new URLSearchParams(rawQuery);
|
|
240
|
+
const ref = params.get('ref')?.trim();
|
|
241
|
+
const contractPath = params.get('path')?.trim();
|
|
242
|
+
|
|
243
|
+
if (!ref || !contractPath) {
|
|
244
|
+
throw new Error(
|
|
245
|
+
`Invalid connect.${namespace}.source="${sourceValue}". Expected github:<owner>/<repo>?ref=<ref>&path=proteum.connected.json.`,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
kind,
|
|
251
|
+
contractPath,
|
|
252
|
+
ref,
|
|
253
|
+
repo,
|
|
254
|
+
sourceValue: `github:${repo}?ref=${ref}&path=${contractPath}`,
|
|
255
|
+
};
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const assertProducerAppRoot = (namespace: string, producerAppRoot: string) => {
|
|
259
|
+
const missingEntries = [
|
|
260
|
+
'package.json',
|
|
261
|
+
resolveIdentityConfigFilepath(producerAppRoot),
|
|
262
|
+
resolveSetupConfigFilepath(producerAppRoot),
|
|
263
|
+
path.join(producerAppRoot, 'server', 'index.ts'),
|
|
264
|
+
].filter((filepath) => !fs.existsSync(filepath));
|
|
265
|
+
|
|
266
|
+
if (missingEntries.length === 0) return;
|
|
267
|
+
|
|
268
|
+
throw new Error(
|
|
269
|
+
`Connected project "${namespace}" source ${producerAppRoot} is not a Proteum app root. Missing: ${missingEntries.join(', ')}.`,
|
|
270
|
+
);
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const refreshProducerApp = (namespace: string, producerAppRoot: string) => {
|
|
274
|
+
const connectedRefreshStack = getConnectedRefreshStack();
|
|
275
|
+
if (connectedRefreshStack.includes(producerAppRoot)) {
|
|
276
|
+
throw new Error(
|
|
277
|
+
`Connected project "${namespace}" creates a refresh cycle through ${producerAppRoot}. Break the circular connected-project dependency first.`,
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const cliBin = path.join(cli.paths.core.root, 'cli', 'bin.js');
|
|
282
|
+
const currentAppRoot = normalizeAbsolutePath(app.paths.root);
|
|
283
|
+
const nextRefreshStack = [...connectedRefreshStack, currentAppRoot].join(path.delimiter);
|
|
284
|
+
const result = childProcess.spawnSync(process.execPath, [cliBin, 'refresh'], {
|
|
285
|
+
cwd: producerAppRoot,
|
|
286
|
+
env: {
|
|
287
|
+
...process.env,
|
|
288
|
+
[connectedRefreshStackEnvKey]: nextRefreshStack,
|
|
289
|
+
},
|
|
290
|
+
encoding: 'utf8',
|
|
291
|
+
stdio: 'pipe',
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
if (result.status === 0) return;
|
|
295
|
+
|
|
296
|
+
const stdout = result.stdout?.trim();
|
|
297
|
+
const stderr = result.stderr?.trim();
|
|
298
|
+
const details = [stdout, stderr].filter(Boolean).join('\n').trim();
|
|
299
|
+
|
|
300
|
+
throw new Error(
|
|
301
|
+
`Connected project "${namespace}" failed to refresh producer ${producerAppRoot}.${details ? `\n${details}` : ''}`,
|
|
302
|
+
);
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const ensureSymlink = (linkPath: string, targetPath: string) => {
|
|
306
|
+
fs.ensureDirSync(path.dirname(linkPath));
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
const linkStats = fs.lstatSync(linkPath);
|
|
310
|
+
|
|
311
|
+
if (linkStats.isSymbolicLink()) {
|
|
312
|
+
const currentTarget = path.resolve(path.dirname(linkPath), fs.readlinkSync(linkPath));
|
|
313
|
+
if (currentTarget === path.resolve(targetPath)) return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
fs.removeSync(linkPath);
|
|
317
|
+
} catch (error) {
|
|
318
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') throw error;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
fs.symlinkSync(targetPath, linkPath, process.platform === 'win32' ? 'junction' : 'dir');
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const getConnectedTypesPackageName = (namespace: string) =>
|
|
325
|
+
`${connectedTypesPackageScope}/${getConnectedProjectSlug(namespace).toLowerCase().replace(/_/g, '-')}`;
|
|
326
|
+
|
|
327
|
+
const ensureConnectedTypesPackage = (namespace: string, producerAppRoot: string) => {
|
|
328
|
+
const packageName = getConnectedTypesPackageName(namespace);
|
|
329
|
+
const packageRoot = path.join(app.paths.root, 'node_modules', connectedTypesPackageScope, packageName.split('/')[1]);
|
|
330
|
+
|
|
331
|
+
ensureSymlink(packageRoot, producerAppRoot);
|
|
332
|
+
return packageName;
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const writeCachedConnectedContract = (namespace: string, contract: TConnectedProjectContract) => {
|
|
336
|
+
fs.ensureDirSync(connectedContractsCacheDir);
|
|
337
|
+
const cachedContractFilepath = path.join(connectedContractsCacheDir, `${namespace}.json`);
|
|
338
|
+
writeIfChanged(cachedContractFilepath, JSON.stringify(contract, null, 2) + '\n');
|
|
339
|
+
return normalizeAbsolutePath(cachedContractFilepath);
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const fetchGithubConnectedContract = async (namespace: string, source: Extract<TParsedConnectedProjectSource, { kind: 'github' }>) => {
|
|
343
|
+
const encodedPath = source.contractPath
|
|
344
|
+
.split('/')
|
|
345
|
+
.map((segment) => encodeURIComponent(segment))
|
|
346
|
+
.join('/');
|
|
347
|
+
const apiUrl = `https://api.github.com/repos/${source.repo}/contents/${encodedPath}`;
|
|
348
|
+
const githubToken = process.env.GITHUB_TOKEN?.trim();
|
|
349
|
+
|
|
350
|
+
if (!githubToken) {
|
|
351
|
+
throw new Error(
|
|
352
|
+
`Connected project "${namespace}" uses a github: source but GITHUB_TOKEN is not set. Private GitHub contract fetches require GITHUB_TOKEN.`,
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const rawContract = await got(apiUrl, {
|
|
357
|
+
headers: {
|
|
358
|
+
Accept: 'application/vnd.github.raw',
|
|
359
|
+
Authorization: `Bearer ${githubToken}`,
|
|
360
|
+
'User-Agent': 'proteum-connected-contract-fetch',
|
|
361
|
+
},
|
|
362
|
+
responseType: 'text',
|
|
363
|
+
retry: { limit: 0 },
|
|
364
|
+
searchParams: {
|
|
365
|
+
ref: source.ref,
|
|
366
|
+
},
|
|
367
|
+
}).text();
|
|
368
|
+
|
|
369
|
+
return JSON.parse(rawContract);
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
export const writeConnectedProjectContract = (controllers: TControllerFileMeta[]) => {
|
|
373
|
+
const contract = {
|
|
374
|
+
version: connectedProjectContractVersion,
|
|
375
|
+
packageName: String(app.packageJson.name || '').trim() || undefined,
|
|
376
|
+
identity: {
|
|
377
|
+
name: app.identity.name,
|
|
378
|
+
identifier: app.identity.identifier,
|
|
379
|
+
},
|
|
380
|
+
controllers: buildContractControllers(controllers),
|
|
381
|
+
} satisfies TConnectedProjectContract;
|
|
382
|
+
|
|
383
|
+
writeIfChanged(path.join(app.paths.root, connectedContractJsonFilename), JSON.stringify(contract, null, 2) + '\n');
|
|
384
|
+
writeIfChanged(path.join(app.paths.proteum, connectedContractDtsFilename), buildConnectedContractDts(contract));
|
|
385
|
+
fs.removeSync(path.join(app.paths.root, connectedContractDtsFilename));
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
export const resolveConnectedProjectContracts = async (
|
|
389
|
+
rawConnectedProjects: TConnectedProjectsConfig,
|
|
390
|
+
): Promise<TResolvedConnectedProjectContract[]> => {
|
|
391
|
+
const connectedProjects = Object.entries(normalizeConnectedProjectsConfig(rawConnectedProjects)).sort(([left], [right]) =>
|
|
392
|
+
left.localeCompare(right),
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
const contracts: TResolvedConnectedProjectContract[] = [];
|
|
396
|
+
|
|
397
|
+
for (const [namespace, config] of connectedProjects) {
|
|
398
|
+
const source = parseConnectedProjectSource(namespace, config);
|
|
399
|
+
|
|
400
|
+
if (source.kind === 'file') {
|
|
401
|
+
assertProducerAppRoot(namespace, source.producerAppRoot);
|
|
402
|
+
refreshProducerApp(namespace, source.producerAppRoot);
|
|
403
|
+
|
|
404
|
+
const producerContractFilepath = path.join(source.producerAppRoot, connectedContractJsonFilename);
|
|
405
|
+
const producerTypesFilepath = path.join(source.producerAppRoot, '.proteum', connectedContractDtsFilename);
|
|
406
|
+
|
|
407
|
+
if (!fs.existsSync(producerContractFilepath)) {
|
|
408
|
+
throw new Error(
|
|
409
|
+
`Connected project "${namespace}" expected ${producerContractFilepath}, but it is missing after producer refresh.`,
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (!fs.existsSync(producerTypesFilepath)) {
|
|
414
|
+
throw new Error(
|
|
415
|
+
`Connected project "${namespace}" expected ${producerTypesFilepath}, but it is missing after producer refresh.`,
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const contract = validateContract({
|
|
420
|
+
contract: fs.readJsonSync(producerContractFilepath),
|
|
421
|
+
contractFilepath: normalizeAbsolutePath(producerContractFilepath),
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
contracts.push({
|
|
425
|
+
namespace,
|
|
426
|
+
cachedContractFilepath: writeCachedConnectedContract(namespace, contract),
|
|
427
|
+
contract,
|
|
428
|
+
sourceKind: source.kind,
|
|
429
|
+
sourceValue: source.sourceValue,
|
|
430
|
+
typingMode: 'local-typed',
|
|
431
|
+
typeImportModuleSpecifier: `${ensureConnectedTypesPackage(namespace, source.producerAppRoot)}/.proteum/proteum.connected`,
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
continue;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const contract = validateContract({
|
|
438
|
+
contract: await fetchGithubConnectedContract(namespace, source),
|
|
439
|
+
contractFilepath: source.sourceValue,
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
contracts.push({
|
|
443
|
+
namespace,
|
|
444
|
+
cachedContractFilepath: writeCachedConnectedContract(namespace, contract),
|
|
445
|
+
contract,
|
|
446
|
+
sourceKind: source.kind,
|
|
447
|
+
sourceValue: source.sourceValue,
|
|
448
|
+
typingMode: 'runtime-only',
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return contracts;
|
|
453
|
+
};
|