svelte-reflector 1.3.5 → 1.3.7
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/dist/core/generators/ApiCallStrategy.d.ts +1 -1
- package/dist/core/generators/ApiCallStrategy.js +3 -1
- package/dist/core/generators/CallMethodGenerator.js +2 -2
- package/dist/core/generators/CallStrategy.d.ts +9 -2
- package/dist/core/generators/ModuleCallStrategy.d.ts +1 -1
- package/dist/core/generators/ModuleCallStrategy.js +3 -1
- package/dist/core/method/Method.d.ts +10 -1
- package/dist/core/method/Method.js +9 -0
- package/dist/core/method/MethodNameDisambiguator.d.ts +38 -0
- package/dist/core/method/MethodNameDisambiguator.js +118 -0
- package/dist/core/module/Module.js +2 -0
- package/dist/core/module/ModuleClassBuilder.js +10 -4
- package/dist/core/module/ModuleMethodProcessor.js +7 -4
- package/dist/core/module/ModuleSchemaFileBuilder.js +1 -1
- package/dist/core/schema/SchemaClassRenderer.js +1 -1
- package/dist/runtime/reflector.svelte.ts +8 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { CallMethodInput, CallStrategy } from "./CallStrategy.js";
|
|
2
2
|
export declare class ApiCallStrategy implements CallStrategy {
|
|
3
|
-
|
|
3
|
+
listStateAccess(_method: CallMethodInput): string;
|
|
4
4
|
buildSignature(method: CallMethodInput): string;
|
|
5
5
|
entityStateAccess(_method: CallMethodInput): string;
|
|
6
6
|
formStateAccess(_method: CallMethodInput): string;
|
|
@@ -78,7 +78,7 @@ export class CallMethodGenerator {
|
|
|
78
78
|
endpoint,
|
|
79
79
|
queryData: { ${querys} }
|
|
80
80
|
})
|
|
81
|
-
${strategy.listStateAccess} = ${method.analyzers.request.responseType}.from(response.data);
|
|
81
|
+
${strategy.listStateAccess(method)} = ${method.analyzers.request.responseType}.from(response.data);
|
|
82
82
|
this.totalPages = response.totalPages;
|
|
83
83
|
`;
|
|
84
84
|
return { inside, outside: "" };
|
|
@@ -134,7 +134,7 @@ export class CallMethodGenerator {
|
|
|
134
134
|
buildMethodReturn(method, strategy) {
|
|
135
135
|
const { attributeType, responseType, hasEnumResponse, isPrimitiveResponse } = method.analyzers.request;
|
|
136
136
|
if (attributeType === "list")
|
|
137
|
-
return strategy.listStateAccess;
|
|
137
|
+
return strategy.listStateAccess(method);
|
|
138
138
|
if (!responseType)
|
|
139
139
|
return "null";
|
|
140
140
|
if (hasEnumResponse)
|
|
@@ -2,6 +2,11 @@ import type { MethodAnalyzers } from "../method/Method.js";
|
|
|
2
2
|
/** Structural subset of `Method` that the generator and strategies actually read. */
|
|
3
3
|
export interface CallMethodInput {
|
|
4
4
|
name: string;
|
|
5
|
+
/** Disambiguation suffix appended to the base name; empty when unique. */
|
|
6
|
+
nameSuffix: string;
|
|
7
|
+
/** Disambiguation suffix for the shared list state field; empty when
|
|
8
|
+
* the module has at most one list-typed method. */
|
|
9
|
+
stateSuffix: string;
|
|
5
10
|
endpoint: string;
|
|
6
11
|
description: string | undefined;
|
|
7
12
|
responseTypeInterface: string;
|
|
@@ -14,8 +19,10 @@ export interface CallMethodInput {
|
|
|
14
19
|
export interface CallStrategy {
|
|
15
20
|
/** Full method signature incl. params — e.g. `async call(params?: ...)` or `protected async _foo(params?: ...)` */
|
|
16
21
|
buildSignature(method: CallMethodInput): string;
|
|
17
|
-
/** State field holding list results — e.g. `this.list` (module) or `this.data` (api)
|
|
18
|
-
|
|
22
|
+
/** State field holding list results — e.g. `this.list` / `this.listControllers` (module) or `this.data` (api).
|
|
23
|
+
* Takes `method` so the module strategy can suffix the field when two list
|
|
24
|
+
* operations collide in the same controller. */
|
|
25
|
+
listStateAccess(method: CallMethodInput): string;
|
|
19
26
|
/** State field receiving the fetched entity */
|
|
20
27
|
entityStateAccess(method: CallMethodInput): string;
|
|
21
28
|
/** Form instance accessed to bundle body data */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { CallMethodInput, CallStrategy } from "./CallStrategy.js";
|
|
2
2
|
export declare class ModuleCallStrategy implements CallStrategy {
|
|
3
|
-
|
|
3
|
+
listStateAccess(method: CallMethodInput): string;
|
|
4
4
|
buildSignature(method: CallMethodInput): string;
|
|
5
5
|
entityStateAccess(method: CallMethodInput): string;
|
|
6
6
|
formStateAccess(method: CallMethodInput): string;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { treatByUppercase } from "../../helpers/helpers.js";
|
|
2
2
|
export class ModuleCallStrategy {
|
|
3
|
-
listStateAccess
|
|
3
|
+
listStateAccess(method) {
|
|
4
|
+
return `this.list${method.stateSuffix}`;
|
|
5
|
+
}
|
|
4
6
|
buildSignature(method) {
|
|
5
7
|
const paramsType = this.buildParamsType(method);
|
|
6
8
|
return `protected async _${method.name}(params?: ${paramsType})`;
|
|
@@ -22,7 +22,16 @@ export interface MethodAnalyzers {
|
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
24
|
export declare class Method {
|
|
25
|
-
|
|
25
|
+
name: string;
|
|
26
|
+
/** Disambiguation suffix appended to the base name when two methods in
|
|
27
|
+
* the same module collide (e.g. two `listAll`). Empty when unique. */
|
|
28
|
+
nameSuffix: string;
|
|
29
|
+
/** Disambiguation suffix for the shared list state field (`list`,
|
|
30
|
+
* `bundledList`, `clearList`). Set when the module has more than one
|
|
31
|
+
* `list`-typed method, even if the method names themselves are unique
|
|
32
|
+
* (e.g. `listAll` + `getMessages` both producing `list`). Empty when
|
|
33
|
+
* the module has at most one list method. */
|
|
34
|
+
stateSuffix: string;
|
|
26
35
|
readonly endpoint: string;
|
|
27
36
|
readonly apiType: ApiType;
|
|
28
37
|
readonly attributeType: ReflectorRequestType;
|
|
@@ -2,6 +2,15 @@ import { MethodBuilder } from "./MethodBuilder.js";
|
|
|
2
2
|
import { MethodGenerator } from "./generators/MethodGenerator.js";
|
|
3
3
|
export class Method {
|
|
4
4
|
name;
|
|
5
|
+
/** Disambiguation suffix appended to the base name when two methods in
|
|
6
|
+
* the same module collide (e.g. two `listAll`). Empty when unique. */
|
|
7
|
+
nameSuffix = "";
|
|
8
|
+
/** Disambiguation suffix for the shared list state field (`list`,
|
|
9
|
+
* `bundledList`, `clearList`). Set when the module has more than one
|
|
10
|
+
* `list`-typed method, even if the method names themselves are unique
|
|
11
|
+
* (e.g. `listAll` + `getMessages` both producing `list`). Empty when
|
|
12
|
+
* the module has at most one list method. */
|
|
13
|
+
stateSuffix = "";
|
|
5
14
|
endpoint;
|
|
6
15
|
apiType;
|
|
7
16
|
attributeType;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Method } from "./Method.js";
|
|
2
|
+
/**
|
|
3
|
+
* Two kinds of collision inside a single controller (module) produce invalid
|
|
4
|
+
* generated code. Both are fixed here by appending a suffix derived from the
|
|
5
|
+
* path's last non-parameterized segment.
|
|
6
|
+
*
|
|
7
|
+
* 1. Method-name collision (`nameSuffix`).
|
|
8
|
+
* Two operations with the same base name (e.g. two `listAll`, two
|
|
9
|
+
* `findOne`) would emit duplicate `_listAll` / `_findOne` methods and
|
|
10
|
+
* duplicate per-operation classes (`ListAll`, `FindOnePaths`, etc.).
|
|
11
|
+
*
|
|
12
|
+
* 2. List-state collision (`stateSuffix`).
|
|
13
|
+
* Any two list-typed operations in the same module emit `list = $state<…>`
|
|
14
|
+
* twice, even when their method names differ (e.g. `listAll` vs
|
|
15
|
+
* `getMessages`). The `list` field, `bundledList`, and `clearList()` must
|
|
16
|
+
* be suffixed per method so both are unique.
|
|
17
|
+
*
|
|
18
|
+
* Suffix derivation (same for both cases, so a single method can reuse one
|
|
19
|
+
* suffix for both):
|
|
20
|
+
* - Take the last non-`{param}` segment of the endpoint path.
|
|
21
|
+
* - Pluralize it when the path does NOT end with a `{param}`
|
|
22
|
+
* (collection endpoints: listAll, create).
|
|
23
|
+
* - Keep it singular when the path ends with a `{param}`
|
|
24
|
+
* (item endpoints: findOne, update, remove).
|
|
25
|
+
* - Camel-case the result to strip hyphens and capitalize.
|
|
26
|
+
*
|
|
27
|
+
* When a method has no collision of either kind, both suffixes stay empty
|
|
28
|
+
* and the output matches the pre-fix behavior — no churn for consumers of
|
|
29
|
+
* non-colliding controllers.
|
|
30
|
+
*/
|
|
31
|
+
export declare class MethodNameDisambiguator {
|
|
32
|
+
static apply(methods: Method[]): void;
|
|
33
|
+
private static disambiguateNames;
|
|
34
|
+
private static disambiguateListStates;
|
|
35
|
+
private static ensureUnique;
|
|
36
|
+
private static deriveSuffix;
|
|
37
|
+
private static pluralize;
|
|
38
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { capitalizeFirstLetter, toCamelCase } from "../../helpers/helpers.js";
|
|
2
|
+
/**
|
|
3
|
+
* Two kinds of collision inside a single controller (module) produce invalid
|
|
4
|
+
* generated code. Both are fixed here by appending a suffix derived from the
|
|
5
|
+
* path's last non-parameterized segment.
|
|
6
|
+
*
|
|
7
|
+
* 1. Method-name collision (`nameSuffix`).
|
|
8
|
+
* Two operations with the same base name (e.g. two `listAll`, two
|
|
9
|
+
* `findOne`) would emit duplicate `_listAll` / `_findOne` methods and
|
|
10
|
+
* duplicate per-operation classes (`ListAll`, `FindOnePaths`, etc.).
|
|
11
|
+
*
|
|
12
|
+
* 2. List-state collision (`stateSuffix`).
|
|
13
|
+
* Any two list-typed operations in the same module emit `list = $state<…>`
|
|
14
|
+
* twice, even when their method names differ (e.g. `listAll` vs
|
|
15
|
+
* `getMessages`). The `list` field, `bundledList`, and `clearList()` must
|
|
16
|
+
* be suffixed per method so both are unique.
|
|
17
|
+
*
|
|
18
|
+
* Suffix derivation (same for both cases, so a single method can reuse one
|
|
19
|
+
* suffix for both):
|
|
20
|
+
* - Take the last non-`{param}` segment of the endpoint path.
|
|
21
|
+
* - Pluralize it when the path does NOT end with a `{param}`
|
|
22
|
+
* (collection endpoints: listAll, create).
|
|
23
|
+
* - Keep it singular when the path ends with a `{param}`
|
|
24
|
+
* (item endpoints: findOne, update, remove).
|
|
25
|
+
* - Camel-case the result to strip hyphens and capitalize.
|
|
26
|
+
*
|
|
27
|
+
* When a method has no collision of either kind, both suffixes stay empty
|
|
28
|
+
* and the output matches the pre-fix behavior — no churn for consumers of
|
|
29
|
+
* non-colliding controllers.
|
|
30
|
+
*/
|
|
31
|
+
export class MethodNameDisambiguator {
|
|
32
|
+
static apply(methods) {
|
|
33
|
+
this.disambiguateNames(methods);
|
|
34
|
+
this.disambiguateListStates(methods);
|
|
35
|
+
}
|
|
36
|
+
static disambiguateNames(methods) {
|
|
37
|
+
const groups = new Map();
|
|
38
|
+
for (const m of methods) {
|
|
39
|
+
const bucket = groups.get(m.name);
|
|
40
|
+
if (bucket)
|
|
41
|
+
bucket.push(m);
|
|
42
|
+
else
|
|
43
|
+
groups.set(m.name, [m]);
|
|
44
|
+
}
|
|
45
|
+
for (const bucket of groups.values()) {
|
|
46
|
+
if (bucket.length < 2)
|
|
47
|
+
continue;
|
|
48
|
+
const used = new Set();
|
|
49
|
+
for (const method of bucket) {
|
|
50
|
+
const suffix = this.deriveSuffix(method.endpoint);
|
|
51
|
+
if (!suffix)
|
|
52
|
+
continue;
|
|
53
|
+
const finalSuffix = this.ensureUnique(suffix, used);
|
|
54
|
+
used.add(finalSuffix);
|
|
55
|
+
method.name = `${method.name}${finalSuffix}`;
|
|
56
|
+
method.nameSuffix = finalSuffix;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
static disambiguateListStates(methods) {
|
|
61
|
+
const listMethods = methods.filter((m) => m.analyzers.request.attributeType === "list");
|
|
62
|
+
if (listMethods.length < 2)
|
|
63
|
+
return;
|
|
64
|
+
const used = new Set();
|
|
65
|
+
for (const method of listMethods) {
|
|
66
|
+
// If `disambiguateNames` already assigned a suffix for this method
|
|
67
|
+
// (two list-typed ops with the same name), reuse it so the method
|
|
68
|
+
// name and list state stay aligned (e.g. `_listAllPackages` ↔
|
|
69
|
+
// `listPackages`).
|
|
70
|
+
let suffix = method.nameSuffix || this.deriveSuffix(method.endpoint);
|
|
71
|
+
if (!suffix)
|
|
72
|
+
continue;
|
|
73
|
+
const finalSuffix = this.ensureUnique(suffix, used);
|
|
74
|
+
used.add(finalSuffix);
|
|
75
|
+
method.stateSuffix = finalSuffix;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
static ensureUnique(suffix, used) {
|
|
79
|
+
if (!used.has(suffix))
|
|
80
|
+
return suffix;
|
|
81
|
+
let i = 2;
|
|
82
|
+
let candidate = `${suffix}${i}`;
|
|
83
|
+
while (used.has(candidate)) {
|
|
84
|
+
i += 1;
|
|
85
|
+
candidate = `${suffix}${i}`;
|
|
86
|
+
}
|
|
87
|
+
return candidate;
|
|
88
|
+
}
|
|
89
|
+
static deriveSuffix(rawPath) {
|
|
90
|
+
const segments = rawPath.split("/").filter(Boolean);
|
|
91
|
+
if (segments.length === 0)
|
|
92
|
+
return "";
|
|
93
|
+
const isParam = (s) => /^\{.+\}$/.test(s);
|
|
94
|
+
const last = segments[segments.length - 1];
|
|
95
|
+
let baseSegment = "";
|
|
96
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
97
|
+
const seg = segments[i];
|
|
98
|
+
if (!isParam(seg)) {
|
|
99
|
+
baseSegment = seg;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (!baseSegment)
|
|
104
|
+
return "";
|
|
105
|
+
const shouldPluralize = !isParam(last);
|
|
106
|
+
const word = shouldPluralize ? this.pluralize(baseSegment) : baseSegment;
|
|
107
|
+
return capitalizeFirstLetter(toCamelCase(word));
|
|
108
|
+
}
|
|
109
|
+
static pluralize(word) {
|
|
110
|
+
if (word.endsWith("s"))
|
|
111
|
+
return word;
|
|
112
|
+
if (/[^aeiou]y$/i.test(word))
|
|
113
|
+
return `${word.slice(0, -1)}ies`;
|
|
114
|
+
if (/(x|ch|sh)$/i.test(word))
|
|
115
|
+
return `${word}es`;
|
|
116
|
+
return `${word}s`;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -2,6 +2,7 @@ import * as path from "node:path";
|
|
|
2
2
|
import { Source } from "../../file.js";
|
|
3
3
|
import { capitalizeFirstLetter, toKebabCase } from "../../helpers/helpers.js";
|
|
4
4
|
import { Method } from "../method/Method.js";
|
|
5
|
+
import { MethodNameDisambiguator } from "../method/MethodNameDisambiguator.js";
|
|
5
6
|
import { generatedDir } from "../../vars.global.js";
|
|
6
7
|
import { ModuleImports, ModuleMethodProcessor, ModuleParamProcessor, ModuleClassBuilder, ModuleConstructorBuilder, ModuleFileBuilder, ApiFileBuilder, ApiClassBuilder, } from "../index.js";
|
|
7
8
|
export class Module {
|
|
@@ -42,6 +43,7 @@ export class Module {
|
|
|
42
43
|
this.fileBuilder = new ModuleFileBuilder({ imports: this.imports });
|
|
43
44
|
// Processa os métodos
|
|
44
45
|
this.methods = operations.map((operation) => Method.fromOperation(operation, this.name, context));
|
|
46
|
+
MethodNameDisambiguator.apply(this.methods);
|
|
45
47
|
const processedMethods = this.methodProcessor.process({ methods: this.methods });
|
|
46
48
|
// Processa os parâmetros
|
|
47
49
|
const processedParams = this.paramProcessor.process({
|
|
@@ -67,6 +67,9 @@ export class ModuleClassBuilder {
|
|
|
67
67
|
]);
|
|
68
68
|
}
|
|
69
69
|
`;
|
|
70
|
+
if (bundle.length > 0) {
|
|
71
|
+
this.imports.addReflectorImport("bundleStrict");
|
|
72
|
+
}
|
|
70
73
|
return `
|
|
71
74
|
class ${outputName} {
|
|
72
75
|
${attributes.join(";")}
|
|
@@ -75,9 +78,9 @@ export class ModuleClassBuilder {
|
|
|
75
78
|
|
|
76
79
|
${bundle.length > 0 ? `
|
|
77
80
|
bundle() {
|
|
78
|
-
return {
|
|
81
|
+
return bundleStrict({
|
|
79
82
|
${bundle.join(",")}
|
|
80
|
-
}
|
|
83
|
+
})
|
|
81
84
|
}
|
|
82
85
|
` : ""}
|
|
83
86
|
}
|
|
@@ -92,12 +95,15 @@ export class ModuleClassBuilder {
|
|
|
92
95
|
bundle.push(prop.bundleBuild());
|
|
93
96
|
});
|
|
94
97
|
}
|
|
98
|
+
if (bundle.length > 0) {
|
|
99
|
+
this.imports.addReflectorImport("bundleStrict");
|
|
100
|
+
}
|
|
95
101
|
const bundleBuild = bundle.length > 0
|
|
96
102
|
? `
|
|
97
103
|
bundle() {
|
|
98
|
-
return {
|
|
104
|
+
return bundleStrict({
|
|
99
105
|
${bundle.join(",")}
|
|
100
|
-
}
|
|
106
|
+
})
|
|
101
107
|
}
|
|
102
108
|
`
|
|
103
109
|
: "";
|
|
@@ -42,12 +42,15 @@ export class ModuleMethodProcessor {
|
|
|
42
42
|
methodsClear.add(`protected clear${capitalizeFirstLetter(entityName)}() { this.${entityName} = undefined }`);
|
|
43
43
|
}
|
|
44
44
|
else if (attributeType === "list") {
|
|
45
|
+
const listField = `list${method.stateSuffix}`;
|
|
46
|
+
const bundledField = `bundled${capitalizeFirstLetter(listField)}`;
|
|
47
|
+
const clearMethod = `clear${capitalizeFirstLetter(listField)}`;
|
|
45
48
|
methodsAttributes.add("totalPages = $state<number>(1)");
|
|
46
|
-
methodsAttributes.add(
|
|
49
|
+
methodsAttributes.add(`${listField} = $state<${responseType}['data']>([])`);
|
|
47
50
|
this.imports.addReflectorImport("genericArrayBundler");
|
|
48
|
-
methodsAttributes.add(
|
|
49
|
-
methodsInit.add(
|
|
50
|
-
methodsClear.add(`protected
|
|
51
|
+
methodsAttributes.add(`${bundledField} = $derived(genericArrayBundler(this.${listField}))`);
|
|
52
|
+
methodsInit.add(`this.${clearMethod}()`);
|
|
53
|
+
methodsClear.add(`protected ${clearMethod}() { this.${listField} = [] }`);
|
|
51
54
|
}
|
|
52
55
|
if (bodyType) {
|
|
53
56
|
entries.add(bodyType);
|
|
@@ -18,7 +18,7 @@ export class ModuleSchemaFileBuilder {
|
|
|
18
18
|
}
|
|
19
19
|
const treatedSchemas = schemas.map((s) => `${s.interface};\n${s.schema};`);
|
|
20
20
|
const imports = [
|
|
21
|
-
`import { build, BuildedInput } from "${config.reflectorAlias}/reflector.svelte";`,
|
|
21
|
+
`import { build, BuildedInput, bundleStrict } from "${config.reflectorAlias}/reflector.svelte";`,
|
|
22
22
|
`import { validateInputs } from "${config.validatorsImport}";`,
|
|
23
23
|
];
|
|
24
24
|
if (enumDeps.size > 0) {
|
|
@@ -208,3 +208,11 @@ export function changeArrayParam({ values, key }: { values: string[]; key: strin
|
|
|
208
208
|
values.forEach((value) => url.searchParams.append(key, value));
|
|
209
209
|
goto(url, { replaceState: true, keepFocus: true });
|
|
210
210
|
}
|
|
211
|
+
|
|
212
|
+
export function bundleStrict<T extends Record<string, unknown>>(
|
|
213
|
+
payload: T,
|
|
214
|
+
): { [K in keyof T]: Exclude<T[K], undefined> extends never ? never : T[K] } {
|
|
215
|
+
return Object.fromEntries(
|
|
216
|
+
Object.entries(payload).filter(([, v]) => v !== undefined),
|
|
217
|
+
) as typeof payload as { [K in keyof T]: Exclude<T[K], undefined> extends never ? never : T[K] };
|
|
218
|
+
}
|