webspresso 0.0.112 → 0.0.114
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/bin/commands/admin-password.js +1 -7
- package/bin/commands/api.js +1 -7
- package/bin/commands/new.js +1 -7
- package/bin/commands/page.js +1 -7
- package/bin/commands/seed.js +1 -7
- package/bin/commands/service.js +1 -7
- package/bin/utils/prompt.js +17 -0
- package/core/cache/tagged-lru.js +9 -3
- package/core/client/adapter/datastar.js +602 -26
- package/core/client/api.js +151 -7
- package/core/client/controller.js +172 -11
- package/core/client/devtool.js +59 -33
- package/core/client/forms.js +479 -29
- package/core/client/fragments.js +456 -27
- package/core/client/index.js +18 -1
- package/core/client/server-fragments.js +125 -40
- package/core/client/utils.js +72 -17
- package/core/console/commands.js +26 -5
- package/core/console/repl.js +7 -1
- package/core/http/pipeline.js +82 -1
- package/core/openapi/build-from-api-routes.js +5 -5
- package/core/orm/json-fields.js +7 -6
- package/core/orm/schema-helpers.js +18 -15
- package/core/orm/utils/nanoid.js +7 -8
- package/core/plugins/plugin-manager.js +155 -1
- package/core/services/validator.js +12 -1
- package/core/validation/apply.js +3 -0
- package/core/validation/index.js +25 -0
- package/core/validation/zod-introspect.js +187 -0
- package/index.d.ts +113 -1
- package/package.json +12 -11
- package/plugins/mcp/providers/services.js +2 -2
- package/src/api/define-api.js +2 -0
- package/src/api/validation-i18n.js +6 -2
- package/src/bundle/index.js +140 -6
- package/src/dev/api-typegen.js +79 -22
- package/src/dev/typegen.js +61 -35
- package/src/dev-reload/index.js +44 -2
- package/src/file-router.js +24 -6
- package/src/routing/mount-discovered-routes.js +24 -6
- package/src/server.js +59 -3
package/index.d.ts
CHANGED
|
@@ -1919,6 +1919,9 @@ export interface PluginRegisterContext {
|
|
|
1919
1919
|
addHelper(name: string, fn: (...args: unknown[]) => unknown): void;
|
|
1920
1920
|
addFilter(name: string, fn: (...args: unknown[]) => unknown): void;
|
|
1921
1921
|
addRoute(method: string, path: string, ...handlers: RequestHandler[]): void;
|
|
1922
|
+
registerClientController(name: string, filePath: string): void;
|
|
1923
|
+
registerClientService(name: string, filePath: string): void;
|
|
1924
|
+
registerFragment(name: string, fragmentDef: FragmentDefinition<any, any>): void;
|
|
1922
1925
|
routes: unknown;
|
|
1923
1926
|
[key: string]: unknown;
|
|
1924
1927
|
}
|
|
@@ -1936,6 +1939,18 @@ export interface WebspressoPlugin {
|
|
|
1936
1939
|
}
|
|
1937
1940
|
|
|
1938
1941
|
export class PluginManager {
|
|
1942
|
+
registerClientController(name: string, filePath: string, pluginName?: string): void;
|
|
1943
|
+
registerClientService(name: string, filePath: string, pluginName?: string): void;
|
|
1944
|
+
registerFragment(name: string, fragmentDef: FragmentDefinition<any, any>, pluginName?: string): void;
|
|
1945
|
+
getClientControllers(): Array<{ name: string; filePath: string; pluginName: string }>;
|
|
1946
|
+
getClientServices(): Array<{ name: string; filePath: string; pluginName: string }>;
|
|
1947
|
+
getFragment(name: string): FragmentDefinition<any, any> | null;
|
|
1948
|
+
getFragments(): Record<string, FragmentDefinition<any, any>>;
|
|
1949
|
+
getClientManifest(): {
|
|
1950
|
+
controllers: Array<{ name: string; filePath: string; pluginName: string }>;
|
|
1951
|
+
services: Array<{ name: string; filePath: string; pluginName: string }>;
|
|
1952
|
+
fragments: string[];
|
|
1953
|
+
};
|
|
1939
1954
|
[key: string]: unknown;
|
|
1940
1955
|
}
|
|
1941
1956
|
|
|
@@ -5075,6 +5090,11 @@ export interface DefineApiOptions<
|
|
|
5075
5090
|
middleware?: (string | import('express').RequestHandler | [string, Record<string, unknown>])[];
|
|
5076
5091
|
flag?: string | { key: string; statusCode?: number; message?: string };
|
|
5077
5092
|
schema?: TSchema;
|
|
5093
|
+
validateFields?: boolean | string[];
|
|
5094
|
+
validate?: (
|
|
5095
|
+
req: ApiRequest<TBody, TParams, TQuery, THeaders>,
|
|
5096
|
+
ctx: { body: TBody; fields: string[]; req: ApiRequest<TBody, TParams, TQuery, THeaders>; res: ApiResponse<TBody, TParams, TQuery, THeaders> }
|
|
5097
|
+
) => Promise<Record<string, string> | null | undefined> | Record<string, string> | null | undefined;
|
|
5078
5098
|
response?: Record<number | string, unknown>;
|
|
5079
5099
|
handler: (
|
|
5080
5100
|
req: ApiRequest<TBody, TParams, TQuery, THeaders>,
|
|
@@ -5094,6 +5114,16 @@ export function defineApi<
|
|
|
5094
5114
|
>(
|
|
5095
5115
|
config: {
|
|
5096
5116
|
schema: TSchema;
|
|
5117
|
+
validateFields?: boolean | string[];
|
|
5118
|
+
validate?: (
|
|
5119
|
+
req: ApiRequest<ApiNoInfer<TBody>, ApiNoInfer<TParams>, ApiNoInfer<TQuery>, ApiNoInfer<THeaders>>,
|
|
5120
|
+
ctx: {
|
|
5121
|
+
body: ApiNoInfer<TBody>;
|
|
5122
|
+
fields: string[];
|
|
5123
|
+
req: ApiRequest<ApiNoInfer<TBody>, ApiNoInfer<TParams>, ApiNoInfer<TQuery>, ApiNoInfer<THeaders>>;
|
|
5124
|
+
res: ApiResponse<ApiNoInfer<TBody>, ApiNoInfer<TParams>, ApiNoInfer<TQuery>, ApiNoInfer<THeaders>>;
|
|
5125
|
+
}
|
|
5126
|
+
) => Promise<Record<string, string> | null | undefined> | Record<string, string> | null | undefined;
|
|
5097
5127
|
description?: string;
|
|
5098
5128
|
tags?: string[];
|
|
5099
5129
|
middleware?: (string | import('express').RequestHandler | [string, Record<string, unknown>])[];
|
|
@@ -6367,6 +6397,9 @@ export class ClientRequestError<TData = unknown> extends Error {
|
|
|
6367
6397
|
readonly isTimedOut: boolean;
|
|
6368
6398
|
readonly isDropped: boolean;
|
|
6369
6399
|
readonly isSuperseded: boolean;
|
|
6400
|
+
readonly dispatched: boolean;
|
|
6401
|
+
readonly outcomeUnknown: boolean;
|
|
6402
|
+
readonly outcome: 'unknown' | 'failed' | 'not_sent';
|
|
6370
6403
|
readonly cause?: unknown;
|
|
6371
6404
|
constructor(options: {
|
|
6372
6405
|
message: string;
|
|
@@ -6383,6 +6416,9 @@ export class ClientRequestError<TData = unknown> extends Error {
|
|
|
6383
6416
|
isTimedOut?: boolean;
|
|
6384
6417
|
isDropped?: boolean;
|
|
6385
6418
|
isSuperseded?: boolean;
|
|
6419
|
+
dispatched?: boolean;
|
|
6420
|
+
outcomeUnknown?: boolean;
|
|
6421
|
+
outcome?: 'unknown' | 'failed' | 'not_sent';
|
|
6386
6422
|
});
|
|
6387
6423
|
}
|
|
6388
6424
|
|
|
@@ -6411,6 +6447,12 @@ export interface ClientRequestCallOptions<
|
|
|
6411
6447
|
timeout?: number;
|
|
6412
6448
|
retry?: boolean | number | ClientRetryOptions;
|
|
6413
6449
|
retryMutations?: boolean;
|
|
6450
|
+
onUploadProgress?: (progress: {
|
|
6451
|
+
loaded: number;
|
|
6452
|
+
total: number;
|
|
6453
|
+
percent: number | null;
|
|
6454
|
+
lengthComputable: boolean;
|
|
6455
|
+
}) => void;
|
|
6414
6456
|
}
|
|
6415
6457
|
|
|
6416
6458
|
export interface AppApiRouteMap {
|
|
@@ -6440,6 +6482,8 @@ export interface ClientTaskOptions {
|
|
|
6440
6482
|
concurrency?: ClientConcurrencyPolicy;
|
|
6441
6483
|
policy?: ClientConcurrencyPolicy;
|
|
6442
6484
|
key?: string;
|
|
6485
|
+
mutation?: boolean;
|
|
6486
|
+
kind?: 'query' | 'mutation';
|
|
6443
6487
|
staleTime?: number | string;
|
|
6444
6488
|
refetchOnMount?: ClientRefetchPolicy;
|
|
6445
6489
|
refetchOnVisit?: ClientRefetchPolicy;
|
|
@@ -6714,6 +6758,8 @@ export interface ClientFormSubmitResult<TData = any> {
|
|
|
6714
6758
|
hookError?: Error | null;
|
|
6715
6759
|
error?: ClientRequestError | Error;
|
|
6716
6760
|
aborted?: boolean;
|
|
6761
|
+
outcomeUnknown?: boolean;
|
|
6762
|
+
outcome?: 'unknown' | 'failed' | 'not_sent';
|
|
6717
6763
|
}
|
|
6718
6764
|
|
|
6719
6765
|
export interface ClientFormHookMeta<
|
|
@@ -6738,6 +6784,9 @@ export interface ClientFormHandle<
|
|
|
6738
6784
|
readonly services: ClientServicesRegistry<TServices>;
|
|
6739
6785
|
readonly pending: boolean;
|
|
6740
6786
|
readonly dirty: boolean;
|
|
6787
|
+
readonly touched: boolean;
|
|
6788
|
+
readonly touchedFields: string[];
|
|
6789
|
+
readonly submitted: boolean;
|
|
6741
6790
|
readonly inputRevision: number;
|
|
6742
6791
|
readonly draftKey: string;
|
|
6743
6792
|
readonly baseline: TValues;
|
|
@@ -6746,6 +6795,34 @@ export interface ClientFormHandle<
|
|
|
6746
6795
|
readonly lastError: ClientRequestError | Error | null;
|
|
6747
6796
|
readonly fieldErrors: Partial<Record<keyof TValues & string, string>> & Record<string, string>;
|
|
6748
6797
|
disabled: boolean;
|
|
6798
|
+
isTouched(fieldName?: keyof TValues & string | string): boolean;
|
|
6799
|
+
touch(fieldName?: keyof TValues & string | string): this;
|
|
6800
|
+
untouch(fieldName?: keyof TValues & string | string): this;
|
|
6801
|
+
validateField(
|
|
6802
|
+
fieldName: (keyof TValues & string) | string,
|
|
6803
|
+
options?: { url?: string; method?: string; contextFields?: string[] | '*' }
|
|
6804
|
+
): Promise<{
|
|
6805
|
+
ok: boolean;
|
|
6806
|
+
valid?: boolean;
|
|
6807
|
+
stale?: boolean;
|
|
6808
|
+
canceled?: boolean;
|
|
6809
|
+
networkError?: boolean;
|
|
6810
|
+
errors?: Record<string, string>;
|
|
6811
|
+
error?: Error;
|
|
6812
|
+
}>;
|
|
6813
|
+
readonly uploads: Array<{
|
|
6814
|
+
field: string;
|
|
6815
|
+
name: string;
|
|
6816
|
+
size: number;
|
|
6817
|
+
status: 'uploading' | 'done' | 'error' | 'canceled';
|
|
6818
|
+
loaded: number;
|
|
6819
|
+
total: number;
|
|
6820
|
+
percent: number | null;
|
|
6821
|
+
error: string | null;
|
|
6822
|
+
}>;
|
|
6823
|
+
cancelUpload(): boolean;
|
|
6824
|
+
createPreviewUrl(file: File | Blob, fieldName?: string): string | null;
|
|
6825
|
+
revokePreviewUrls(fieldName?: string): void;
|
|
6749
6826
|
values(): TValues;
|
|
6750
6827
|
read(submitter?: HTMLElement | null): TValues;
|
|
6751
6828
|
fill(values: Partial<TValues> & Record<string, unknown>, options?: ClientFormFillOptions): this;
|
|
@@ -6801,10 +6878,12 @@ export interface ControllerFormsRegistry<TServices = AppClientServices> {
|
|
|
6801
6878
|
|
|
6802
6879
|
export interface FragmentRefreshResult {
|
|
6803
6880
|
ok: boolean;
|
|
6881
|
+
partial?: boolean;
|
|
6804
6882
|
refreshed: string[];
|
|
6805
6883
|
errors?: Error[];
|
|
6806
6884
|
skipped?: boolean;
|
|
6807
6885
|
canceled?: boolean;
|
|
6886
|
+
superseded?: boolean;
|
|
6808
6887
|
error?: Error | null;
|
|
6809
6888
|
timings?: {
|
|
6810
6889
|
loadMs: number;
|
|
@@ -6816,6 +6895,12 @@ export interface FragmentRefreshOptions {
|
|
|
6816
6895
|
replaceInput?: boolean;
|
|
6817
6896
|
timeout?: number;
|
|
6818
6897
|
throwOnError?: boolean;
|
|
6898
|
+
mode?: 'replace' | 'append' | 'prepend';
|
|
6899
|
+
limit?: number;
|
|
6900
|
+
target?: string;
|
|
6901
|
+
removeKeys?: string[];
|
|
6902
|
+
onBeforePatch?: (el: Element, inst: any) => void;
|
|
6903
|
+
onAfterPatch?: (el: Element, inst: any) => void;
|
|
6819
6904
|
onSuccess?: (result: FragmentRefreshResult) => void | Promise<void>;
|
|
6820
6905
|
onFailure?: (error: Error) => void | Promise<void>;
|
|
6821
6906
|
onError?: (error: Error) => void | Promise<void>;
|
|
@@ -6825,6 +6910,7 @@ export interface FragmentRefreshOptions {
|
|
|
6825
6910
|
|
|
6826
6911
|
export interface ControllerFragmentHandle<TInput extends Record<string, any> = Record<string, any>> {
|
|
6827
6912
|
readonly input: TInput;
|
|
6913
|
+
readonly inputs: Record<string, TInput>;
|
|
6828
6914
|
refresh(
|
|
6829
6915
|
nextInput?: Partial<TInput> & Record<string, unknown>,
|
|
6830
6916
|
options?: FragmentRefreshOptions
|
|
@@ -6836,10 +6922,17 @@ export interface ControllerErrorMeta {
|
|
|
6836
6922
|
task?: string;
|
|
6837
6923
|
event?: Event;
|
|
6838
6924
|
actionEl?: HTMLElement | null;
|
|
6839
|
-
phase?: 'action' | 'task' | 'connect' | 'disconnect';
|
|
6925
|
+
phase?: 'action' | 'task' | 'connect' | 'disconnect' | 'beforePatch' | 'afterPatch';
|
|
6840
6926
|
[key: string]: unknown;
|
|
6841
6927
|
}
|
|
6842
6928
|
|
|
6929
|
+
export interface FragmentPatchHookMeta {
|
|
6930
|
+
fragmentId?: string | null;
|
|
6931
|
+
fragmentName?: string | null;
|
|
6932
|
+
isFragmentRoot?: boolean;
|
|
6933
|
+
isDescendant?: boolean;
|
|
6934
|
+
}
|
|
6935
|
+
|
|
6843
6936
|
export type WatchSource<T = any> =
|
|
6844
6937
|
| (() => T)
|
|
6845
6938
|
| string
|
|
@@ -6988,6 +7081,22 @@ export interface ControllerDefinition<
|
|
|
6988
7081
|
TServices
|
|
6989
7082
|
>
|
|
6990
7083
|
) => void;
|
|
7084
|
+
beforePatch?: (
|
|
7085
|
+
ctx: ControllerContext<
|
|
7086
|
+
[TState][TState extends any ? 0 : never] & { readonly [K in keyof TComputed]: ReturnType<TComputed[K]> },
|
|
7087
|
+
Record<string, ClientTask>,
|
|
7088
|
+
TServices
|
|
7089
|
+
>,
|
|
7090
|
+
meta: FragmentPatchHookMeta
|
|
7091
|
+
) => void;
|
|
7092
|
+
afterPatch?: (
|
|
7093
|
+
ctx: ControllerContext<
|
|
7094
|
+
[TState][TState extends any ? 0 : never] & { readonly [K in keyof TComputed]: ReturnType<TComputed[K]> },
|
|
7095
|
+
Record<string, ClientTask>,
|
|
7096
|
+
TServices
|
|
7097
|
+
>,
|
|
7098
|
+
meta: FragmentPatchHookMeta
|
|
7099
|
+
) => void | Promise<void>;
|
|
6991
7100
|
onError?: ControllerErrorHandler<
|
|
6992
7101
|
[TState][TState extends any ? 0 : never] & { readonly [K in keyof TComputed]: ReturnType<TComputed[K]> },
|
|
6993
7102
|
Record<string, ClientTask>,
|
|
@@ -7052,6 +7161,9 @@ export interface FragmentDefinition<TInput = Record<string, unknown>, TData = Re
|
|
|
7052
7161
|
refetchOnVisit?: ClientRefetchPolicy;
|
|
7053
7162
|
refetchInterval?: number | string | false;
|
|
7054
7163
|
refetchIntervalInBackground?: boolean;
|
|
7164
|
+
lazy?: boolean;
|
|
7165
|
+
placeholder?: string | ((ctx: { input: TInput; instanceId: string; name: string }) => string);
|
|
7166
|
+
placeholderTemplate?: string;
|
|
7055
7167
|
load?: (context: FragmentLoaderContext<TInput>) => TData | Promise<TData>;
|
|
7056
7168
|
}
|
|
7057
7169
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webspresso",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.114",
|
|
4
4
|
"description": "Minimal, production-ready SSR framework for Node.js with file-based routing, Nunjucks templating, built-in i18n, and CLI tooling",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
"bench:ci:local": "npm run bench:baseline && npm run bench:ci",
|
|
101
101
|
"bench:profile": "softscope --top 30 node scripts/profile-bench.mjs",
|
|
102
102
|
"bench:profile:ai": "softscope --top 40 -o softscope-bench.ai node scripts/profile-bench.mjs",
|
|
103
|
-
"check:types": "tsc --project tests/ts-smoke/tsconfig.json && tsc --noEmit --skipLibCheck false --types node index.d.ts",
|
|
103
|
+
"check:types": "tsc --project tests/ts-smoke/tsconfig.json && tsc --noEmit --skipLibCheck false --esModuleInterop --types node index.d.ts",
|
|
104
104
|
"rebuild:native": "npm rebuild better-sqlite3 bcrypt sharp",
|
|
105
105
|
"docs:dev": "docboot dev ./docs",
|
|
106
106
|
"docs:build": "docboot build ./docs",
|
|
@@ -134,21 +134,21 @@
|
|
|
134
134
|
"views/partials/webspresso-client-runtime.njk"
|
|
135
135
|
],
|
|
136
136
|
"dependencies": {
|
|
137
|
-
"commander": "^
|
|
137
|
+
"commander": "^14.0.3",
|
|
138
138
|
"cookie-parser": "^1.4.7",
|
|
139
139
|
"csv-parse": "^7.0.2",
|
|
140
140
|
"dayjs": "^1.11.19",
|
|
141
141
|
"express": "^5.2.1",
|
|
142
142
|
"express-session": "^1.18.0",
|
|
143
|
-
"helmet": "^
|
|
144
|
-
"inquirer": "^
|
|
143
|
+
"helmet": "^8.3.0",
|
|
144
|
+
"inquirer": "^13.4.3",
|
|
145
145
|
"knex": "^3.1.0",
|
|
146
146
|
"multer": "^2.4.0",
|
|
147
147
|
"nunjucks": "^3.2.4",
|
|
148
148
|
"qs": "^6.17.1",
|
|
149
149
|
"sanitize-html": "^2.18.1",
|
|
150
150
|
"yaml": "^2.8.3",
|
|
151
|
-
"zod": "^
|
|
151
|
+
"zod": "^4.6.7",
|
|
152
152
|
"zod-to-json-schema": "^3.25.2"
|
|
153
153
|
},
|
|
154
154
|
"peerDependencies": {
|
|
@@ -218,7 +218,7 @@
|
|
|
218
218
|
"@swup/scripts-plugin": "^2.1.0",
|
|
219
219
|
"@types/express": "^5.0.0",
|
|
220
220
|
"@types/node": "^20.14.0",
|
|
221
|
-
"@vitest/coverage-v8": "^4.1.
|
|
221
|
+
"@vitest/coverage-v8": "^4.1.12",
|
|
222
222
|
"bcrypt": "^6.0.0",
|
|
223
223
|
"better-sqlite3": "^11.10.0",
|
|
224
224
|
"chokidar": "^4.0.3",
|
|
@@ -226,7 +226,7 @@
|
|
|
226
226
|
"dotenv": "^16.3.1",
|
|
227
227
|
"exceljs": "^4.4.0",
|
|
228
228
|
"express-rate-limit": "^8.4.1",
|
|
229
|
-
"hono": "^4.13.
|
|
229
|
+
"hono": "^4.13.16",
|
|
230
230
|
"mjml": "^5.4.0",
|
|
231
231
|
"nodemailer": "^10.0.10",
|
|
232
232
|
"release-it": "^20.2.1",
|
|
@@ -235,14 +235,14 @@
|
|
|
235
235
|
"supertest": "^6.3.4",
|
|
236
236
|
"swup": "^4.8.3",
|
|
237
237
|
"typescript": "~5.6.3",
|
|
238
|
-
"vitest": "^4.1.
|
|
238
|
+
"vitest": "^4.1.12",
|
|
239
239
|
"vitest-doctor": "^0.3.0"
|
|
240
240
|
},
|
|
241
241
|
"engines": {
|
|
242
242
|
"node": ">=20.0.0"
|
|
243
243
|
},
|
|
244
244
|
"overrides": {
|
|
245
|
-
"basic-ftp": "^6.2.
|
|
245
|
+
"basic-ftp": "^6.2.4",
|
|
246
246
|
"chokidar": "^4.0.3",
|
|
247
247
|
"katex": "^0.18.2",
|
|
248
248
|
"tar": "^7.5.11",
|
|
@@ -251,7 +251,8 @@
|
|
|
251
251
|
"js-yaml": "^5.4.2",
|
|
252
252
|
"qs": "^6.17.1",
|
|
253
253
|
"sanitize-html": "^2.18.1",
|
|
254
|
-
"vite": "^8.3.5"
|
|
254
|
+
"vite": "^8.3.5",
|
|
255
|
+
"minimatch@>=7.0.0 <=11.0.0": "^11.0.8"
|
|
255
256
|
},
|
|
256
257
|
"allowScripts": {
|
|
257
258
|
"bcrypt@6.0.0": true,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @module plugins/mcp/providers/services
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
const {
|
|
7
|
+
const { zodToJsonSchemaCompat } = require('../../../core/validation/zod-introspect');
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Normalizes a dot-separated service name to an MCP-safe tool name
|
|
@@ -99,7 +99,7 @@ function convertZodToInputSchema(schema) {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
try {
|
|
102
|
-
const rawSchema =
|
|
102
|
+
const rawSchema = zodToJsonSchemaCompat(schema, {
|
|
103
103
|
$refStrategy: 'none',
|
|
104
104
|
removeAdditionalStrategy: 'strict',
|
|
105
105
|
});
|
package/src/api/define-api.js
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
* @param {Array<string|Function>} [config.middleware] - Named or functional middlewares
|
|
14
14
|
* @param {string|{ key: string, statusCode?: number, message?: string }} [config.flag] - Feature flag required to access this endpoint
|
|
15
15
|
* @param {Object|Function} [config.schema] - Request schemas (params, query, body, headers)
|
|
16
|
+
* @param {boolean|string[]} [config.validateFields] - Opt-in field-level validation without running mutation handler
|
|
17
|
+
* @param {Function} [config.validate] - Optional async read-only validation hook: async (ctx, fields) => ({ field: 'msg' })
|
|
16
18
|
* @param {Object} [config.response] - Expected response schemas by status code
|
|
17
19
|
* @param {Function} config.handler - Request handler: async (req, ctx) => data
|
|
18
20
|
* @returns {Object}
|
|
@@ -94,8 +94,12 @@ function translateSingleIssue(issue, t) {
|
|
|
94
94
|
|
|
95
95
|
const pathKey = Array.isArray(issue.path) && issue.path.length > 0 ? issue.path.join('.') : '';
|
|
96
96
|
const field = Array.isArray(issue.path) && issue.path.length > 0 ? issue.path[issue.path.length - 1] : '';
|
|
97
|
-
const isRequired = (issue.code === 'invalid_type' && (
|
|
98
|
-
|
|
97
|
+
const isRequired = (issue.code === 'invalid_type' && (
|
|
98
|
+
issue.received === 'undefined' ||
|
|
99
|
+
issue.input === undefined ||
|
|
100
|
+
issue.message === 'Required' ||
|
|
101
|
+
/received undefined$/i.test(issue.message || '')
|
|
102
|
+
)) || issue.message === 'Required';
|
|
99
103
|
|
|
100
104
|
const params = Object.assign(
|
|
101
105
|
{
|
package/src/bundle/index.js
CHANGED
|
@@ -213,22 +213,52 @@ function findPageEntryPoints(cwd = process.cwd()) {
|
|
|
213
213
|
walkViews(dirToScan);
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
+
// 4. Plugin-registered client controllers and services (if PluginManager is active)
|
|
217
|
+
try {
|
|
218
|
+
const { getPluginManager } = require('../../core/plugins/plugin-manager');
|
|
219
|
+
const pm = typeof getPluginManager === 'function' ? getPluginManager() : null;
|
|
220
|
+
if (pm) {
|
|
221
|
+
if (typeof pm.getClientControllers === 'function') {
|
|
222
|
+
for (const ctrl of pm.getClientControllers()) {
|
|
223
|
+
const entryName = `plugins/${ctrl.pluginName}/${ctrl.name}`;
|
|
224
|
+
if (!pageEntries[entryName]) {
|
|
225
|
+
pageEntries[entryName] = path.isAbsolute(ctrl.filePath)
|
|
226
|
+
? ctrl.filePath
|
|
227
|
+
: path.resolve(cwd, ctrl.filePath);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (typeof pm.getClientServices === 'function') {
|
|
232
|
+
for (const svc of pm.getClientServices()) {
|
|
233
|
+
const entryName = `plugins/${svc.pluginName}/services/${svc.name}`;
|
|
234
|
+
if (!pageEntries[entryName]) {
|
|
235
|
+
pageEntries[entryName] = path.isAbsolute(svc.filePath)
|
|
236
|
+
? svc.filePath
|
|
237
|
+
: path.resolve(cwd, svc.filePath);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
} catch (_e) {}
|
|
243
|
+
|
|
216
244
|
return pageEntries;
|
|
217
245
|
}
|
|
218
246
|
|
|
219
247
|
/**
|
|
220
|
-
* Discovers all `.client.{js,ts,mjs,jsx,tsx}` controller files across `pages
|
|
248
|
+
* Discovers all `.client.{js,ts,mjs,jsx,tsx}` controller files across `pages/`, `views/`, and plugins
|
|
221
249
|
* and validates deterministic controller names for collisions.
|
|
222
250
|
*
|
|
223
251
|
* @param {string} [cwd=process.cwd()]
|
|
224
|
-
* @
|
|
252
|
+
* @param {object} [options]
|
|
253
|
+
* @returns {Array<{ controllerName: string, entryName: string, filePath: string, relativePath: string, pluginName?: string }>}
|
|
225
254
|
*/
|
|
226
|
-
function discoverClientControllers(cwd = process.cwd()) {
|
|
255
|
+
function discoverClientControllers(cwd = process.cwd(), options = {}) {
|
|
227
256
|
const pageEntries = findPageEntryPoints(cwd);
|
|
228
257
|
const discovered = [];
|
|
229
258
|
const byControllerName = new Map();
|
|
230
259
|
|
|
231
260
|
for (const [entryName, filePath] of Object.entries(pageEntries)) {
|
|
261
|
+
if (entryName.startsWith('plugins/')) continue;
|
|
232
262
|
const relativePath = path.relative(cwd, filePath).replace(/\\/g, '/');
|
|
233
263
|
const controllerName = deriveControllerNameFromPath(relativePath);
|
|
234
264
|
if (!controllerName) continue;
|
|
@@ -256,6 +286,46 @@ function discoverClientControllers(cwd = process.cwd()) {
|
|
|
256
286
|
discovered.push(record);
|
|
257
287
|
}
|
|
258
288
|
|
|
289
|
+
try {
|
|
290
|
+
const pm =
|
|
291
|
+
options.pluginManager ||
|
|
292
|
+
require('../../core/plugins/plugin-manager').getPluginManager();
|
|
293
|
+
if (pm && typeof pm.getClientControllers === 'function') {
|
|
294
|
+
for (const ctrl of pm.getClientControllers()) {
|
|
295
|
+
const absPath = path.isAbsolute(ctrl.filePath)
|
|
296
|
+
? ctrl.filePath
|
|
297
|
+
: path.resolve(cwd, ctrl.filePath);
|
|
298
|
+
const relativePath = path.relative(cwd, absPath).replace(/\\/g, '/');
|
|
299
|
+
if (byControllerName.has(ctrl.name)) {
|
|
300
|
+
const existing = byControllerName.get(ctrl.name);
|
|
301
|
+
if (existing.filePath !== absPath) {
|
|
302
|
+
const err = new Error(
|
|
303
|
+
`Controller name collision: "${ctrl.name}" is defined by both "${existing.relativePath}" and "${relativePath}".`
|
|
304
|
+
);
|
|
305
|
+
err.code = 'CONTROLLER_NAME_COLLISION';
|
|
306
|
+
err.controllerName = ctrl.name;
|
|
307
|
+
err.files = [existing.relativePath, relativePath];
|
|
308
|
+
throw err;
|
|
309
|
+
}
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
const record = {
|
|
313
|
+
controllerName: ctrl.name,
|
|
314
|
+
entryName: `plugins/${ctrl.pluginName}/${ctrl.name}`,
|
|
315
|
+
filePath: absPath,
|
|
316
|
+
relativePath,
|
|
317
|
+
pluginName: ctrl.pluginName,
|
|
318
|
+
};
|
|
319
|
+
byControllerName.set(ctrl.name, record);
|
|
320
|
+
discovered.push(record);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
} catch (err) {
|
|
324
|
+
if (err && err.code === 'CONTROLLER_NAME_COLLISION') {
|
|
325
|
+
throw err;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
259
329
|
return discovered;
|
|
260
330
|
}
|
|
261
331
|
|
|
@@ -944,17 +1014,34 @@ function buildBundleFallback({ cwd, entryPoints, outdir, publicDir, manifestPath
|
|
|
944
1014
|
const emittedChunksBySource = new Map(); // absSourcePath -> { absOut, relFromOutdir, relOutput }
|
|
945
1015
|
|
|
946
1016
|
function transpileSource(absFile, rawCode) {
|
|
1017
|
+
let code = rawCode;
|
|
947
1018
|
if ((absFile.endsWith('.ts') || absFile.endsWith('.tsx')) && ts) {
|
|
948
|
-
const transpiled = ts.transpileModule(
|
|
1019
|
+
const transpiled = ts.transpileModule(code, {
|
|
949
1020
|
compilerOptions: {
|
|
950
1021
|
module: ts.ModuleKind.ESNext,
|
|
951
1022
|
target: ts.ScriptTarget.ES2020,
|
|
952
1023
|
},
|
|
953
1024
|
fileName: absFile,
|
|
954
1025
|
});
|
|
955
|
-
|
|
1026
|
+
code = transpiled.outputText;
|
|
1027
|
+
}
|
|
1028
|
+
return code.replace(/\bprocess\.env\.NODE_ENV\b/g, '"production"');
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
function extractCjsExportNames(code) {
|
|
1032
|
+
const match = code.match(/module\.exports\s*=\s*\{([\s\S]*?)\};?\s*$/);
|
|
1033
|
+
if (!match) return [];
|
|
1034
|
+
const body = match[1].replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '');
|
|
1035
|
+
const names = new Set();
|
|
1036
|
+
const propRegex = /^\s*(?:get\s+|set\s+)?([A-Za-z_$][A-Za-z0-9_$]*)\s*(?:,|:|\()/gm;
|
|
1037
|
+
let m;
|
|
1038
|
+
while ((m = propRegex.exec(body)) !== null) {
|
|
1039
|
+
const name = m[1];
|
|
1040
|
+
if (name && name !== 'get' && name !== 'set' && name !== 'function') {
|
|
1041
|
+
names.add(name);
|
|
1042
|
+
}
|
|
956
1043
|
}
|
|
957
|
-
return
|
|
1044
|
+
return Array.from(names);
|
|
958
1045
|
}
|
|
959
1046
|
|
|
960
1047
|
function emitChunkModule(absModulePath) {
|
|
@@ -1038,6 +1125,53 @@ function buildBundleFallback({ cwd, entryPoints, outdir, publicDir, manifestPath
|
|
|
1038
1125
|
}
|
|
1039
1126
|
);
|
|
1040
1127
|
|
|
1128
|
+
// 3. Transform CommonJS modules (`module.exports = ...` / `require('./...')`) into native ESM chunks
|
|
1129
|
+
if (/\bmodule\.exports\b/.test(rewritten)) {
|
|
1130
|
+
const cjsImports = [];
|
|
1131
|
+
let depSeq = 0;
|
|
1132
|
+
const depVarBySpec = new Map();
|
|
1133
|
+
const exportNames = extractCjsExportNames(rewritten);
|
|
1134
|
+
|
|
1135
|
+
rewritten = rewritten.replace(
|
|
1136
|
+
/\b(?:reqFn|require)\s*\(\s*(['"])(\.[^'"]+)\1\s*\)/g,
|
|
1137
|
+
(fullMatch, quote, spec) => {
|
|
1138
|
+
if (spec === './debug' || spec === './devtool' || spec === './server-fragments') {
|
|
1139
|
+
return '{}';
|
|
1140
|
+
}
|
|
1141
|
+
const resolved = resolveLocalModuleFile(importerAbsPath, spec);
|
|
1142
|
+
if (!resolved) return fullMatch;
|
|
1143
|
+
const chunkInfo = emitChunkModule(resolved);
|
|
1144
|
+
if (!chunkInfo || !chunkInfo.absOut) return fullMatch;
|
|
1145
|
+
let relSpec = path.relative(emitterOutDir, chunkInfo.absOut).replace(/\\/g, '/');
|
|
1146
|
+
if (!relSpec.startsWith('.')) relSpec = './' + relSpec;
|
|
1147
|
+
let depVar = depVarBySpec.get(relSpec);
|
|
1148
|
+
if (!depVar) {
|
|
1149
|
+
depVar = `__cjs_dep_${++depSeq}`;
|
|
1150
|
+
depVarBySpec.set(relSpec, depVar);
|
|
1151
|
+
cjsImports.push(`import ${depVar} from ${quote}${relSpec}${quote};`);
|
|
1152
|
+
collectedImports.push({ path: chunkInfo.relOutput, kind: 'import-statement' });
|
|
1153
|
+
}
|
|
1154
|
+
return depVar;
|
|
1155
|
+
}
|
|
1156
|
+
);
|
|
1157
|
+
|
|
1158
|
+
const strippedBody = rewritten.replace(/^\s*['"]use strict['"];?\s*/, '');
|
|
1159
|
+
const namedExportLines = exportNames.map(
|
|
1160
|
+
(name) => `export const ${name} = __cjs_exports.${name};`
|
|
1161
|
+
);
|
|
1162
|
+
rewritten = [
|
|
1163
|
+
...cjsImports,
|
|
1164
|
+
'const __cjs_exports = (() => {',
|
|
1165
|
+
'const module = { exports: {} };',
|
|
1166
|
+
'const exports = module.exports;',
|
|
1167
|
+
strippedBody,
|
|
1168
|
+
'return module.exports;',
|
|
1169
|
+
'})();',
|
|
1170
|
+
'export default __cjs_exports;',
|
|
1171
|
+
...namedExportLines,
|
|
1172
|
+
].join('\n');
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1041
1175
|
return rewritten;
|
|
1042
1176
|
}
|
|
1043
1177
|
|