hbs-magic 0.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.
Files changed (33) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +386 -0
  3. package/bin/cli.ts +48 -0
  4. package/dist/bin/cli.js +25 -0
  5. package/dist/src/cli-messages.js +84 -0
  6. package/dist/src/file-helpers.js +94 -0
  7. package/dist/src/formatting-helpers.js +63 -0
  8. package/dist/src/hbs-process-helpers.js +22 -0
  9. package/dist/src/process-helpers.js +50 -0
  10. package/examples/advanced_csharp-url-helpers/Result.gen.cs +455 -0
  11. package/examples/advanced_csharp-url-helpers/external-input/links.ts +182 -0
  12. package/examples/advanced_csharp-url-helpers/input/hbs-helpers.ts +93 -0
  13. package/examples/advanced_csharp-url-helpers/input/input-data.json +244 -0
  14. package/examples/advanced_csharp-url-helpers/input/preparation-script.ts +175 -0
  15. package/examples/advanced_csharp-url-helpers/input/template.hbs +44 -0
  16. package/examples/advanced_csharp-url-helpers/input/template_partial_node.hbs +32 -0
  17. package/examples/from-api_ts-api-client/Result.gen.ts +189 -0
  18. package/examples/from-api_ts-api-client/input/hbs-helpers.ts +53 -0
  19. package/examples/from-api_ts-api-client/input/template.hbs +30 -0
  20. package/examples/simple_assets-helper/Result.gen.ts +36 -0
  21. package/examples/simple_assets-helper/external-input/dummy_audio_1.mp3 +0 -0
  22. package/examples/simple_assets-helper/external-input/dummy_audio_2.mp3 +0 -0
  23. package/examples/simple_assets-helper/external-input/dummy_audio_3.mp3 +0 -0
  24. package/examples/simple_assets-helper/input/preparation-script.ts +45 -0
  25. package/examples/simple_assets-helper/input/template.hbs +31 -0
  26. package/package.json +52 -0
  27. package/src/cli-messages.ts +88 -0
  28. package/src/file-helpers.ts +108 -0
  29. package/src/formatting-helpers.ts +81 -0
  30. package/src/hbs-process-helpers.ts +36 -0
  31. package/src/process-helpers.ts +78 -0
  32. package/tsconfig.json +14 -0
  33. package/tsconfig.node.json +8 -0
@@ -0,0 +1,182 @@
1
+ import {
2
+ createRoute,
3
+ RequiredNumberParam,
4
+ RequiredStringParam,
5
+ NumberParam,
6
+ StringParam,
7
+ ArrayParam,
8
+ BooleanParam,
9
+ } from "react-router-url-params";
10
+
11
+ /**
12
+ * This file defines all the routes in some theoretical application using react-router-url-params.
13
+ * It is analyzed by preparation-script.ts and produces input-data.json to
14
+ * generate C# URL helper methods for ASP.NET Core.
15
+ */
16
+
17
+ export const Links = {
18
+ Unauthorized: {
19
+ Login: createRoute("/login", undefined, {
20
+ redirect: StringParam,
21
+ }),
22
+
23
+ Register: createRoute("/register", undefined, {
24
+ ref: StringParam,
25
+ }),
26
+
27
+ ConfirmEmail: createRoute("/confirm-email/:userId/:token", {
28
+ userId: RequiredStringParam,
29
+ token: RequiredStringParam,
30
+ }),
31
+
32
+ ForgotPassword: createRoute("/forgot-password"),
33
+ },
34
+
35
+ Authorized: {
36
+ Dashboard: createRoute("/", undefined, {
37
+ tab: StringParam,
38
+ }),
39
+
40
+ Profile: createRoute("/profile/:userId", {
41
+ userId: RequiredNumberParam,
42
+ section: StringParam,
43
+ }),
44
+
45
+ Settings: createRoute("/settings", undefined, {
46
+ page: StringParam,
47
+ }),
48
+
49
+ // ------------------------
50
+ // Courses
51
+ // ------------------------
52
+ Courses: createRoute("/courses", undefined, {
53
+ page: NumberParam,
54
+ pageSize: NumberParam,
55
+ search: StringParam,
56
+ tags: ArrayParam, // ?tags=a&tags=b
57
+ }),
58
+
59
+ CourseDetails: createRoute("/courses/:courseId", {
60
+ courseId: RequiredNumberParam,
61
+ tab: StringParam,
62
+ }),
63
+
64
+ CourseNotFound: createRoute("/courses/:courseId/not-found", {
65
+ courseId: RequiredNumberParam,
66
+ }),
67
+
68
+ CourseAccessDenied: createRoute("/courses/:courseId/access-denied", {
69
+ courseId: RequiredNumberParam,
70
+ }),
71
+
72
+ CourseStudents: createRoute("/courses/:courseId/students", {
73
+ courseId: RequiredNumberParam,
74
+ page: NumberParam,
75
+ roles: ArrayParam,
76
+ }),
77
+
78
+ // ------------------------
79
+ // Assignments
80
+ // ------------------------
81
+ AssignmentList: createRoute("/courses/:courseId/assignments", {
82
+ courseId: RequiredNumberParam,
83
+ status: StringParam,
84
+ dueBefore: StringParam,
85
+ }),
86
+
87
+ AssignmentDetails: createRoute(
88
+ "/courses/:courseId/assignments/:assignmentId",
89
+ {
90
+ courseId: RequiredNumberParam,
91
+ assignmentId: RequiredNumberParam,
92
+ tab: StringParam,
93
+ },
94
+ ),
95
+
96
+ AssignmentSubmissions: createRoute(
97
+ "/courses/:courseId/assignments/:assignmentId/submissions",
98
+ {
99
+ courseId: RequiredNumberParam,
100
+ assignmentId: RequiredNumberParam,
101
+ page: NumberParam,
102
+ userIds: ArrayParam,
103
+ },
104
+ ),
105
+
106
+ // ------------------------
107
+ // Announcements
108
+ // ------------------------
109
+ Announcements: createRoute("/courses/:courseId/announcements", {
110
+ courseId: RequiredNumberParam,
111
+ page: NumberParam,
112
+ }),
113
+
114
+ AnnouncementDetails: createRoute(
115
+ "/courses/:courseId/announcements/:announcementId",
116
+ {
117
+ courseId: RequiredNumberParam,
118
+ announcementId: RequiredNumberParam,
119
+ highlight: StringParam,
120
+ },
121
+ ),
122
+
123
+ // ------------------------
124
+ // Products
125
+ // ------------------------
126
+ Products: createRoute("/products", undefined, {
127
+ page: NumberParam,
128
+ pageSize: NumberParam,
129
+ sortBy: StringParam,
130
+ categories: ArrayParam,
131
+ minPrice: NumberParam,
132
+ maxPrice: NumberParam,
133
+ }),
134
+
135
+ ProductDetails: createRoute("/products/:id", {
136
+ id: RequiredNumberParam,
137
+ }),
138
+
139
+ CreateProduct: createRoute("/products/create", undefined, {
140
+ templateId: NumberParam,
141
+ }),
142
+
143
+ EditProduct: createRoute("/products/:id/edit", {
144
+ id: RequiredNumberParam,
145
+ mode: StringParam,
146
+ }),
147
+
148
+ // ------------------------
149
+ // Admin
150
+ // ------------------------
151
+ Admin: createRoute("/admin", undefined, {
152
+ tab: StringParam,
153
+ }),
154
+
155
+ AdminUsers: createRoute("/admin/users", undefined, {
156
+ page: NumberParam,
157
+ roles: ArrayParam,
158
+ search: StringParam,
159
+ }),
160
+
161
+ AdminUserDetails: createRoute("/admin/users/:userId", {
162
+ userId: RequiredNumberParam,
163
+ tab: StringParam,
164
+ }),
165
+
166
+ // ------------------------
167
+ // Misc
168
+ // ------------------------
169
+ Notifications: createRoute("/notifications", undefined, {
170
+ unreadOnly: BooleanParam,
171
+ }),
172
+
173
+ Search: createRoute("/search", undefined, {
174
+ types: ArrayParam,
175
+ page: NumberParam,
176
+ }),
177
+
178
+ UiKit: createRoute("/uikit", undefined, {
179
+ component: StringParam,
180
+ }),
181
+ },
182
+ };
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Converts TS types into their C# equivalents.
3
+ */
4
+ function csType(type: string): string {
5
+ switch (type) {
6
+ case "string":
7
+ return "string";
8
+ case "number":
9
+ return "int";
10
+ case "boolean":
11
+ return "bool";
12
+ case "array":
13
+ return "List<string>";
14
+ case "date":
15
+ return "DateTime";
16
+ default:
17
+ throw new Error(`Unknown type "${type}"`);
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Checks if the node is a group (i.e., has children but no route).
23
+ * Groups are used for organizational purposes and do not correspond to actual endpoints.
24
+ */
25
+ function isGroup(node: any) {
26
+ return !node.route;
27
+ }
28
+
29
+ /**
30
+ * Checks if the node is a route (i.e., has a route property).
31
+ */
32
+ function isRoute(node: any) {
33
+ return !!node.route;
34
+ }
35
+
36
+ /**
37
+ * Converts a field name into its C# property name equivalent by capitalizing the first letter.
38
+ */
39
+ function csFieldName(name: string) {
40
+ return name.charAt(0).toUpperCase() + name.slice(1);
41
+ }
42
+
43
+ /**
44
+ * Produces a C# expression (depending on type) that checks
45
+ * whether the value is null or not.
46
+ */
47
+ function csNullableCheck(fieldName: string, type: string) {
48
+ const csField = csFieldName(fieldName);
49
+
50
+ switch (type) {
51
+ case "string":
52
+ return `!string.IsNullOrEmpty(${csField})`;
53
+ case "number":
54
+ case "boolean":
55
+ case "date":
56
+ return `${csField} != null`;
57
+ case "array":
58
+ return `${csField} is { Count: > 0 }`;
59
+ default:
60
+ throw new Error(`Unknown type "${type}" for field "${fieldName}"`);
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Produces a C# expression that retrieves
66
+ * the value to then concatenate it as query parameter.
67
+ */
68
+ function csNullableValue(fieldName: string, type: string) {
69
+ const csField = csFieldName(fieldName);
70
+
71
+ switch (type) {
72
+ case "string":
73
+ return csField;
74
+ case "boolean":
75
+ case "number":
76
+ return `${csField}.Value`;
77
+ case "array":
78
+ return `string.Join(",", ${csField})`;
79
+ case "date":
80
+ return `${csField}.Value`;
81
+ default:
82
+ throw new Error(`Unknown type "${type}" for field "${fieldName}"`);
83
+ }
84
+ }
85
+
86
+ export default {
87
+ isGroup,
88
+ isRoute,
89
+ csType,
90
+ csFieldName,
91
+ csNullableCheck,
92
+ csNullableValue,
93
+ };
@@ -0,0 +1,244 @@
1
+ {
2
+ "Routes": {
3
+ "Unauthorized": {
4
+ "Login": {
5
+ "route": "/login",
6
+ "queryParams": {
7
+ "redirect": "string"
8
+ }
9
+ },
10
+ "Register": {
11
+ "route": "/register",
12
+ "queryParams": {
13
+ "ref": "string"
14
+ }
15
+ },
16
+ "ConfirmEmail": {
17
+ "route": "/confirm-email/:userId/:token",
18
+ "routeParams": {
19
+ "userId": "string",
20
+ "token": "string"
21
+ }
22
+ },
23
+ "ForgotPassword": {
24
+ "route": "/forgot-password"
25
+ }
26
+ },
27
+ "Authorized": {
28
+ "Dashboard": {
29
+ "route": "/",
30
+ "queryParams": {
31
+ "tab": "string"
32
+ }
33
+ },
34
+ "Profile": {
35
+ "route": "/profile/:userId",
36
+ "routeParams": {
37
+ "userId": "number",
38
+ "section": "string"
39
+ }
40
+ },
41
+ "Settings": {
42
+ "route": "/settings",
43
+ "queryParams": {
44
+ "page": "string"
45
+ }
46
+ },
47
+ "Courses": {
48
+ "route": "/courses",
49
+ "queryParams": {
50
+ "page": "number",
51
+ "pageSize": "number",
52
+ "search": "string",
53
+ "tags": "array"
54
+ }
55
+ },
56
+ "CourseDetails": {
57
+ "route": "/courses/:courseId",
58
+ "routeParams": {
59
+ "courseId": "number",
60
+ "tab": "string"
61
+ }
62
+ },
63
+ "CourseNotFound": {
64
+ "route": "/courses/:courseId/not-found",
65
+ "routeParams": {
66
+ "courseId": "number"
67
+ }
68
+ },
69
+ "CourseAccessDenied": {
70
+ "route": "/courses/:courseId/access-denied",
71
+ "routeParams": {
72
+ "courseId": "number"
73
+ }
74
+ },
75
+ "CourseStudents": {
76
+ "route": "/courses/:courseId/students",
77
+ "routeParams": {
78
+ "courseId": "number",
79
+ "page": "number",
80
+ "roles": "array"
81
+ }
82
+ },
83
+ "AssignmentList": {
84
+ "route": "/courses/:courseId/assignments",
85
+ "routeParams": {
86
+ "courseId": "number",
87
+ "status": "string",
88
+ "dueBefore": "string"
89
+ }
90
+ },
91
+ "AssignmentDetails": {
92
+ "route": "/courses/:courseId/assignments/:assignmentId",
93
+ "routeParams": {
94
+ "courseId": "number",
95
+ "assignmentId": "number",
96
+ "tab": "string"
97
+ }
98
+ },
99
+ "AssignmentSubmissions": {
100
+ "route": "/courses/:courseId/assignments/:assignmentId/submissions",
101
+ "routeParams": {
102
+ "courseId": "number",
103
+ "assignmentId": "number",
104
+ "page": "number",
105
+ "userIds": "array"
106
+ }
107
+ },
108
+ "Announcements": {
109
+ "route": "/courses/:courseId/announcements",
110
+ "routeParams": {
111
+ "courseId": "number",
112
+ "page": "number"
113
+ }
114
+ },
115
+ "AnnouncementDetails": {
116
+ "route": "/courses/:courseId/announcements/:announcementId",
117
+ "routeParams": {
118
+ "courseId": "number",
119
+ "announcementId": "number",
120
+ "highlight": "string"
121
+ }
122
+ },
123
+ "Products": {
124
+ "route": "/products",
125
+ "queryParams": {
126
+ "page": "number",
127
+ "pageSize": "number",
128
+ "sortBy": "string",
129
+ "categories": "array",
130
+ "minPrice": "number",
131
+ "maxPrice": "number"
132
+ }
133
+ },
134
+ "ProductDetails": {
135
+ "route": "/products/:id",
136
+ "routeParams": {
137
+ "id": "number"
138
+ }
139
+ },
140
+ "CreateProduct": {
141
+ "route": "/products/create",
142
+ "queryParams": {
143
+ "templateId": "number"
144
+ }
145
+ },
146
+ "EditProduct": {
147
+ "route": "/products/:id/edit",
148
+ "routeParams": {
149
+ "id": "number",
150
+ "mode": "string"
151
+ }
152
+ },
153
+ "Admin": {
154
+ "route": "/admin",
155
+ "queryParams": {
156
+ "tab": "string"
157
+ }
158
+ },
159
+ "AdminUsers": {
160
+ "route": "/admin/users",
161
+ "queryParams": {
162
+ "page": "number",
163
+ "roles": "array",
164
+ "search": "string"
165
+ }
166
+ },
167
+ "AdminUserDetails": {
168
+ "route": "/admin/users/:userId",
169
+ "routeParams": {
170
+ "userId": "number",
171
+ "tab": "string"
172
+ }
173
+ },
174
+ "Notifications": {
175
+ "route": "/notifications",
176
+ "queryParams": {
177
+ "unreadOnly": "boolean"
178
+ }
179
+ },
180
+ "Search": {
181
+ "route": "/search",
182
+ "queryParams": {
183
+ "types": "array",
184
+ "page": "number"
185
+ }
186
+ },
187
+ "UiKit": {
188
+ "route": "/uikit",
189
+ "queryParams": {
190
+ "component": "string"
191
+ }
192
+ }
193
+ }
194
+ },
195
+ "QueryParamCollections": {
196
+ "Login": {
197
+ "redirect": "string"
198
+ },
199
+ "Register": {
200
+ "ref": "string"
201
+ },
202
+ "Dashboard": {
203
+ "tab": "string"
204
+ },
205
+ "Settings": {
206
+ "page": "string"
207
+ },
208
+ "Courses": {
209
+ "page": "number",
210
+ "pageSize": "number",
211
+ "search": "string",
212
+ "tags": "array"
213
+ },
214
+ "Products": {
215
+ "page": "number",
216
+ "pageSize": "number",
217
+ "sortBy": "string",
218
+ "categories": "array",
219
+ "minPrice": "number",
220
+ "maxPrice": "number"
221
+ },
222
+ "CreateProduct": {
223
+ "templateId": "number"
224
+ },
225
+ "Admin": {
226
+ "tab": "string"
227
+ },
228
+ "AdminUsers": {
229
+ "page": "number",
230
+ "roles": "array",
231
+ "search": "string"
232
+ },
233
+ "Notifications": {
234
+ "unreadOnly": "boolean"
235
+ },
236
+ "Search": {
237
+ "types": "array",
238
+ "page": "number"
239
+ },
240
+ "UiKit": {
241
+ "component": "string"
242
+ }
243
+ }
244
+ }
@@ -0,0 +1,175 @@
1
+ import {
2
+ Project,
3
+ SyntaxKind,
4
+ ObjectLiteralExpression,
5
+ Node,
6
+ Symbol,
7
+ } from "ts-morph";
8
+ import ts from "typescript";
9
+ import fs from "fs";
10
+ import path from "path";
11
+ import { fileURLToPath } from "url";
12
+
13
+ const project = new Project({
14
+ tsConfigFilePath: "tsconfig.json",
15
+ });
16
+
17
+ const externalInputPath =
18
+ "examples/advanced_csharp-url-helpers/external-input/links.ts";
19
+ project.addSourceFileAtPath(externalInputPath);
20
+ const sourceFile = project.getSourceFileOrThrow(externalInputPath);
21
+
22
+ const paramTypeMap: Record<string, string> = {
23
+ StringParam: "string",
24
+ RequiredStringParam: "string",
25
+ NumberParam: "number",
26
+ RequiredNumberParam: "number",
27
+ BooleanParam: "boolean",
28
+ RequiredBooleanParam: "boolean",
29
+ DateParam: "date",
30
+ RequiredDateParam: "date",
31
+ ArrayParam: "array",
32
+ RequiredArrayParam: "array",
33
+ };
34
+
35
+ function isFlagPresent(symbol: Symbol, flag: ts.SymbolFlags): boolean {
36
+ return (symbol.getFlags() & flag) !== 0;
37
+ }
38
+
39
+ function resolveSpreadObject(node: Node): ObjectLiteralExpression | undefined {
40
+ let symbol = node.getSymbol();
41
+ if (!symbol) return;
42
+
43
+ if (isFlagPresent(symbol, ts.SymbolFlags.Alias)) {
44
+ symbol = symbol.getAliasedSymbol();
45
+ }
46
+
47
+ const declarations = symbol?.getDeclarations() ?? [];
48
+
49
+ for (const decl of declarations) {
50
+ if (Node.isVariableDeclaration(decl)) {
51
+ const init = decl.getInitializer();
52
+
53
+ if (Node.isObjectLiteralExpression(init)) {
54
+ return init;
55
+ }
56
+ }
57
+ }
58
+
59
+ return;
60
+ }
61
+
62
+ function convertParams(obj?: ObjectLiteralExpression) {
63
+ if (!obj) return undefined;
64
+
65
+ const result: Record<string, any> = {};
66
+
67
+ for (const prop of obj.getProperties()) {
68
+ if (Node.isPropertyAssignment(prop)) {
69
+ const key = prop.getName();
70
+ const valueText = prop.getInitializer()?.getText() ?? "";
71
+
72
+ result[key] = paramTypeMap[valueText] ?? valueText;
73
+ }
74
+
75
+ if (Node.isSpreadAssignment(prop)) {
76
+ const spreadExpr = prop.getExpression();
77
+
78
+ const spreadObj = resolveSpreadObject(spreadExpr);
79
+
80
+ if (spreadObj) {
81
+ const expanded = convertParams(spreadObj);
82
+ Object.assign(result, expanded);
83
+ }
84
+ }
85
+ }
86
+
87
+ return result;
88
+ }
89
+
90
+ function transformObject(obj: ObjectLiteralExpression): any {
91
+ const result: Record<string, any> = {};
92
+
93
+ for (const prop of obj.getProperties()) {
94
+ if (!Node.isPropertyAssignment(prop)) continue;
95
+
96
+ const key = prop.getName();
97
+ const initializer = prop.getInitializer();
98
+
99
+ if (!initializer) continue;
100
+
101
+ if (Node.isCallExpression(initializer)) {
102
+ if (initializer.getExpression().getText() === "createRoute") {
103
+ const args = initializer.getArguments();
104
+
105
+ const route = args[0]?.getText().replace(/['"]/g, "");
106
+
107
+ const routeParams = Node.isObjectLiteralExpression(args[1])
108
+ ? args[1]
109
+ : undefined;
110
+
111
+ const queryParams = Node.isObjectLiteralExpression(args[2])
112
+ ? args[2]
113
+ : undefined;
114
+
115
+ result[key] = {
116
+ route,
117
+ ...(routeParams ? { routeParams: convertParams(routeParams) } : {}),
118
+ ...(queryParams ? { queryParams: convertParams(queryParams) } : {}),
119
+ };
120
+
121
+ continue;
122
+ }
123
+ }
124
+
125
+ if (Node.isObjectLiteralExpression(initializer)) {
126
+ result[key] = transformObject(initializer);
127
+ }
128
+ }
129
+
130
+ return result;
131
+ }
132
+
133
+ function getQueryParamCollections(
134
+ routesObject: any,
135
+ ): Record<string, Record<string, string>> {
136
+ const collections: Record<string, Record<string, string>> = {};
137
+
138
+ function traverse(obj: any) {
139
+ for (const key in obj) {
140
+ if (obj[key] && typeof obj[key] === "object") {
141
+ if (obj[key].queryParams) {
142
+ collections[key] = obj[key].queryParams;
143
+ }
144
+ traverse(obj[key]);
145
+ }
146
+ }
147
+ }
148
+
149
+ traverse(routesObject);
150
+ return collections;
151
+ }
152
+
153
+ const __filename = fileURLToPath(import.meta.url);
154
+ const __dirname = path.dirname(__filename);
155
+
156
+ /**
157
+ * Example of preparation script that transforms a nested object with route definitions into a JSON `input-data.json`
158
+ * for Handlebars template input. It also extracts query parameter collections for easier access in the template.
159
+ */
160
+ export default function preparationScript() {
161
+ const rootObject = sourceFile.getFirstDescendantByKindOrThrow(
162
+ SyntaxKind.ObjectLiteralExpression,
163
+ );
164
+
165
+ const routesObject = transformObject(rootObject);
166
+ const resultJson = {
167
+ Routes: routesObject,
168
+ QueryParamCollections: getQueryParamCollections(routesObject),
169
+ };
170
+
171
+ fs.writeFileSync(
172
+ path.join(__dirname, "input-data.json"),
173
+ JSON.stringify(resultJson, null, 2),
174
+ );
175
+ }