proteum 1.0.0-1

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 (156) hide show
  1. package/.dockerignore +10 -0
  2. package/Rte.zip +0 -0
  3. package/cli/app/config.ts +54 -0
  4. package/cli/app/index.ts +195 -0
  5. package/cli/bin.js +11 -0
  6. package/cli/commands/build.ts +34 -0
  7. package/cli/commands/deploy/app.ts +29 -0
  8. package/cli/commands/deploy/web.ts +60 -0
  9. package/cli/commands/dev.ts +109 -0
  10. package/cli/commands/init.ts +85 -0
  11. package/cli/compiler/client/identite.ts +72 -0
  12. package/cli/compiler/client/index.ts +334 -0
  13. package/cli/compiler/common/babel/index.ts +170 -0
  14. package/cli/compiler/common/babel/plugins/index.ts +0 -0
  15. package/cli/compiler/common/babel/plugins/services.ts +579 -0
  16. package/cli/compiler/common/babel/routes/imports.ts +127 -0
  17. package/cli/compiler/common/babel/routes/routes.ts +1130 -0
  18. package/cli/compiler/common/files/autres.ts +39 -0
  19. package/cli/compiler/common/files/images.ts +35 -0
  20. package/cli/compiler/common/files/style.ts +78 -0
  21. package/cli/compiler/common/index.ts +154 -0
  22. package/cli/compiler/index.ts +532 -0
  23. package/cli/compiler/server/index.ts +211 -0
  24. package/cli/index.ts +189 -0
  25. package/cli/paths.ts +165 -0
  26. package/cli/print.ts +12 -0
  27. package/cli/tsconfig.json +38 -0
  28. package/cli/utils/index.ts +22 -0
  29. package/cli/utils/keyboard.ts +78 -0
  30. package/client/app/component.tsx +54 -0
  31. package/client/app/index.ts +142 -0
  32. package/client/app/service.ts +34 -0
  33. package/client/app.tsconfig.json +28 -0
  34. package/client/components/Button.tsx +298 -0
  35. package/client/components/Dialog/Manager.tsx +309 -0
  36. package/client/components/Dialog/card.tsx +208 -0
  37. package/client/components/Dialog/index.less +151 -0
  38. package/client/components/Dialog/status.less +176 -0
  39. package/client/components/Dialog/status.tsx +48 -0
  40. package/client/components/index.ts +2 -0
  41. package/client/components/types.d.ts +3 -0
  42. package/client/data/input.ts +32 -0
  43. package/client/global.d.ts +5 -0
  44. package/client/hooks.ts +22 -0
  45. package/client/index.ts +6 -0
  46. package/client/pages/_layout/index.less +6 -0
  47. package/client/pages/_layout/index.tsx +43 -0
  48. package/client/pages/bug.tsx.old +60 -0
  49. package/client/pages/useHeader.tsx +50 -0
  50. package/client/services/captcha/index.ts +67 -0
  51. package/client/services/router/components/Link.tsx +46 -0
  52. package/client/services/router/components/Page.tsx +55 -0
  53. package/client/services/router/components/router.tsx +218 -0
  54. package/client/services/router/index.tsx +521 -0
  55. package/client/services/router/request/api.ts +267 -0
  56. package/client/services/router/request/history.ts +5 -0
  57. package/client/services/router/request/index.ts +53 -0
  58. package/client/services/router/request/multipart.ts +147 -0
  59. package/client/services/router/response/index.tsx +128 -0
  60. package/client/services/router/response/page.ts +86 -0
  61. package/client/services/socket/index.ts +147 -0
  62. package/client/utils/dom.ts +77 -0
  63. package/common/app/index.ts +9 -0
  64. package/common/data/chaines/index.ts +54 -0
  65. package/common/data/dates.ts +179 -0
  66. package/common/data/markdown.ts +73 -0
  67. package/common/data/rte/nodes.ts +83 -0
  68. package/common/data/stats.ts +90 -0
  69. package/common/errors/index.tsx +326 -0
  70. package/common/router/index.ts +213 -0
  71. package/common/router/layouts.ts +93 -0
  72. package/common/router/register.ts +55 -0
  73. package/common/router/request/api.ts +77 -0
  74. package/common/router/request/index.ts +35 -0
  75. package/common/router/response/index.ts +45 -0
  76. package/common/router/response/page.ts +128 -0
  77. package/common/utils/rte.ts +183 -0
  78. package/common/utils.ts +7 -0
  79. package/doc/TODO.md +71 -0
  80. package/doc/front/router.md +27 -0
  81. package/doc/workspace/workspace.png +0 -0
  82. package/doc/workspace/workspace2.png +0 -0
  83. package/doc/workspace/workspace_26.01.22.png +0 -0
  84. package/package.json +171 -0
  85. package/server/app/commands.ts +141 -0
  86. package/server/app/container/config.ts +203 -0
  87. package/server/app/container/console/index.ts +550 -0
  88. package/server/app/container/index.ts +137 -0
  89. package/server/app/index.ts +273 -0
  90. package/server/app/service/container.ts +88 -0
  91. package/server/app/service/index.ts +235 -0
  92. package/server/app.tsconfig.json +28 -0
  93. package/server/context.ts +4 -0
  94. package/server/index.ts +4 -0
  95. package/server/services/auth/index.ts +250 -0
  96. package/server/services/auth/old.ts +277 -0
  97. package/server/services/auth/router/index.ts +95 -0
  98. package/server/services/auth/router/request.ts +54 -0
  99. package/server/services/auth/router/service.json +6 -0
  100. package/server/services/auth/service.json +6 -0
  101. package/server/services/cache/commands.ts +41 -0
  102. package/server/services/cache/index.ts +297 -0
  103. package/server/services/cache/service.json +6 -0
  104. package/server/services/cron/CronTask.ts +86 -0
  105. package/server/services/cron/index.ts +112 -0
  106. package/server/services/cron/service.json +6 -0
  107. package/server/services/disks/driver.ts +103 -0
  108. package/server/services/disks/drivers/local/index.ts +188 -0
  109. package/server/services/disks/drivers/local/service.json +6 -0
  110. package/server/services/disks/drivers/s3/index.ts +301 -0
  111. package/server/services/disks/drivers/s3/service.json +6 -0
  112. package/server/services/disks/index.ts +90 -0
  113. package/server/services/disks/service.json +6 -0
  114. package/server/services/email/index.ts +188 -0
  115. package/server/services/email/utils.ts +53 -0
  116. package/server/services/fetch/index.ts +201 -0
  117. package/server/services/fetch/service.json +7 -0
  118. package/server/services/models.7z +0 -0
  119. package/server/services/prisma/Facet.ts +142 -0
  120. package/server/services/prisma/index.ts +201 -0
  121. package/server/services/prisma/service.json +6 -0
  122. package/server/services/router/http/index.ts +217 -0
  123. package/server/services/router/http/multipart.ts +102 -0
  124. package/server/services/router/http/session.ts.old +40 -0
  125. package/server/services/router/index.ts +801 -0
  126. package/server/services/router/request/api.ts +87 -0
  127. package/server/services/router/request/index.ts +184 -0
  128. package/server/services/router/request/service.ts +21 -0
  129. package/server/services/router/request/validation/zod.ts +180 -0
  130. package/server/services/router/response/index.ts +338 -0
  131. package/server/services/router/response/mask/Filter.ts +323 -0
  132. package/server/services/router/response/mask/index.ts +60 -0
  133. package/server/services/router/response/mask/selecteurs.ts +92 -0
  134. package/server/services/router/response/page/document.tsx +160 -0
  135. package/server/services/router/response/page/index.tsx +196 -0
  136. package/server/services/router/service.json +6 -0
  137. package/server/services/router/service.ts +36 -0
  138. package/server/services/schema/index.ts +44 -0
  139. package/server/services/schema/request.ts +49 -0
  140. package/server/services/schema/router/index.ts +28 -0
  141. package/server/services/schema/router/service.json +6 -0
  142. package/server/services/schema/service.json +6 -0
  143. package/server/services/security/encrypt/aes/index.ts +85 -0
  144. package/server/services/security/encrypt/aes/service.json +6 -0
  145. package/server/services/socket/index.ts +162 -0
  146. package/server/services/socket/scope.ts +226 -0
  147. package/server/services/socket/service.json +6 -0
  148. package/server/services_old/SocketClient.ts +92 -0
  149. package/server/services_old/Token.old.ts +97 -0
  150. package/server/utils/slug.ts +79 -0
  151. package/tsconfig.common.json +45 -0
  152. package/tsconfig.json +3 -0
  153. package/types/aliases.d.ts +54 -0
  154. package/types/global/modules.d.ts +49 -0
  155. package/types/global/utils.d.ts +103 -0
  156. package/types/icons.d.ts +1 -0
@@ -0,0 +1,1130 @@
1
+ /*----------------------------------
2
+ - DEPENDANCES
3
+ ----------------------------------*/
4
+
5
+ // Npm
6
+ import * as types from '@babel/types'
7
+ import type { PluginObj, NodePath } from '@babel/core';
8
+
9
+ // Core
10
+ import cli from '@cli';
11
+ import { App, TAppSide } from '../../../../app';
12
+
13
+ /*----------------------------------
14
+ - WEBPACK RULE
15
+ ----------------------------------*/
16
+
17
+ type TOptions = {
18
+ side: TAppSide,
19
+ app: App,
20
+ debug?: boolean
21
+ }
22
+ type TRouteDefinition = {
23
+ definition: types.CallExpression,
24
+ dataFetchers: types.ObjectProperty[],
25
+ contextName?: string
26
+ }
27
+
28
+ type TFileInfos = {
29
+ path: string,
30
+ process: boolean,
31
+ side: 'front'|'back',
32
+
33
+ importedServices: {[local: string]: string},
34
+ routeDefinitions: TRouteDefinition[],
35
+ }
36
+
37
+ module.exports = (options: TOptions) => (
38
+ [Plugin, options]
39
+ )
40
+
41
+ const clientServices = ['Router'];
42
+ // Others will be called via app.<Service> (backend) or api.post(<path>, <params>) (frontend)
43
+
44
+ const routerMethods = ['get', 'post', 'put', 'delete', 'patch'];
45
+
46
+ /*----------------------------------
47
+ - PLUGIN
48
+ ----------------------------------*/
49
+ function Plugin(babel, { app, side, debug }: TOptions) {
50
+
51
+ //debug = true;
52
+
53
+ const t = babel.types as typeof types;
54
+
55
+ type TPluginState = {
56
+ filename: string,
57
+ file: TFileInfos,
58
+ apiInjectedRootFunctions: WeakSet<types.Node>,
59
+ needsUseContextImport: boolean
60
+ }
61
+
62
+ /*
63
+ - Wrap route.get(...) with (app: Application) => { }
64
+ - Inject chunk ID into client route options
65
+ - Transform api.fetch:
66
+
67
+ Input:
68
+ const { stats } = api.fetch({
69
+ stats: api.get(...)
70
+ }):
71
+
72
+ Output:
73
+
74
+ Route.page('/', { data: { stats: api.get(...) } });
75
+ ...
76
+ const stats = page.data.stats;
77
+ */
78
+
79
+ const plugin: PluginObj<TPluginState> = {
80
+ pre(state) {
81
+ this.filename = state.opts.filename as string;
82
+
83
+ this.file = getFileInfos(this.filename);
84
+
85
+ this.apiInjectedRootFunctions = new WeakSet();
86
+ this.needsUseContextImport = false;
87
+ },
88
+ visitor: {
89
+ // Find @app imports
90
+ // Test: import { Router } from '@app';
91
+ // Replace by: nothing
92
+ ImportDeclaration(path) {
93
+
94
+ const shouldTransformImports = this.file.process;
95
+ if (!shouldTransformImports)
96
+ return;
97
+
98
+ if (path.node.source.value !== '@app')
99
+ return;
100
+
101
+ for (const specifier of path.node.specifiers) {
102
+
103
+ if (specifier.type !== 'ImportSpecifier')
104
+ continue;
105
+
106
+ if (specifier.imported.type !== 'Identifier')
107
+ continue;
108
+
109
+ const serviceName = specifier.imported.name;
110
+
111
+ if (clientServices.includes(serviceName))
112
+ this.file.importedServices[ specifier.local.name ] = serviceName;
113
+ else
114
+ this.file.importedServices[ specifier.local.name ] = 'app';
115
+ }
116
+
117
+ // Remove this import
118
+ path.remove();
119
+
120
+ },
121
+
122
+ // Transform services service calls
123
+ CallExpression(path) {
124
+
125
+ if (!this.file.process)
126
+ return;
127
+
128
+ // object.property()
129
+ const callee = path.node.callee
130
+ if (!(
131
+ callee.type === 'MemberExpression'
132
+ ))
133
+ return;
134
+
135
+ // Create full path
136
+ const completePath: string[] = [];
137
+ let currCallee: types.MemberExpression = callee;
138
+ while (1) {
139
+
140
+ if (currCallee.property.type === 'Identifier')
141
+ completePath.unshift(currCallee.property.name);
142
+
143
+ if (currCallee.object.type === 'MemberExpression')
144
+ currCallee = currCallee.object;
145
+ else {
146
+
147
+ if (currCallee.object.type === 'Identifier')
148
+ completePath.unshift(currCallee.object.name);
149
+
150
+ break;
151
+ }
152
+ }
153
+
154
+ // If we actually call a service
155
+ const serviceName = completePath[0];
156
+
157
+ /*
158
+ Router.page: wrap with export const __register = ({ Router }) => Router.page(...)
159
+ */
160
+ if (
161
+ serviceName === 'Router'
162
+ &&
163
+ callee.property.type === 'Identifier'
164
+ &&
165
+ ['page', 'error', ...routerMethods].includes(callee.property.name)
166
+ ) {
167
+
168
+ // Should be at the root of the document
169
+ if (!(
170
+ path.parent.type === 'ExpressionStatement'
171
+ &&
172
+ path.parentPath.parent.type === 'Program'
173
+ ))
174
+ return;
175
+
176
+ const routeDef: TRouteDefinition = {
177
+ definition: path.node,
178
+ dataFetchers: []
179
+ }
180
+
181
+ // Adjust
182
+ // /client/pages/*
183
+ if (this.file.side === 'front') {
184
+ transformDataFetchers(path, this, routeDef);
185
+ }
186
+
187
+ // Add to the list of route definitons to wrap
188
+ this.file.routeDefinitions.push(routeDef);
189
+
190
+ // Delete the route def since it will be replaced by a wrapper
191
+ path.replaceWithMultiple([]);
192
+
193
+
194
+ } else if (this.file.side === 'front') {
195
+
196
+ const isAService = (
197
+ serviceName in this.file.importedServices
198
+ &&
199
+ serviceName[0] === serviceName[0].toUpperCase()
200
+ );
201
+ if(!isAService)
202
+ return;
203
+
204
+ /* [client] Backend Service calls: Transform to api.post( <method path>, <params> )
205
+
206
+ Events.Create( form.data ).then(res => toast.success(res.message))
207
+ =>
208
+ api.post( '/api/events/create', form.data ).then(res => toast.success(res.message)).catch(app.handleError)
209
+ */
210
+ if (side === 'client' && !clientServices.includes(serviceName)) {
211
+
212
+ ensureApiExposedInRootFunction(path, this);
213
+
214
+ // Get complete call path
215
+ const apiPath = '/api/' + completePath.join('/');
216
+
217
+ // Replace by api.post( <method path>, <params> )
218
+ const apiPostArgs: types.CallExpression["arguments"] = [t.stringLiteral(apiPath)];
219
+ if (path.node.arguments.length >= 1)
220
+ apiPostArgs.push( path.node.arguments[0] );
221
+
222
+ path.replaceWith(
223
+ t.callExpression(
224
+ t.memberExpression(
225
+ t.identifier('api'), t.identifier('post')
226
+ ), apiPostArgs
227
+ )
228
+ )
229
+
230
+ ensureBackendServicePromiseCatch(path);
231
+
232
+ /* [server] Backend Service calls
233
+
234
+ Events.Create( form.data ).then(res => toast.success(res.message))
235
+ =>
236
+ app.Events.Create( form.data, context ).then(res => toast.success(res.message))
237
+ */
238
+ } else {
239
+
240
+ // Rebuild member expression from completePath, adding a api prefix
241
+ let newCallee = t.memberExpression(
242
+ t.identifier('app'),
243
+ t.identifier(completePath[0])
244
+ );
245
+ for (let i = 1; i < completePath.length; i++) {
246
+ newCallee = t.memberExpression(
247
+ newCallee,
248
+ t.identifier(completePath[i])
249
+ );
250
+ }
251
+
252
+ // Replace by app.<service>.<method>(...)
253
+ path.replaceWith(
254
+ t.callExpression(
255
+ newCallee,
256
+ [...path.node.arguments]
257
+ )
258
+ )
259
+ }
260
+ }
261
+
262
+ },
263
+ Program: {
264
+ exit(path, parent) {
265
+
266
+ if (!this.file.process)
267
+ return;
268
+
269
+ ensureUseContextImport(path, this);
270
+
271
+ const wrappedrouteDefs = wrapRouteDefs( this.file );
272
+ if (wrappedrouteDefs)
273
+ path.pushContainer('body', [wrappedrouteDefs])
274
+ }
275
+ }
276
+ }
277
+ }
278
+
279
+ function ensureBackendServicePromiseCatch(path: NodePath<types.CallExpression>) {
280
+
281
+ const chainRoot = getPromiseChainRoot(path);
282
+
283
+ // Only append if we are in a promise chain (e.g. .then(...))
284
+ if (chainRoot === path)
285
+ return;
286
+
287
+ if (isCatchWithAppHandleErrorArrow(chainRoot))
288
+ return;
289
+
290
+ if (isCatchWithAppHandleErrorMember(chainRoot)) {
291
+ const updatedCatch = t.callExpression(
292
+ chainRoot.node.callee,
293
+ [buildCatchHandler()]
294
+ );
295
+ chainRoot.replaceWith(updatedCatch);
296
+ return;
297
+ }
298
+
299
+ if (isCatchWithAppHandleErrorWrapped(chainRoot)) {
300
+ const updatedCatch = t.callExpression(
301
+ chainRoot.node.callee,
302
+ [buildCatchHandler()]
303
+ );
304
+ chainRoot.replaceWith(updatedCatch);
305
+ return;
306
+ }
307
+
308
+ const newChain = t.callExpression(
309
+ t.memberExpression(chainRoot.node, t.identifier('catch')),
310
+ [buildCatchHandler()]
311
+ );
312
+
313
+ chainRoot.replaceWith(newChain);
314
+ }
315
+
316
+ function buildCatchHandler(): types.ArrowFunctionExpression {
317
+ const errorIdentifier = t.identifier('e');
318
+ return t.arrowFunctionExpression(
319
+ [errorIdentifier],
320
+ t.callExpression(
321
+ t.memberExpression(t.identifier('app'), t.identifier('handleError')),
322
+ [t.identifier(errorIdentifier.name)]
323
+ )
324
+ );
325
+ }
326
+
327
+ function getPromiseChainRoot(path: NodePath<types.CallExpression>): NodePath<types.CallExpression> {
328
+
329
+ let current = path;
330
+
331
+ while (1) {
332
+
333
+ const member = current.parentPath;
334
+ if (!member?.isMemberExpression())
335
+ break;
336
+
337
+ if (member.node.object !== current.node)
338
+ break;
339
+
340
+ if (member.node.property.type !== 'Identifier')
341
+ break;
342
+
343
+ const propName = member.node.property.name;
344
+ if (!['then', 'catch', 'finally'].includes(propName))
345
+ break;
346
+
347
+ const call = member.parentPath;
348
+ if (!call?.isCallExpression())
349
+ break;
350
+
351
+ if (call.node.callee !== member.node)
352
+ break;
353
+
354
+ current = call;
355
+ }
356
+
357
+ return current;
358
+ }
359
+
360
+ function isCatchCall(path: NodePath<types.CallExpression>): boolean {
361
+
362
+ const callee = path.node.callee;
363
+ if (callee.type !== 'MemberExpression')
364
+ return false;
365
+
366
+ if (callee.property.type !== 'Identifier' || callee.property.name !== 'catch')
367
+ return false;
368
+
369
+ return true;
370
+ }
371
+
372
+ function isCatchWithAppHandleErrorMember(path: NodePath<types.CallExpression>): boolean {
373
+
374
+ if (!isCatchCall(path))
375
+ return false;
376
+
377
+ if (path.node.arguments.length !== 1)
378
+ return false;
379
+
380
+ const handler = path.node.arguments[0];
381
+ if (handler.type !== 'MemberExpression')
382
+ return false;
383
+
384
+ if (handler.object.type !== 'Identifier' || handler.object.name !== 'app')
385
+ return false;
386
+
387
+ if (handler.property.type !== 'Identifier' || handler.property.name !== 'handleError')
388
+ return false;
389
+
390
+ return true;
391
+ }
392
+
393
+ function isCatchWithAppHandleErrorArrow(path: NodePath<types.CallExpression>): boolean {
394
+
395
+ if (!isCatchCall(path))
396
+ return false;
397
+
398
+ if (path.node.arguments.length !== 1)
399
+ return false;
400
+
401
+ const handler = path.node.arguments[0];
402
+ if (handler.type !== 'ArrowFunctionExpression')
403
+ return false;
404
+
405
+ if (
406
+ handler.params.length !== 1
407
+ ||
408
+ handler.params[0].type !== 'Identifier'
409
+ )
410
+ return false;
411
+
412
+ const errorName = handler.params[0].name;
413
+
414
+ if (handler.body.type !== 'CallExpression')
415
+ return false;
416
+
417
+ const call = handler.body;
418
+ if (call.callee.type !== 'MemberExpression')
419
+ return false;
420
+
421
+ if (call.callee.object.type !== 'Identifier' || call.callee.object.name !== 'app')
422
+ return false;
423
+
424
+ if (call.callee.property.type !== 'Identifier' || call.callee.property.name !== 'handleError')
425
+ return false;
426
+
427
+ if (call.arguments.length !== 1)
428
+ return false;
429
+
430
+ const arg = call.arguments[0];
431
+ if (arg.type !== 'Identifier' || arg.name !== errorName)
432
+ return false;
433
+
434
+ return true;
435
+ }
436
+
437
+ function isCatchWithAppHandleErrorWrapped(path: NodePath<types.CallExpression>): boolean {
438
+
439
+ if (!isCatchCall(path))
440
+ return false;
441
+
442
+ if (path.node.arguments.length !== 1)
443
+ return false;
444
+
445
+ const handler = path.node.arguments[0];
446
+ if (handler.type !== 'ArrowFunctionExpression')
447
+ return false;
448
+
449
+ if (
450
+ handler.params.length !== 1
451
+ ||
452
+ handler.params[0].type !== 'Identifier'
453
+ )
454
+ return false;
455
+
456
+ const errorName = handler.params[0].name;
457
+
458
+ if (handler.body.type !== 'BlockStatement')
459
+ return false;
460
+
461
+ const statements = handler.body.body;
462
+ if (statements.length < 2)
463
+ return false;
464
+
465
+ const first = statements[0];
466
+ if (!(
467
+ first.type === 'ExpressionStatement'
468
+ &&
469
+ first.expression.type === 'CallExpression'
470
+ &&
471
+ first.expression.callee.type === 'MemberExpression'
472
+ &&
473
+ first.expression.callee.object.type === 'Identifier'
474
+ &&
475
+ first.expression.callee.object.name === 'console'
476
+ &&
477
+ first.expression.callee.property.type === 'Identifier'
478
+ &&
479
+ first.expression.callee.property.name === 'log'
480
+ &&
481
+ first.expression.arguments.length === 2
482
+ &&
483
+ first.expression.arguments[0].type === 'StringLiteral'
484
+ &&
485
+ first.expression.arguments[0].value === 'Error catched'
486
+ &&
487
+ first.expression.arguments[1].type === 'Identifier'
488
+ &&
489
+ first.expression.arguments[1].name === errorName
490
+ ))
491
+ return false;
492
+
493
+ const second = statements[1];
494
+ if (!(
495
+ second.type === 'ExpressionStatement'
496
+ &&
497
+ second.expression.type === 'CallExpression'
498
+ &&
499
+ second.expression.callee.type === 'MemberExpression'
500
+ &&
501
+ second.expression.callee.object.type === 'Identifier'
502
+ &&
503
+ second.expression.callee.object.name === 'app'
504
+ &&
505
+ second.expression.callee.property.type === 'Identifier'
506
+ &&
507
+ second.expression.callee.property.name === 'handleError'
508
+ &&
509
+ second.expression.arguments.length === 1
510
+ &&
511
+ second.expression.arguments[0].type === 'Identifier'
512
+ &&
513
+ second.expression.arguments[0].name === errorName
514
+ ))
515
+ return false;
516
+
517
+ return true;
518
+ }
519
+
520
+ function ensureApiExposedInRootFunction(
521
+ path: NodePath<types.CallExpression>,
522
+ pluginState: TPluginState
523
+ ) {
524
+ const needsApi = !path.scope.hasBinding('api');
525
+ const needsApp = !path.scope.hasBinding('app');
526
+ if (!needsApi && !needsApp)
527
+ return;
528
+
529
+ const rootFunctionPath = getRootFunctionPath(path);
530
+ if (!rootFunctionPath)
531
+ return;
532
+
533
+ // Root function should be at the program body level (not nested in another function / expression)
534
+ if (rootFunctionPath.getFunctionParent())
535
+ return;
536
+ if (!isProgramBodyLevelFunction(rootFunctionPath))
537
+ return;
538
+
539
+ const existingContextObjectPattern = findUseContextDestructuringObjectPattern(rootFunctionPath);
540
+ if (existingContextObjectPattern) {
541
+
542
+ const existingKeys = new Set(
543
+ existingContextObjectPattern.properties
544
+ .filter((p): p is types.ObjectProperty =>
545
+ p.type === 'ObjectProperty'
546
+ && p.key.type === 'Identifier'
547
+ )
548
+ .map(p => (p.key as types.Identifier).name)
549
+ );
550
+
551
+ if (needsApi && !existingKeys.has('api')) {
552
+ existingContextObjectPattern.properties.push(
553
+ t.objectProperty(t.identifier('api'), t.identifier('api'), false, true),
554
+ );
555
+ }
556
+
557
+ if (needsApp && !existingKeys.has('app')) {
558
+ existingContextObjectPattern.properties.push(
559
+ t.objectProperty(t.identifier('app'), t.identifier('app'), false, true),
560
+ );
561
+ }
562
+
563
+ pluginState.needsUseContextImport = true;
564
+ return;
565
+ }
566
+
567
+ if (pluginState.apiInjectedRootFunctions.has(rootFunctionPath.node))
568
+ return;
569
+
570
+ const exposeApiDeclaration = t.variableDeclaration('const', [
571
+ t.variableDeclarator(
572
+ t.objectPattern([
573
+ ...(needsApi ? [t.objectProperty(t.identifier('api'), t.identifier('api'), false, true)] : []),
574
+ ...(needsApp ? [t.objectProperty(t.identifier('app'), t.identifier('app'), false, true)] : []),
575
+ ]),
576
+ t.callExpression(t.identifier('useContext'), [])
577
+ )
578
+ ]);
579
+
580
+ const body = rootFunctionPath.node.body;
581
+ if (body.type === 'BlockStatement') {
582
+ body.body.unshift(exposeApiDeclaration);
583
+ } else {
584
+ rootFunctionPath.node.body = t.blockStatement([
585
+ exposeApiDeclaration,
586
+ t.returnStatement(body)
587
+ ]);
588
+ }
589
+
590
+ pluginState.apiInjectedRootFunctions.add(rootFunctionPath.node);
591
+ pluginState.needsUseContextImport = true;
592
+ }
593
+
594
+ function findUseContextDestructuringObjectPattern(
595
+ rootFunctionPath: NodePath<types.Function | types.ArrowFunctionExpression>
596
+ ): types.ObjectPattern | undefined {
597
+
598
+ const body = rootFunctionPath.node.body;
599
+ if (body.type !== 'BlockStatement')
600
+ return;
601
+
602
+ for (const stmt of body.body) {
603
+ if (stmt.type !== 'VariableDeclaration' || stmt.kind !== 'const')
604
+ continue;
605
+
606
+ for (const declarator of stmt.declarations) {
607
+ if (declarator.id.type !== 'ObjectPattern')
608
+ continue;
609
+ if (!declarator.init || declarator.init.type !== 'CallExpression')
610
+ continue;
611
+ if (declarator.init.callee.type !== 'Identifier' || declarator.init.callee.name !== 'useContext')
612
+ continue;
613
+ if (declarator.init.arguments.length !== 0)
614
+ continue;
615
+ return declarator.id;
616
+ }
617
+ }
618
+ }
619
+
620
+ function getRootFunctionPath(path: NodePath): NodePath<types.Function | types.ArrowFunctionExpression> | undefined {
621
+
622
+ let functionPath = path.getFunctionParent();
623
+ if (!functionPath)
624
+ return;
625
+
626
+ // Only support plain functions / arrow functions (no class/object methods)
627
+ if (!(
628
+ functionPath.isFunctionDeclaration()
629
+ || functionPath.isFunctionExpression()
630
+ || functionPath.isArrowFunctionExpression()
631
+ ))
632
+ return;
633
+
634
+ let parentFunction = functionPath.getFunctionParent();
635
+ while (parentFunction) {
636
+
637
+ if (!(
638
+ parentFunction.isFunctionDeclaration()
639
+ || parentFunction.isFunctionExpression()
640
+ || parentFunction.isArrowFunctionExpression()
641
+ ))
642
+ break;
643
+
644
+ functionPath = parentFunction;
645
+ parentFunction = functionPath.getFunctionParent();
646
+ }
647
+
648
+ return functionPath;
649
+ }
650
+
651
+ function isProgramBodyLevelFunction(path: NodePath): boolean {
652
+
653
+ const parent = path.parentPath;
654
+ if (!parent)
655
+ return false;
656
+
657
+ // function Foo() {}
658
+ if (parent.isProgram())
659
+ return true;
660
+
661
+ // export default function Foo() {} / export default () => {}
662
+ if (
663
+ parent.isExportDefaultDeclaration()
664
+ &&
665
+ parent.parentPath?.isProgram()
666
+ )
667
+ return true;
668
+
669
+ // export const Foo = () => {}
670
+ if (
671
+ parent.isExportNamedDeclaration()
672
+ &&
673
+ parent.parentPath?.isProgram()
674
+ )
675
+ return true;
676
+
677
+ // const Foo = () => {} (top-level) / export const Foo = () => {}
678
+ if (parent.isVariableDeclarator()) {
679
+
680
+ const declaration = parent.parentPath;
681
+ if (!declaration?.isVariableDeclaration())
682
+ return false;
683
+
684
+ const declarationParent = declaration.parentPath;
685
+ if (!declarationParent)
686
+ return false;
687
+
688
+ if (declarationParent.isProgram())
689
+ return true;
690
+
691
+ if (
692
+ declarationParent.isExportNamedDeclaration()
693
+ &&
694
+ declarationParent.parentPath?.isProgram()
695
+ )
696
+ return true;
697
+ }
698
+
699
+ return false;
700
+ }
701
+
702
+ function ensureUseContextImport(path: NodePath<types.Program>, pluginState: TPluginState) {
703
+
704
+ if (!pluginState.needsUseContextImport)
705
+ return;
706
+
707
+ const body = path.node.body;
708
+
709
+ // Already imported as a value import
710
+ for (const stmt of body) {
711
+ if (
712
+ stmt.type === 'ImportDeclaration'
713
+ &&
714
+ stmt.source.value === '@/client/context'
715
+ &&
716
+ stmt.importKind !== 'type'
717
+ &&
718
+ stmt.specifiers.some(s =>
719
+ s.type === 'ImportDefaultSpecifier' && s.local.name === 'useContext'
720
+ )
721
+ )
722
+ return;
723
+ }
724
+
725
+ // Try to reuse an existing value import from the same module
726
+ for (const stmt of body) {
727
+ if (
728
+ stmt.type !== 'ImportDeclaration'
729
+ ||
730
+ stmt.source.value !== '@/client/context'
731
+ ||
732
+ stmt.importKind === 'type'
733
+ )
734
+ continue;
735
+
736
+ const hasDefaultImport = stmt.specifiers.some(s => s.type === 'ImportDefaultSpecifier');
737
+ if (!hasDefaultImport) {
738
+ stmt.specifiers.unshift(
739
+ t.importDefaultSpecifier(t.identifier('useContext'))
740
+ );
741
+ return;
742
+ }
743
+ }
744
+
745
+ // Otherwise, add a new import (placed after existing imports)
746
+ const importDeclaration = t.importDeclaration(
747
+ [t.importDefaultSpecifier(t.identifier('useContext'))],
748
+ t.stringLiteral('@/client/context')
749
+ );
750
+
751
+ let insertIndex = 0;
752
+ while (insertIndex < body.length && body[insertIndex].type === 'ImportDeclaration')
753
+ insertIndex++;
754
+
755
+ body.splice(insertIndex, 0, importDeclaration);
756
+ }
757
+
758
+ function getFileInfos( filename: string ): TFileInfos {
759
+
760
+ const file: TFileInfos = {
761
+ process: true,
762
+ side: 'back',
763
+ path: filename,
764
+ importedServices: {},
765
+ routeDefinitions: []
766
+ }
767
+
768
+ // Relative path
769
+ let relativeFileName: string | undefined;
770
+ if (filename.startsWith( cli.paths.appRoot ))
771
+ relativeFileName = filename.substring( cli.paths.appRoot.length );
772
+ if (filename.startsWith( cli.paths.coreRoot ))
773
+ relativeFileName = filename.substring( cli.paths.coreRoot.length );
774
+
775
+ // The file isn't a route definition
776
+ if (relativeFileName === undefined) {
777
+ file.process = false;
778
+ return file;
779
+ }
780
+
781
+ // Differenciate back / front
782
+ if (relativeFileName.startsWith('/client/pages') || relativeFileName.startsWith('/client/components') || relativeFileName.startsWith('/client/hooks')) {
783
+ file.side = 'front';
784
+ } else if (relativeFileName.startsWith('/server/routes')) {
785
+ file.side = 'back';
786
+ } else
787
+ file.process = false;
788
+
789
+ return file
790
+ }
791
+
792
+ function transformDataFetchers(
793
+ path: NodePath<types.CallExpression>,
794
+ routerDefContext: TPluginState,
795
+ routeDef: TRouteDefinition
796
+ ) {
797
+ path.traverse({
798
+ // api.load => move data fetchers to route.options.data
799
+ // So the router is able to load data before rendering the component
800
+ CallExpression(path) {
801
+
802
+ const callee = path.node.callee
803
+
804
+ // Detect api.fetch
805
+ if (!(
806
+ callee.type === 'MemberExpression'
807
+ &&
808
+ callee.object.type === 'Identifier'
809
+ &&
810
+ callee.property.type === 'Identifier'
811
+ &&
812
+ callee.object.name === 'api'
813
+ &&
814
+ callee.property.name === 'fetch'
815
+ ))
816
+ return;
817
+
818
+ /* Reference data fetchers
819
+ {
820
+ stats: api.get(...)
821
+ }
822
+ */
823
+ routeDef.dataFetchers.push(
824
+ ...path.node.arguments[0].properties.map(p => {
825
+
826
+ // Server side: Pass request context as 2nd argument
827
+ // companies: Companies.create( <params>, context )
828
+ if (
829
+ side === 'server'
830
+ &&
831
+ p.type === 'ObjectProperty'
832
+ &&
833
+ p.key.type === 'Identifier'
834
+ &&
835
+ p.value.type === 'CallExpression'
836
+ &&
837
+ // TODO: reliable way to know if it's a service
838
+ !(
839
+ p.value.callee.type === 'MemberExpression'
840
+ &&
841
+ p.value.callee.object.type === 'Identifier'
842
+ &&
843
+ p.value.callee.object.name === 'api'
844
+ )
845
+ ) {
846
+
847
+ // Pass request context as 2nd argument
848
+ p.value.arguments = p.value.arguments.length === 0
849
+ ? [ t.objectExpression([]), t.identifier('context') ]
850
+ : [ p.value.arguments[0], t.identifier('context') ];
851
+
852
+ }
853
+
854
+ return p;
855
+
856
+ })
857
+ );
858
+
859
+ /* Replace the:
860
+ const { stats } = api.fetch({
861
+ stats: api.get(...)
862
+ })
863
+
864
+ by:
865
+ const { stats } = context.data.stats;
866
+ */
867
+ path.replaceWith(
868
+ t.memberExpression(
869
+ t.identifier( routeDef.contextName || 'context' ),
870
+ t.identifier('data'),
871
+ )
872
+ );
873
+ }
874
+ }, routerDefContext);
875
+ }
876
+
877
+ function enrichRouteOptions(
878
+ routeDef: TRouteDefinition,
879
+ routeArgs: types.CallExpression["arguments"],
880
+ filename: string
881
+ ): types.CallExpression["arguments"] | 'ALREADY_PROCESSED' {
882
+
883
+ // Extract client route definition arguments
884
+ let routeOptions: types.ObjectExpression | undefined;
885
+ let renderer: types.ArrowFunctionExpression;
886
+ if (routeArgs.length === 1)
887
+ ([ renderer ] = routeArgs);
888
+ else
889
+ ([ routeOptions, renderer ] = routeArgs);
890
+
891
+ // Generate page chunk id
892
+ const { filepath, chunkId } = cli.paths.getPageChunk(app, filename);
893
+ debug && console.log(`[routes]`, filename.replace(cli.paths.appRoot + '/client/pages', ''));
894
+
895
+ // Create new options to add in route.options
896
+ const newProperties = [
897
+ t.objectProperty(
898
+ t.identifier('id'),
899
+ t.stringLiteral(chunkId)
900
+ ),
901
+ t.objectProperty(
902
+ t.identifier('filepath'),
903
+ t.stringLiteral(filepath)
904
+ ),
905
+ ]
906
+
907
+ // Add data fetchers
908
+ if (routeDef.dataFetchers.length !== 0) {
909
+
910
+ const rendererContext = t.cloneNode( renderer.params[0] );
911
+ // If not already present, add context to the 1st argument (a object spread)
912
+ if (!rendererContext.properties.some( p => p.key.name === 'context' ))
913
+ rendererContext.properties.push(
914
+ t.objectProperty(
915
+ t.identifier('context'),
916
+ t.identifier('context')
917
+ )
918
+ )
919
+
920
+ // (contollerParams) => { stats: api.get(...) }
921
+ const dataFetchersFunc = t.arrowFunctionExpression(
922
+ [rendererContext],
923
+ t.objectExpression(
924
+ routeDef.dataFetchers.map( df => t.cloneNode( df ))
925
+ )
926
+ )
927
+
928
+ // Add the data fetchers to options.data
929
+ newProperties.push(
930
+ t.objectProperty(
931
+ t.identifier('data'),
932
+ dataFetchersFunc
933
+ )
934
+ );
935
+
936
+ // Expose the context variable in the renderer
937
+ exposeContextProperty( renderer, routeDef );
938
+ }
939
+
940
+ if (routeOptions?.properties === undefined)
941
+ return [
942
+ t.objectExpression(newProperties),
943
+ renderer
944
+ ]
945
+
946
+ // Test if the route options were not already processed
947
+ const wasAlreadyProcessed = routeOptions.properties.some( o =>
948
+ o.type === 'ObjectProperty'
949
+ &&
950
+ o.key.type === 'Identifier'
951
+ &&
952
+ o.key.name === 'id'
953
+ )
954
+
955
+ if (wasAlreadyProcessed) {
956
+ // Cancel processing
957
+ debug && console.log(`[routes]`, filename, 'Already Processed');
958
+ return 'ALREADY_PROCESSED';
959
+ }
960
+
961
+ // Create the new options object
962
+ return [
963
+ t.objectExpression([
964
+ ...routeOptions.properties,
965
+ ...newProperties
966
+ ]),
967
+ renderer
968
+ ]
969
+ }
970
+
971
+ function exposeContextProperty(
972
+ renderer: types.ArrowFunctionExpression,
973
+ routeDef: TRouteDefinition
974
+ ) {
975
+ const contextParam = renderer.params[0];
976
+ if (contextParam?.type === 'ObjectPattern') {
977
+
978
+ for (const property of contextParam.properties) {
979
+ if (
980
+ property.type === 'ObjectProperty'
981
+ &&
982
+ property.key.type === 'Identifier'
983
+ &&
984
+ property.key.name === 'context'
985
+ &&
986
+ property.value.type === 'Identifier'
987
+ ) {
988
+
989
+ routeDef.contextName = property.value.name;
990
+ break;
991
+ }
992
+ }
993
+
994
+ if (!routeDef.contextName) {
995
+ routeDef.contextName = 'context';
996
+ contextParam.properties.push(
997
+ t.objectProperty( t.identifier('context'), t.identifier( routeDef.contextName ) )
998
+ );
999
+ }
1000
+
1001
+ } else if (contextParam?.type === 'Identifier') {
1002
+ console.log("routeDef.contextName", routeDef.contextName);
1003
+ routeDef.contextName = contextParam.name;
1004
+ }
1005
+ }
1006
+
1007
+ function wrapRouteDefs( file: TFileInfos ) {
1008
+
1009
+ const importedServicesList = Object.entries(file.importedServices);
1010
+ if (importedServicesList.length === 0)
1011
+ return;
1012
+
1013
+ const definitions: types.BlockStatement["body"] = [];
1014
+ if (file.side === 'front') {
1015
+
1016
+ // Limit to one route def per file
1017
+ const routesDefCount = file.routeDefinitions.length;
1018
+ if (routesDefCount === 0)
1019
+ return;
1020
+ else if (routesDefCount > 1)
1021
+ throw new Error(`Frontend route definition files (/client/pages/**/**.ts) can contain only one route definition.
1022
+ ${routesDefCount} were given in ${file.path}.`);
1023
+
1024
+ const routeDef = file.routeDefinitions[0];
1025
+
1026
+ // Client route definition: Add chunk id
1027
+ let [routePath, ...routeArgs] = routeDef.definition.arguments;
1028
+ const callee = routeDef.definition.callee;
1029
+
1030
+ if (callee.object.name === 'Router') {
1031
+
1032
+ // Inject chunk id in options (2nd arg)
1033
+ const newRouteArgs = enrichRouteOptions(routeDef, routeArgs, file.path);
1034
+ if (newRouteArgs === 'ALREADY_PROCESSED')
1035
+ return;
1036
+
1037
+ routeArgs = newRouteArgs;
1038
+ }
1039
+
1040
+ // Force babel to create new fresh nodes
1041
+ // If we directy use statementParent, it will not be included in the final compiler code
1042
+ definitions.push(
1043
+ t.returnStatement(
1044
+ t.callExpression(
1045
+ t.memberExpression(
1046
+ t.identifier( callee.object.name ),
1047
+ callee.property,
1048
+ ),
1049
+ [routePath, ...routeArgs]
1050
+ )
1051
+ )
1052
+ )
1053
+
1054
+ } else {
1055
+
1056
+ definitions.push(
1057
+ // Without spread = react jxx need additionnal loader
1058
+ ...file.routeDefinitions.map( def =>
1059
+ t.expressionStatement(def.definition)
1060
+ ),
1061
+ )
1062
+ }
1063
+
1064
+ /*
1065
+ ({ Router, app: { Events } }}) => {
1066
+ ...
1067
+ }
1068
+ */
1069
+ const appSpread: types.ObjectProperty[] = []
1070
+ const servicesSpread: types.ObjectProperty[] = [
1071
+ t.objectProperty(
1072
+ t.identifier('app'),
1073
+ t.identifier('app'),
1074
+ ),
1075
+ t.objectProperty(
1076
+ t.identifier('context'),
1077
+ t.identifier('context'),
1078
+ )
1079
+ ]
1080
+ for (const [local, imported] of importedServicesList) {
1081
+ if (imported === 'app')
1082
+ appSpread.push(
1083
+ t.objectProperty(
1084
+ t.identifier(local),
1085
+ t.identifier(local),
1086
+ )
1087
+ )
1088
+ else
1089
+ servicesSpread.push(
1090
+ t.objectProperty(
1091
+ t.identifier(local),
1092
+ t.identifier(imported),
1093
+ )
1094
+ )
1095
+ }
1096
+
1097
+ // export const __register = ({ Router, app }) => { ... }
1098
+ const exportDeclaration = t.exportNamedDeclaration(
1099
+ t.variableDeclaration('const', [
1100
+ t.variableDeclarator(
1101
+ t.identifier('__register'),
1102
+ t.arrowFunctionExpression(
1103
+ [
1104
+ t.objectPattern(servicesSpread)
1105
+ ],
1106
+ t.blockStatement([
1107
+ // const { Events } = app;
1108
+ t.variableDeclaration('const', [
1109
+ t.variableDeclarator(
1110
+ t.objectPattern(appSpread),
1111
+ t.identifier('app')
1112
+ )
1113
+ ]),
1114
+ // Router.post(....)
1115
+ ...definitions,
1116
+ ])
1117
+ )
1118
+ )
1119
+ ])
1120
+ );
1121
+
1122
+
1123
+ // (file.path.includes('clients/prospect/search') && side === 'client')
1124
+ // && console.log( file.path, generate(exportDeclaration).code );
1125
+
1126
+ return exportDeclaration;
1127
+ }
1128
+
1129
+ return plugin;
1130
+ }