proteum 2.0.0 → 2.1.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/AGENTS.md +13 -1
- package/README.md +375 -0
- package/agents/framework/AGENTS.md +917 -0
- package/agents/project/AGENTS.md +138 -0
- package/agents/{codex → project}/CODING_STYLE.md +3 -2
- package/agents/project/client/AGENTS.md +108 -0
- package/agents/{codex → project}/client/pages/AGENTS.md +8 -8
- package/agents/{codex → project}/server/routes/AGENTS.md +2 -1
- package/agents/project/server/services/AGENTS.md +170 -0
- package/agents/{codex → project}/tests/AGENTS.md +1 -0
- package/cli/app/config.ts +3 -2
- package/cli/app/index.ts +6 -66
- package/cli/bin.js +7 -2
- package/cli/commands/build.ts +94 -27
- package/cli/commands/check.ts +15 -1
- package/cli/commands/dev.ts +288 -132
- package/cli/commands/doctor.ts +108 -0
- package/cli/commands/explain.ts +226 -0
- package/cli/commands/init.ts +76 -70
- package/cli/commands/lint.ts +18 -1
- package/cli/commands/refresh.ts +16 -6
- package/cli/commands/typecheck.ts +14 -1
- package/cli/compiler/artifacts/controllers.ts +150 -0
- package/cli/compiler/artifacts/discovery.ts +132 -0
- package/cli/compiler/artifacts/manifest.ts +267 -0
- package/cli/compiler/artifacts/routing.ts +315 -0
- package/cli/compiler/artifacts/services.ts +480 -0
- package/cli/compiler/artifacts/shared.ts +12 -0
- package/cli/compiler/client/identite.ts +2 -1
- package/cli/compiler/client/index.ts +13 -3
- package/cli/compiler/common/controllers.ts +23 -28
- package/cli/compiler/common/files/style.ts +3 -4
- package/cli/compiler/common/generatedRouteModules.ts +333 -19
- package/cli/compiler/common/proteumManifest.ts +133 -0
- package/cli/compiler/index.ts +33 -896
- package/cli/compiler/server/index.ts +21 -4
- package/cli/context.ts +71 -0
- package/cli/index.ts +39 -181
- package/cli/presentation/commands.ts +208 -0
- package/cli/presentation/compileReporter.ts +65 -0
- package/cli/presentation/devSession.ts +70 -0
- package/cli/presentation/help.ts +193 -0
- package/cli/presentation/ink.ts +69 -0
- package/cli/presentation/layout.ts +83 -0
- package/cli/runtime/argv.ts +49 -0
- package/cli/runtime/command.ts +25 -0
- package/cli/runtime/commands.ts +221 -0
- package/cli/runtime/importEsm.ts +7 -0
- package/cli/runtime/verbose.ts +15 -0
- package/cli/utils/agents.ts +5 -4
- package/cli/utils/keyboard.ts +12 -6
- package/client/app/index.ts +0 -6
- package/client/services/router/index.tsx +1 -1
- package/client/services/router/response/index.tsx +2 -2
- package/common/dev/serverHotReload.ts +12 -0
- package/common/router/index.ts +3 -2
- package/common/router/layouts.ts +1 -1
- package/common/router/pageSetup.ts +1 -0
- package/package.json +10 -8
- package/prettier/router-registration-plugin.cjs +52 -0
- package/prettier.config.cjs +1 -0
- package/scripts/cleanup-generated-controllers.ts +2 -2
- package/scripts/fix-reference-app-typing.ts +2 -2
- package/scripts/format-router-registrations.ts +119 -0
- package/scripts/migrate-explicit-controllers-and-request.ts +423 -0
- package/scripts/refactor-server-controllers.ts +19 -18
- package/scripts/refactor-server-runtime-aliases.ts +1 -1
- package/server/app/commands.ts +309 -25
- package/server/app/container/config.ts +1 -1
- package/server/app/container/index.ts +2 -2
- package/server/app/controller/index.ts +13 -4
- package/server/app/index.ts +53 -37
- package/server/app/service/container.ts +26 -28
- package/server/app/service/index.ts +10 -20
- package/server/app.tsconfig.json +9 -2
- package/server/index.ts +32 -1
- package/server/services/auth/index.ts +234 -15
- package/server/services/auth/router/index.ts +39 -7
- package/server/services/auth/router/request.ts +40 -8
- package/server/services/disks/index.ts +1 -1
- package/server/services/prisma/Facet.ts +2 -2
- package/server/services/prisma/index.ts +22 -5
- package/server/services/prisma/mariadb.ts +47 -0
- package/server/services/router/http/index.ts +9 -1
- package/server/services/router/index.ts +10 -4
- package/server/services/router/response/index.ts +26 -6
- package/types/auth-check-rules.test.ts +51 -0
- package/types/controller-request-context.test.ts +55 -0
- package/types/service-config.test.ts +39 -0
- package/agents/codex/AGENTS.md +0 -95
- package/agents/codex/client/AGENTS.md +0 -102
- package/agents/codex/server/services/AGENTS.md +0 -137
- package/server/services/models.7z +0 -0
- /package/agents/{codex → project}/agents.md.zip +0 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
|
|
3
|
+
import app from '../../app';
|
|
4
|
+
import cli from '../..';
|
|
5
|
+
import { generateControllerClientTree, indexControllers, printControllerTree } from '../common/controllers';
|
|
6
|
+
import { TProteumManifestController } from '../common/proteumManifest';
|
|
7
|
+
import writeIfChanged from '../writeIfChanged';
|
|
8
|
+
import { normalizeAbsolutePath } from './shared';
|
|
9
|
+
|
|
10
|
+
const getManifestScopeFromImportPath = (importPath: string) =>
|
|
11
|
+
importPath.startsWith('@server/controllers/') ? 'framework' : 'app';
|
|
12
|
+
|
|
13
|
+
export const generateControllerArtifacts = () => {
|
|
14
|
+
const controllers = indexControllers([
|
|
15
|
+
{ importPrefix: '@server/controllers/', root: path.join(cli.paths.core.root, 'server', 'controllers') },
|
|
16
|
+
{ importPrefix: '@/server/controllers/', root: path.join(app.paths.root, 'server', 'controllers') },
|
|
17
|
+
]);
|
|
18
|
+
const manifestControllers = controllers.flatMap<TProteumManifestController>((controller) =>
|
|
19
|
+
controller.methods.map((method) => ({
|
|
20
|
+
className: controller.className,
|
|
21
|
+
importPath: controller.importPath,
|
|
22
|
+
filepath: normalizeAbsolutePath(controller.filepath),
|
|
23
|
+
sourceLocation: method.sourceLocation,
|
|
24
|
+
routeBasePath: controller.routeBasePath,
|
|
25
|
+
methodName: method.name,
|
|
26
|
+
inputCallsCount: method.inputCallsCount,
|
|
27
|
+
hasInput: method.inputCallsCount > 0,
|
|
28
|
+
routePath: method.routePath,
|
|
29
|
+
httpPath: '/api/' + method.routePath,
|
|
30
|
+
clientAccessor: method.routePath.split('/').join('.'),
|
|
31
|
+
scope: getManifestScopeFromImportPath(controller.importPath),
|
|
32
|
+
})),
|
|
33
|
+
);
|
|
34
|
+
const clientTree = generateControllerClientTree(controllers);
|
|
35
|
+
|
|
36
|
+
const getControllerLeafMeta = (leaf: string) => {
|
|
37
|
+
const meta = JSON.parse(leaf) as {
|
|
38
|
+
routePath: string;
|
|
39
|
+
importPath: string;
|
|
40
|
+
className: string;
|
|
41
|
+
methodName: string;
|
|
42
|
+
hasInput: boolean;
|
|
43
|
+
};
|
|
44
|
+
const controllerIndex = controllers.findIndex((controller) => controller.importPath === meta.importPath);
|
|
45
|
+
|
|
46
|
+
if (controllerIndex === -1) {
|
|
47
|
+
throw new Error(`Unable to find controller import ${meta.importPath} while generating controller types.`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return { ...meta, controllerIndex };
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const runtimeLeaf = (leaf: string) => {
|
|
54
|
+
const meta = getControllerLeafMeta(leaf);
|
|
55
|
+
const resultType = `TControllerResult<Controller${meta.controllerIndex}, ${JSON.stringify(meta.methodName)}>`;
|
|
56
|
+
|
|
57
|
+
return meta.hasInput
|
|
58
|
+
? `(data) => api.createFetcher<${resultType}>('POST', ${JSON.stringify(meta.routePath)}, data)`
|
|
59
|
+
: `() => api.createFetcher<${resultType}>('POST', ${JSON.stringify(meta.routePath)})`;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const typeImports = controllers
|
|
63
|
+
.map((controller, index) => `import type Controller${index} from ${JSON.stringify(controller.importPath)};`)
|
|
64
|
+
.join('\n');
|
|
65
|
+
|
|
66
|
+
const typeLeaf = (leaf: string) => {
|
|
67
|
+
const meta = getControllerLeafMeta(leaf);
|
|
68
|
+
const fetcherType = `TControllerFetcher<Controller${meta.controllerIndex}, ${JSON.stringify(meta.methodName)}>`;
|
|
69
|
+
|
|
70
|
+
return meta.hasInput ? `(data: any) => ${fetcherType}` : `() => ${fetcherType}`;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const createControllersContent = `/*----------------------------------
|
|
74
|
+
- GENERATED FILE
|
|
75
|
+
----------------------------------*/
|
|
76
|
+
|
|
77
|
+
// This file is generated by Proteum from server controller files.
|
|
78
|
+
// Do not edit it manually.
|
|
79
|
+
|
|
80
|
+
import type ApiClient from '@common/router/request/api';
|
|
81
|
+
import type { TFetcher } from '@common/router/request/api';
|
|
82
|
+
${typeImports ? '\n' + typeImports : ''}
|
|
83
|
+
|
|
84
|
+
type TControllerResult<TController, TMethod extends keyof TController> =
|
|
85
|
+
TController[TMethod] extends (...args: any[]) => infer TResult ? Awaited<TResult> : never;
|
|
86
|
+
|
|
87
|
+
type TControllerFetcher<TController, TMethod extends keyof TController> = TFetcher<TControllerResult<TController, TMethod>>;
|
|
88
|
+
|
|
89
|
+
export type TControllers = ${printControllerTree(clientTree, typeLeaf)};
|
|
90
|
+
|
|
91
|
+
export const createControllers = (
|
|
92
|
+
api: Pick<ApiClient, 'createFetcher'>
|
|
93
|
+
): TControllers => (
|
|
94
|
+
${printControllerTree(clientTree, runtimeLeaf)}
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
export default createControllers;
|
|
98
|
+
`;
|
|
99
|
+
|
|
100
|
+
writeIfChanged(path.join(app.paths.common.generated, 'controllers.ts'), createControllersContent);
|
|
101
|
+
|
|
102
|
+
writeIfChanged(
|
|
103
|
+
path.join(app.paths.client.generated, 'controllers.ts'),
|
|
104
|
+
`export { createControllers, default } from '@generated/common/controllers';
|
|
105
|
+
export type { TControllers } from '@generated/common/controllers';
|
|
106
|
+
`,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const controllerImports = controllers
|
|
110
|
+
.map((controller, index) => `import Controller${index} from ${JSON.stringify(controller.importPath)};`)
|
|
111
|
+
.join('\n');
|
|
112
|
+
|
|
113
|
+
const controllerEntries = controllers.flatMap((controller, controllerIndex) =>
|
|
114
|
+
controller.methods.map(
|
|
115
|
+
(method) => ` {
|
|
116
|
+
path: ${JSON.stringify('/api/' + method.routePath)},
|
|
117
|
+
Controller: Controller${controllerIndex},
|
|
118
|
+
method: ${JSON.stringify(method.name)},
|
|
119
|
+
},`,
|
|
120
|
+
),
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
writeIfChanged(
|
|
124
|
+
path.join(app.paths.server.generated, 'controllers.ts'),
|
|
125
|
+
`/*----------------------------------
|
|
126
|
+
- GENERATED FILE
|
|
127
|
+
----------------------------------*/
|
|
128
|
+
|
|
129
|
+
// This file is generated by Proteum from server controller files.
|
|
130
|
+
// Do not edit it manually.
|
|
131
|
+
|
|
132
|
+
import type Controller from '@server/app/controller';
|
|
133
|
+
${controllerImports ? '\n' + controllerImports : ''}
|
|
134
|
+
|
|
135
|
+
export type TGeneratedControllerDefinition = {
|
|
136
|
+
path: string,
|
|
137
|
+
Controller: new (request: any) => Controller,
|
|
138
|
+
method: string,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const controllers: TGeneratedControllerDefinition[] = [
|
|
142
|
+
${controllerEntries.join('\n')}
|
|
143
|
+
];
|
|
144
|
+
|
|
145
|
+
export default controllers;
|
|
146
|
+
`,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
return manifestControllers;
|
|
150
|
+
};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import ts from 'typescript';
|
|
4
|
+
|
|
5
|
+
import app from '../../app';
|
|
6
|
+
import { normalizePath } from './shared';
|
|
7
|
+
|
|
8
|
+
const ignoredServiceDirectories = new Set(['node_modules', 'proteum']);
|
|
9
|
+
|
|
10
|
+
export const findServiceDirectories = (dir: string): string[] => {
|
|
11
|
+
if (!fs.existsSync(dir)) return [];
|
|
12
|
+
|
|
13
|
+
const directories: string[] = [];
|
|
14
|
+
|
|
15
|
+
for (const dirent of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
16
|
+
const filePath = path.resolve(dir, dirent.name);
|
|
17
|
+
|
|
18
|
+
if (ignoredServiceDirectories.has(dirent.name)) continue;
|
|
19
|
+
|
|
20
|
+
let shouldTraverse = false;
|
|
21
|
+
|
|
22
|
+
if (dirent.isSymbolicLink()) {
|
|
23
|
+
const realPath = path.resolve(dir, fs.readlinkSync(filePath));
|
|
24
|
+
shouldTraverse = fs.lstatSync(realPath).isDirectory();
|
|
25
|
+
} else if (dirent.isDirectory()) {
|
|
26
|
+
shouldTraverse = true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (shouldTraverse) {
|
|
30
|
+
directories.push(...findServiceDirectories(filePath));
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (dirent.name === 'service.json') {
|
|
35
|
+
directories.push(path.dirname(filePath));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return directories;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const hasRegisteredRouteDefinitions = (filepath: string, content: string) => {
|
|
43
|
+
const sourceFile = ts.createSourceFile(
|
|
44
|
+
filepath,
|
|
45
|
+
content,
|
|
46
|
+
ts.ScriptTarget.Latest,
|
|
47
|
+
true,
|
|
48
|
+
filepath.endsWith('.tsx') ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return sourceFile.statements.some((statement) => {
|
|
52
|
+
if (!ts.isExpressionStatement(statement)) return false;
|
|
53
|
+
if (!ts.isCallExpression(statement.expression)) return false;
|
|
54
|
+
if (!ts.isPropertyAccessExpression(statement.expression.expression)) return false;
|
|
55
|
+
|
|
56
|
+
const callee = statement.expression.expression;
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
ts.isIdentifier(callee.expression) &&
|
|
60
|
+
callee.expression.text === 'Router' &&
|
|
61
|
+
['page', 'error', 'get', 'post', 'put', 'delete', 'patch'].includes(callee.name.text)
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const findRegisteredRouteFiles = (dir: string, options: { excludeLayoutDirectories?: boolean } = {}): string[] => {
|
|
67
|
+
if (!fs.existsSync(dir)) return [];
|
|
68
|
+
|
|
69
|
+
const files: string[] = [];
|
|
70
|
+
|
|
71
|
+
for (const dirent of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
72
|
+
const filePath = path.join(dir, dirent.name);
|
|
73
|
+
|
|
74
|
+
if (dirent.isDirectory()) {
|
|
75
|
+
if (options.excludeLayoutDirectories && dirent.name === '_layout') continue;
|
|
76
|
+
|
|
77
|
+
files.push(...findRegisteredRouteFiles(filePath, options));
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!dirent.isFile()) continue;
|
|
82
|
+
if (!/\.(ts|tsx)$/.test(dirent.name)) continue;
|
|
83
|
+
|
|
84
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
85
|
+
if (!hasRegisteredRouteDefinitions(filePath, content)) continue;
|
|
86
|
+
|
|
87
|
+
files.push(filePath);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return files;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const findClientRouteFiles = (dir: string) => findRegisteredRouteFiles(dir, { excludeLayoutDirectories: true });
|
|
94
|
+
|
|
95
|
+
export const findServerRouteFiles = (dir: string) => findRegisteredRouteFiles(dir);
|
|
96
|
+
|
|
97
|
+
export const findLayoutFiles = (dir: string): string[] => {
|
|
98
|
+
if (!fs.existsSync(dir)) return [];
|
|
99
|
+
|
|
100
|
+
const files: string[] = [];
|
|
101
|
+
|
|
102
|
+
for (const dirent of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
103
|
+
const filePath = path.join(dir, dirent.name);
|
|
104
|
+
|
|
105
|
+
if (dirent.isDirectory()) {
|
|
106
|
+
files.push(...findLayoutFiles(filePath));
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!dirent.isFile()) continue;
|
|
111
|
+
if (dirent.name !== 'index.tsx') continue;
|
|
112
|
+
if (!normalizePath(filePath).includes('/_layout/')) continue;
|
|
113
|
+
|
|
114
|
+
files.push(filePath);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return files;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export const readPreloadedRouteChunks = () => {
|
|
121
|
+
const preloadPath = path.join(app.paths.pages, 'preload.json');
|
|
122
|
+
|
|
123
|
+
if (!fs.existsSync(preloadPath)) return new Set<string>();
|
|
124
|
+
|
|
125
|
+
const content = fs.readJsonSync(preloadPath);
|
|
126
|
+
|
|
127
|
+
if (!Array.isArray(content)) {
|
|
128
|
+
throw new Error(`Invalid client/pages/preload.json format: expected an array of chunk ids.`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return new Set<string>(content.filter((value): value is string => typeof value === 'string'));
|
|
132
|
+
};
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import yaml from 'yaml';
|
|
4
|
+
|
|
5
|
+
import app from '../../app';
|
|
6
|
+
import cli from '../..';
|
|
7
|
+
import { reservedRouteSetupKeys, routeSetupOptionKeys } from '../../../common/router/pageSetup';
|
|
8
|
+
import {
|
|
9
|
+
TProteumManifest,
|
|
10
|
+
TProteumManifestController,
|
|
11
|
+
TProteumManifestDiagnostic,
|
|
12
|
+
TProteumManifestLayout,
|
|
13
|
+
TProteumManifestRoute,
|
|
14
|
+
} from '../common/proteumManifest';
|
|
15
|
+
import { writeProteumManifest } from '../common/proteumManifest';
|
|
16
|
+
import { normalizeAbsolutePath, normalizePath } from './shared';
|
|
17
|
+
|
|
18
|
+
const envRequiredTopLevelKeys = ['name', 'profile', 'router', 'console'];
|
|
19
|
+
|
|
20
|
+
const getEnvTopLevelKeys = () => {
|
|
21
|
+
const envFilepath = path.join(app.paths.root, 'env.yaml');
|
|
22
|
+
|
|
23
|
+
if (!fs.existsSync(envFilepath)) return [];
|
|
24
|
+
|
|
25
|
+
const rawEnv = yaml.parse(fs.readFileSync(envFilepath, 'utf8'));
|
|
26
|
+
|
|
27
|
+
if (!rawEnv || typeof rawEnv !== 'object' || Array.isArray(rawEnv)) return [];
|
|
28
|
+
|
|
29
|
+
return Object.keys(rawEnv).sort((a, b) => a.localeCompare(b));
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const collectManifestDiagnostics = ({
|
|
33
|
+
controllers,
|
|
34
|
+
routes,
|
|
35
|
+
}: {
|
|
36
|
+
controllers: TProteumManifestController[];
|
|
37
|
+
routes: TProteumManifest['routes'];
|
|
38
|
+
}) => {
|
|
39
|
+
const diagnostics: TProteumManifestDiagnostic[] = [];
|
|
40
|
+
|
|
41
|
+
const pushDiagnostic = (diagnostic: TProteumManifestDiagnostic) => {
|
|
42
|
+
diagnostics.push(diagnostic);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const createDuplicateDiagnostics = <
|
|
46
|
+
TEntry extends { filepath: string; sourceLocation?: { line: number; column: number } },
|
|
47
|
+
>(
|
|
48
|
+
entries: TEntry[],
|
|
49
|
+
{
|
|
50
|
+
code,
|
|
51
|
+
level,
|
|
52
|
+
message,
|
|
53
|
+
}: {
|
|
54
|
+
code: string;
|
|
55
|
+
level: TProteumManifestDiagnostic['level'];
|
|
56
|
+
message: (entry: TEntry, others: TEntry[]) => string;
|
|
57
|
+
},
|
|
58
|
+
) => {
|
|
59
|
+
if (entries.length < 2) return;
|
|
60
|
+
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
pushDiagnostic({
|
|
63
|
+
level,
|
|
64
|
+
code,
|
|
65
|
+
message: message(
|
|
66
|
+
entry,
|
|
67
|
+
entries.filter((candidate) => candidate !== entry),
|
|
68
|
+
),
|
|
69
|
+
filepath: entry.filepath,
|
|
70
|
+
sourceLocation: entry.sourceLocation,
|
|
71
|
+
relatedFilepaths: entries.filter((candidate) => candidate !== entry).map((candidate) => candidate.filepath),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const trackDuplicates = <TEntry extends { filepath: string; sourceLocation?: { line: number; column: number } }>(
|
|
77
|
+
entries: TEntry[],
|
|
78
|
+
getKey: (entry: TEntry) => string | undefined,
|
|
79
|
+
config: {
|
|
80
|
+
code: string;
|
|
81
|
+
level: TProteumManifestDiagnostic['level'];
|
|
82
|
+
message: (entry: TEntry, others: TEntry[]) => string;
|
|
83
|
+
},
|
|
84
|
+
) => {
|
|
85
|
+
const groups = new Map<string, TEntry[]>();
|
|
86
|
+
|
|
87
|
+
for (const entry of entries) {
|
|
88
|
+
const key = getKey(entry);
|
|
89
|
+
if (!key) continue;
|
|
90
|
+
|
|
91
|
+
const group = groups.get(key) || [];
|
|
92
|
+
group.push(entry);
|
|
93
|
+
groups.set(key, group);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
for (const group of groups.values()) {
|
|
97
|
+
createDuplicateDiagnostics(group, config);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
for (const route of [...routes.client, ...routes.server]) {
|
|
102
|
+
if (route.targetResolution === 'dynamic-expression') {
|
|
103
|
+
pushDiagnostic({
|
|
104
|
+
level: 'warning',
|
|
105
|
+
code: 'route.dynamic-target',
|
|
106
|
+
message:
|
|
107
|
+
route.kind === 'client-error'
|
|
108
|
+
? `Proteum could not resolve this error code statically. Prefer a numeric literal or a const-only expression.`
|
|
109
|
+
: `Proteum could not resolve this route path statically. Prefer a string literal or a const-only expression.`,
|
|
110
|
+
filepath: route.filepath,
|
|
111
|
+
sourceLocation: route.sourceLocation,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
for (const optionKey of route.invalidOptionKeys) {
|
|
116
|
+
pushDiagnostic({
|
|
117
|
+
level: 'error',
|
|
118
|
+
code: 'route.invalid-option-key',
|
|
119
|
+
message: `"${optionKey}" is not a supported Router option key.`,
|
|
120
|
+
filepath: route.filepath,
|
|
121
|
+
sourceLocation: route.sourceLocation,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
for (const optionKey of route.reservedOptionKeys) {
|
|
126
|
+
pushDiagnostic({
|
|
127
|
+
level: 'error',
|
|
128
|
+
code: 'route.reserved-option-key',
|
|
129
|
+
message: `"${optionKey}" is a reserved Router option key and cannot be set explicitly.`,
|
|
130
|
+
filepath: route.filepath,
|
|
131
|
+
sourceLocation: route.sourceLocation,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
trackDuplicates(routes.client.filter((route) => route.kind === 'client-page'), (route) => route.path, {
|
|
137
|
+
code: 'route.duplicate-client-path',
|
|
138
|
+
level: 'warning',
|
|
139
|
+
message: (route, others) =>
|
|
140
|
+
`Duplicate client page path "${(route as TProteumManifestRoute).path}" also registered in ${others
|
|
141
|
+
.map((other) => normalizePath(path.relative(app.paths.root, other.filepath)))
|
|
142
|
+
.join(', ')}.`,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
trackDuplicates(
|
|
146
|
+
routes.client.filter((route) => route.kind === 'client-error'),
|
|
147
|
+
(route) => (typeof route.code === 'number' ? String(route.code) : undefined),
|
|
148
|
+
{
|
|
149
|
+
code: 'route.duplicate-client-error',
|
|
150
|
+
level: 'warning',
|
|
151
|
+
message: (route, others) =>
|
|
152
|
+
`Duplicate client error code "${(route as TProteumManifestRoute).code}" also registered in ${others
|
|
153
|
+
.map((other) => normalizePath(path.relative(app.paths.root, other.filepath)))
|
|
154
|
+
.join(', ')}.`,
|
|
155
|
+
},
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
trackDuplicates(
|
|
159
|
+
routes.server,
|
|
160
|
+
(route) => (route.path ? `${route.methodName}:${route.path}` : undefined),
|
|
161
|
+
{
|
|
162
|
+
code: 'route.duplicate-server-route',
|
|
163
|
+
level: 'warning',
|
|
164
|
+
message: (route, others) =>
|
|
165
|
+
`Duplicate server route "${(route as TProteumManifestRoute).methodName.toUpperCase()} ${(route as TProteumManifestRoute).path}" also registered in ${others
|
|
166
|
+
.map((other) => normalizePath(path.relative(app.paths.root, other.filepath)))
|
|
167
|
+
.join(', ')}.`,
|
|
168
|
+
},
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
trackDuplicates(controllers, (controller) => controller.clientAccessor, {
|
|
172
|
+
code: 'controller.duplicate-client-accessor',
|
|
173
|
+
level: 'error',
|
|
174
|
+
message: (controller, others) =>
|
|
175
|
+
`Duplicate controller accessor "${controller.clientAccessor}" also registered in ${others
|
|
176
|
+
.map((other) => normalizePath(path.relative(app.paths.root, other.filepath)))
|
|
177
|
+
.join(', ')}.`,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
trackDuplicates(controllers, (controller) => controller.httpPath, {
|
|
181
|
+
code: 'controller.duplicate-http-path',
|
|
182
|
+
level: 'error',
|
|
183
|
+
message: (controller, others) =>
|
|
184
|
+
`Duplicate controller HTTP path "${controller.httpPath}" also registered in ${others
|
|
185
|
+
.map((other) => normalizePath(path.relative(app.paths.root, other.filepath)))
|
|
186
|
+
.join(', ')}.`,
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const postServerRoutesByPath = new Map(
|
|
190
|
+
routes.server
|
|
191
|
+
.filter((route) => route.methodName === 'post' && !!route.path)
|
|
192
|
+
.map((route) => [route.path as string, route]),
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
for (const controller of controllers) {
|
|
196
|
+
const matchingRoute = postServerRoutesByPath.get(controller.httpPath);
|
|
197
|
+
|
|
198
|
+
if (!matchingRoute) continue;
|
|
199
|
+
|
|
200
|
+
pushDiagnostic({
|
|
201
|
+
level: 'error',
|
|
202
|
+
code: 'controller.server-route-collision',
|
|
203
|
+
message: `Controller path "${controller.httpPath}" collides with an explicit POST server route.`,
|
|
204
|
+
filepath: controller.filepath,
|
|
205
|
+
sourceLocation: controller.sourceLocation,
|
|
206
|
+
relatedFilepaths: [matchingRoute.filepath],
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return diagnostics.sort((a, b) => {
|
|
211
|
+
if (a.level !== b.level) return a.level === 'error' ? -1 : 1;
|
|
212
|
+
if (a.filepath !== b.filepath) return a.filepath.localeCompare(b.filepath);
|
|
213
|
+
if ((a.sourceLocation?.line || 0) !== (b.sourceLocation?.line || 0)) {
|
|
214
|
+
return (a.sourceLocation?.line || 0) - (b.sourceLocation?.line || 0);
|
|
215
|
+
}
|
|
216
|
+
return a.code.localeCompare(b.code);
|
|
217
|
+
});
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
export const writeCurrentProteumManifest = ({
|
|
221
|
+
services,
|
|
222
|
+
controllers,
|
|
223
|
+
routes,
|
|
224
|
+
layouts,
|
|
225
|
+
}: {
|
|
226
|
+
services: TProteumManifest['services'];
|
|
227
|
+
controllers: TProteumManifestController[];
|
|
228
|
+
routes: TProteumManifest['routes'];
|
|
229
|
+
layouts: TProteumManifestLayout[];
|
|
230
|
+
}) => {
|
|
231
|
+
const manifest: TProteumManifest = {
|
|
232
|
+
version: 1,
|
|
233
|
+
app: {
|
|
234
|
+
root: normalizeAbsolutePath(app.paths.root),
|
|
235
|
+
coreRoot: normalizeAbsolutePath(cli.paths.core.root),
|
|
236
|
+
identityFilepath: normalizeAbsolutePath(path.join(app.paths.root, 'identity.yaml')),
|
|
237
|
+
identity: {
|
|
238
|
+
name: app.identity.name,
|
|
239
|
+
identifier: app.identity.identifier,
|
|
240
|
+
description: app.identity.description,
|
|
241
|
+
language: app.identity.language,
|
|
242
|
+
locale: app.identity.locale,
|
|
243
|
+
title: app.identity.web?.title,
|
|
244
|
+
titleSuffix: app.identity.web?.titleSuffix,
|
|
245
|
+
fullTitle: app.identity.web?.fullTitle,
|
|
246
|
+
webDescription: app.identity.web?.description,
|
|
247
|
+
version: app.identity.web?.version,
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
conventions: {
|
|
251
|
+
routeSetupOptionKeys: [...routeSetupOptionKeys],
|
|
252
|
+
reservedRouteSetupKeys: [...reservedRouteSetupKeys],
|
|
253
|
+
},
|
|
254
|
+
env: {
|
|
255
|
+
sourceFilepath: normalizeAbsolutePath(path.join(app.paths.root, 'env.yaml')),
|
|
256
|
+
loadedTopLevelKeys: getEnvTopLevelKeys(),
|
|
257
|
+
requiredTopLevelKeys: [...envRequiredTopLevelKeys],
|
|
258
|
+
},
|
|
259
|
+
services,
|
|
260
|
+
controllers,
|
|
261
|
+
routes,
|
|
262
|
+
layouts,
|
|
263
|
+
diagnostics: collectManifestDiagnostics({ controllers, routes }),
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
writeProteumManifest(app.paths.root, manifest);
|
|
267
|
+
};
|