proteum 1.0.0 → 1.0.3

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 (70) hide show
  1. package/AGENTS.md +9 -0
  2. package/cli/app/config.ts +61 -0
  3. package/cli/app/index.ts +227 -0
  4. package/cli/bin.js +35 -0
  5. package/cli/commands/build.ts +60 -0
  6. package/cli/commands/deploy/app.ts +29 -0
  7. package/cli/commands/deploy/web.ts +60 -0
  8. package/cli/commands/dev.ts +124 -0
  9. package/cli/commands/init.ts +85 -0
  10. package/cli/commands/refresh.ts +18 -0
  11. package/cli/compiler/client/identite.ts +69 -0
  12. package/cli/compiler/client/index.ts +343 -0
  13. package/cli/compiler/common/babel/index.ts +173 -0
  14. package/cli/compiler/common/babel/plugins/index.ts +0 -0
  15. package/cli/compiler/common/babel/plugins/services.ts +586 -0
  16. package/cli/compiler/common/babel/routes/imports.ts +127 -0
  17. package/cli/compiler/common/babel/routes/routes.ts +1170 -0
  18. package/cli/compiler/common/files/autres.ts +39 -0
  19. package/cli/compiler/common/files/images.ts +42 -0
  20. package/cli/compiler/common/files/style.ts +82 -0
  21. package/cli/compiler/common/index.ts +165 -0
  22. package/cli/compiler/index.ts +585 -0
  23. package/cli/compiler/server/index.ts +220 -0
  24. package/cli/index.ts +213 -0
  25. package/cli/paths.ts +165 -0
  26. package/cli/print.ts +12 -0
  27. package/cli/tsconfig.json +42 -0
  28. package/cli/utils/index.ts +22 -0
  29. package/cli/utils/keyboard.ts +78 -0
  30. package/client/app/index.ts +2 -0
  31. package/client/components/Dialog/Manager.tsx +3 -49
  32. package/client/components/Dialog/index.less +3 -1
  33. package/client/components/index.ts +1 -2
  34. package/client/services/router/index.tsx +6 -16
  35. package/common/errors/index.tsx +12 -31
  36. package/package.json +58 -22
  37. package/server/app/container/config.ts +20 -1
  38. package/server/app/container/console/index.ts +1 -1
  39. package/server/services/auth/index.ts +62 -27
  40. package/server/services/auth/router/request.ts +17 -6
  41. package/server/services/router/http/index.ts +3 -3
  42. package/server/services/router/response/index.ts +1 -1
  43. package/server/services/schema/request.ts +28 -10
  44. package/server/utils/slug.ts +0 -3
  45. package/tsconfig.common.json +2 -1
  46. package/types/global/constants.d.ts +12 -0
  47. package/changelog.md +0 -5
  48. package/client/components/Button.tsx +0 -298
  49. package/client/components/Dialog/card.tsx +0 -208
  50. package/client/data/input.ts +0 -32
  51. package/client/pages/bug.tsx.old +0 -60
  52. package/templates/composant.tsx +0 -40
  53. package/templates/form.ts +0 -30
  54. package/templates/modal.tsx +0 -47
  55. package/templates/modele.ts +0 -56
  56. package/templates/page.tsx +0 -74
  57. package/templates/route.ts +0 -43
  58. package/templates/service.ts +0 -75
  59. package/vscode/copyimportationpath/.eslintrc.json +0 -24
  60. package/vscode/copyimportationpath/.vscodeignore +0 -12
  61. package/vscode/copyimportationpath/CHANGELOG.md +0 -9
  62. package/vscode/copyimportationpath/README.md +0 -3
  63. package/vscode/copyimportationpath/copyimportationpath-0.0.1.vsix +0 -0
  64. package/vscode/copyimportationpath/out/extension.js +0 -206
  65. package/vscode/copyimportationpath/out/extension.js.map +0 -1
  66. package/vscode/copyimportationpath/package-lock.json +0 -4536
  67. package/vscode/copyimportationpath/package.json +0 -86
  68. package/vscode/copyimportationpath/src/extension.ts +0 -300
  69. package/vscode/copyimportationpath/tsconfig.json +0 -22
  70. package/vscode/copyimportationpath/vsc-extension-quickstart.md +0 -42
@@ -0,0 +1,1170 @@
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
+ let currentPath: NodePath | null = path;
654
+ while (currentPath) {
655
+
656
+ const parent = currentPath.parentPath;
657
+ if (!parent)
658
+ return false;
659
+
660
+ // function Foo() {}
661
+ if (parent.isProgram())
662
+ return true;
663
+
664
+ // export default function Foo() {} / export default () => {}
665
+ if (
666
+ parent.isExportDefaultDeclaration()
667
+ &&
668
+ parent.parentPath?.isProgram()
669
+ )
670
+ return true;
671
+
672
+ // export const Foo = () => {}
673
+ if (
674
+ parent.isExportNamedDeclaration()
675
+ &&
676
+ parent.parentPath?.isProgram()
677
+ )
678
+ return true;
679
+
680
+ // const Foo = () => {} (top-level) / export const Foo = () => {}
681
+ if (parent.isVariableDeclarator()) {
682
+
683
+ const declaration = parent.parentPath;
684
+ if (!declaration?.isVariableDeclaration())
685
+ return false;
686
+
687
+ const declarationParent = declaration.parentPath;
688
+ if (!declarationParent)
689
+ return false;
690
+
691
+ if (declarationParent.isProgram())
692
+ return true;
693
+
694
+ if (
695
+ declarationParent.isExportNamedDeclaration()
696
+ &&
697
+ declarationParent.parentPath?.isProgram()
698
+ )
699
+ return true;
700
+ }
701
+
702
+ // Support top-level component wrappers such as React.forwardRef(...) and React.memo(...)
703
+ if (
704
+ parent.isCallExpression()
705
+ &&
706
+ isSupportedComponentWrapperCall(parent, currentPath)
707
+ ) {
708
+ currentPath = parent;
709
+ continue;
710
+ }
711
+
712
+ return false;
713
+ }
714
+
715
+ return false;
716
+ }
717
+
718
+ function isSupportedComponentWrapperCall(
719
+ callPath: NodePath<types.CallExpression>,
720
+ wrappedPath: NodePath
721
+ ): boolean {
722
+
723
+ if (callPath.node.arguments[0] !== wrappedPath.node)
724
+ return false;
725
+
726
+ const callee = callPath.node.callee;
727
+ if (callee.type === 'Identifier')
728
+ return ['forwardRef', 'memo'].includes(callee.name);
729
+
730
+ if (
731
+ callee.type === 'MemberExpression'
732
+ &&
733
+ !callee.computed
734
+ &&
735
+ callee.property.type === 'Identifier'
736
+ )
737
+ return ['forwardRef', 'memo'].includes(callee.property.name);
738
+
739
+ return false;
740
+ }
741
+
742
+ function ensureUseContextImport(path: NodePath<types.Program>, pluginState: TPluginState) {
743
+
744
+ if (!pluginState.needsUseContextImport)
745
+ return;
746
+
747
+ const body = path.node.body;
748
+
749
+ // Already imported as a value import
750
+ for (const stmt of body) {
751
+ if (
752
+ stmt.type === 'ImportDeclaration'
753
+ &&
754
+ stmt.source.value === '@/client/context'
755
+ &&
756
+ stmt.importKind !== 'type'
757
+ &&
758
+ stmt.specifiers.some(s =>
759
+ s.type === 'ImportDefaultSpecifier' && s.local.name === 'useContext'
760
+ )
761
+ )
762
+ return;
763
+ }
764
+
765
+ // Try to reuse an existing value import from the same module
766
+ for (const stmt of body) {
767
+ if (
768
+ stmt.type !== 'ImportDeclaration'
769
+ ||
770
+ stmt.source.value !== '@/client/context'
771
+ ||
772
+ stmt.importKind === 'type'
773
+ )
774
+ continue;
775
+
776
+ const hasDefaultImport = stmt.specifiers.some(s => s.type === 'ImportDefaultSpecifier');
777
+ if (!hasDefaultImport) {
778
+ stmt.specifiers.unshift(
779
+ t.importDefaultSpecifier(t.identifier('useContext'))
780
+ );
781
+ return;
782
+ }
783
+ }
784
+
785
+ // Otherwise, add a new import (placed after existing imports)
786
+ const importDeclaration = t.importDeclaration(
787
+ [t.importDefaultSpecifier(t.identifier('useContext'))],
788
+ t.stringLiteral('@/client/context')
789
+ );
790
+
791
+ let insertIndex = 0;
792
+ while (insertIndex < body.length && body[insertIndex].type === 'ImportDeclaration')
793
+ insertIndex++;
794
+
795
+ body.splice(insertIndex, 0, importDeclaration);
796
+ }
797
+
798
+ function getFileInfos( filename: string ): TFileInfos {
799
+
800
+ const file: TFileInfos = {
801
+ process: true,
802
+ side: 'back',
803
+ path: filename,
804
+ importedServices: {},
805
+ routeDefinitions: []
806
+ }
807
+
808
+ // Relative path
809
+ let relativeFileName: string | undefined;
810
+ if (filename.startsWith( cli.paths.appRoot ))
811
+ relativeFileName = filename.substring( cli.paths.appRoot.length );
812
+ if (filename.startsWith( cli.paths.coreRoot ))
813
+ relativeFileName = filename.substring( cli.paths.coreRoot.length );
814
+
815
+ // The file isn't a route definition
816
+ if (relativeFileName === undefined) {
817
+ file.process = false;
818
+ return file;
819
+ }
820
+
821
+ // Differenciate back / front
822
+ if (relativeFileName.startsWith('/client/pages') || relativeFileName.startsWith('/client/components') || relativeFileName.startsWith('/client/hooks')) {
823
+ file.side = 'front';
824
+ } else if (relativeFileName.startsWith('/server/routes')) {
825
+ file.side = 'back';
826
+ } else
827
+ file.process = false;
828
+
829
+ return file
830
+ }
831
+
832
+ function transformDataFetchers(
833
+ path: NodePath<types.CallExpression>,
834
+ routerDefContext: TPluginState,
835
+ routeDef: TRouteDefinition
836
+ ) {
837
+ path.traverse({
838
+ // api.load => move data fetchers to route.options.data
839
+ // So the router is able to load data before rendering the component
840
+ CallExpression(path) {
841
+
842
+ const callee = path.node.callee
843
+
844
+ // Detect api.fetch
845
+ if (!(
846
+ callee.type === 'MemberExpression'
847
+ &&
848
+ callee.object.type === 'Identifier'
849
+ &&
850
+ callee.property.type === 'Identifier'
851
+ &&
852
+ callee.object.name === 'api'
853
+ &&
854
+ callee.property.name === 'fetch'
855
+ ))
856
+ return;
857
+
858
+ /* Reference data fetchers
859
+ {
860
+ stats: api.get(...)
861
+ }
862
+ */
863
+ routeDef.dataFetchers.push(
864
+ ...path.node.arguments[0].properties.map(p => {
865
+
866
+ // Server side: Pass request context as 2nd argument
867
+ // companies: Companies.create( <params>, context )
868
+ if (
869
+ side === 'server'
870
+ &&
871
+ p.type === 'ObjectProperty'
872
+ &&
873
+ p.key.type === 'Identifier'
874
+ &&
875
+ p.value.type === 'CallExpression'
876
+ &&
877
+ // TODO: reliable way to know if it's a service
878
+ !(
879
+ p.value.callee.type === 'MemberExpression'
880
+ &&
881
+ p.value.callee.object.type === 'Identifier'
882
+ &&
883
+ p.value.callee.object.name === 'api'
884
+ )
885
+ ) {
886
+
887
+ // Pass request context as 2nd argument
888
+ p.value.arguments = p.value.arguments.length === 0
889
+ ? [ t.objectExpression([]), t.identifier('context') ]
890
+ : [ p.value.arguments[0], t.identifier('context') ];
891
+
892
+ }
893
+
894
+ return p;
895
+
896
+ })
897
+ );
898
+
899
+ /* Replace the:
900
+ const { stats } = api.fetch({
901
+ stats: api.get(...)
902
+ })
903
+
904
+ by:
905
+ const { stats } = context.data.stats;
906
+ */
907
+ path.replaceWith(
908
+ t.memberExpression(
909
+ t.identifier( routeDef.contextName || 'context' ),
910
+ t.identifier('data'),
911
+ )
912
+ );
913
+ }
914
+ }, routerDefContext);
915
+ }
916
+
917
+ function enrichRouteOptions(
918
+ routeDef: TRouteDefinition,
919
+ routeArgs: types.CallExpression["arguments"],
920
+ filename: string
921
+ ): types.CallExpression["arguments"] | 'ALREADY_PROCESSED' {
922
+
923
+ // Extract client route definition arguments
924
+ let routeOptions: types.ObjectExpression | undefined;
925
+ let renderer: types.ArrowFunctionExpression;
926
+ if (routeArgs.length === 1)
927
+ ([ renderer ] = routeArgs);
928
+ else
929
+ ([ routeOptions, renderer ] = routeArgs);
930
+
931
+ // Generate page chunk id
932
+ const { filepath, chunkId } = cli.paths.getPageChunk(app, filename);
933
+ debug && console.log(`[routes]`, filename.replace(cli.paths.appRoot + '/client/pages', ''));
934
+
935
+ // Create new options to add in route.options
936
+ const newProperties = [
937
+ t.objectProperty(
938
+ t.identifier('id'),
939
+ t.stringLiteral(chunkId)
940
+ ),
941
+ t.objectProperty(
942
+ t.identifier('filepath'),
943
+ t.stringLiteral(filepath)
944
+ ),
945
+ ]
946
+
947
+ // Add data fetchers
948
+ if (routeDef.dataFetchers.length !== 0) {
949
+
950
+ const rendererContext = t.cloneNode( renderer.params[0] );
951
+ // If not already present, add context to the 1st argument (a object spread)
952
+ if (!rendererContext.properties.some( p => p.key.name === 'context' ))
953
+ rendererContext.properties.push(
954
+ t.objectProperty(
955
+ t.identifier('context'),
956
+ t.identifier('context')
957
+ )
958
+ )
959
+
960
+ // (contollerParams) => { stats: api.get(...) }
961
+ const dataFetchersFunc = t.arrowFunctionExpression(
962
+ [rendererContext],
963
+ t.objectExpression(
964
+ routeDef.dataFetchers.map( df => t.cloneNode( df ))
965
+ )
966
+ )
967
+
968
+ // Add the data fetchers to options.data
969
+ newProperties.push(
970
+ t.objectProperty(
971
+ t.identifier('data'),
972
+ dataFetchersFunc
973
+ )
974
+ );
975
+
976
+ // Expose the context variable in the renderer
977
+ exposeContextProperty( renderer, routeDef );
978
+ }
979
+
980
+ if (routeOptions?.properties === undefined)
981
+ return [
982
+ t.objectExpression(newProperties),
983
+ renderer
984
+ ]
985
+
986
+ // Test if the route options were not already processed
987
+ const wasAlreadyProcessed = routeOptions.properties.some( o =>
988
+ o.type === 'ObjectProperty'
989
+ &&
990
+ o.key.type === 'Identifier'
991
+ &&
992
+ o.key.name === 'id'
993
+ )
994
+
995
+ if (wasAlreadyProcessed) {
996
+ // Cancel processing
997
+ debug && console.log(`[routes]`, filename, 'Already Processed');
998
+ return 'ALREADY_PROCESSED';
999
+ }
1000
+
1001
+ // Create the new options object
1002
+ return [
1003
+ t.objectExpression([
1004
+ ...routeOptions.properties,
1005
+ ...newProperties
1006
+ ]),
1007
+ renderer
1008
+ ]
1009
+ }
1010
+
1011
+ function exposeContextProperty(
1012
+ renderer: types.ArrowFunctionExpression,
1013
+ routeDef: TRouteDefinition
1014
+ ) {
1015
+ const contextParam = renderer.params[0];
1016
+ if (contextParam?.type === 'ObjectPattern') {
1017
+
1018
+ for (const property of contextParam.properties) {
1019
+ if (
1020
+ property.type === 'ObjectProperty'
1021
+ &&
1022
+ property.key.type === 'Identifier'
1023
+ &&
1024
+ property.key.name === 'context'
1025
+ &&
1026
+ property.value.type === 'Identifier'
1027
+ ) {
1028
+
1029
+ routeDef.contextName = property.value.name;
1030
+ break;
1031
+ }
1032
+ }
1033
+
1034
+ if (!routeDef.contextName) {
1035
+ routeDef.contextName = 'context';
1036
+ contextParam.properties.push(
1037
+ t.objectProperty( t.identifier('context'), t.identifier( routeDef.contextName ) )
1038
+ );
1039
+ }
1040
+
1041
+ } else if (contextParam?.type === 'Identifier') {
1042
+ console.log("routeDef.contextName", routeDef.contextName);
1043
+ routeDef.contextName = contextParam.name;
1044
+ }
1045
+ }
1046
+
1047
+ function wrapRouteDefs( file: TFileInfos ) {
1048
+
1049
+ const importedServicesList = Object.entries(file.importedServices);
1050
+ if (importedServicesList.length === 0)
1051
+ return;
1052
+
1053
+ const definitions: types.BlockStatement["body"] = [];
1054
+ if (file.side === 'front') {
1055
+
1056
+ // Limit to one route def per file
1057
+ const routesDefCount = file.routeDefinitions.length;
1058
+ if (routesDefCount === 0)
1059
+ return;
1060
+ else if (routesDefCount > 1)
1061
+ throw new Error(`Frontend route definition files (/client/pages/**/**.ts) can contain only one route definition.
1062
+ ${routesDefCount} were given in ${file.path}.`);
1063
+
1064
+ const routeDef = file.routeDefinitions[0];
1065
+
1066
+ // Client route definition: Add chunk id
1067
+ let [routePath, ...routeArgs] = routeDef.definition.arguments;
1068
+ const callee = routeDef.definition.callee;
1069
+
1070
+ if (callee.object.name === 'Router') {
1071
+
1072
+ // Inject chunk id in options (2nd arg)
1073
+ const newRouteArgs = enrichRouteOptions(routeDef, routeArgs, file.path);
1074
+ if (newRouteArgs === 'ALREADY_PROCESSED')
1075
+ return;
1076
+
1077
+ routeArgs = newRouteArgs;
1078
+ }
1079
+
1080
+ // Force babel to create new fresh nodes
1081
+ // If we directy use statementParent, it will not be included in the final compiler code
1082
+ definitions.push(
1083
+ t.returnStatement(
1084
+ t.callExpression(
1085
+ t.memberExpression(
1086
+ t.identifier( callee.object.name ),
1087
+ callee.property,
1088
+ ),
1089
+ [routePath, ...routeArgs]
1090
+ )
1091
+ )
1092
+ )
1093
+
1094
+ } else {
1095
+
1096
+ definitions.push(
1097
+ // Without spread = react jxx need additionnal loader
1098
+ ...file.routeDefinitions.map( def =>
1099
+ t.expressionStatement(def.definition)
1100
+ ),
1101
+ )
1102
+ }
1103
+
1104
+ /*
1105
+ ({ Router, app: { Events } }}) => {
1106
+ ...
1107
+ }
1108
+ */
1109
+ const appSpread: types.ObjectProperty[] = []
1110
+ const servicesSpread: types.ObjectProperty[] = [
1111
+ t.objectProperty(
1112
+ t.identifier('app'),
1113
+ t.identifier('app'),
1114
+ ),
1115
+ t.objectProperty(
1116
+ t.identifier('context'),
1117
+ t.identifier('context'),
1118
+ )
1119
+ ]
1120
+ for (const [local, imported] of importedServicesList) {
1121
+ if (imported === 'app')
1122
+ appSpread.push(
1123
+ t.objectProperty(
1124
+ t.identifier(local),
1125
+ t.identifier(local),
1126
+ )
1127
+ )
1128
+ else
1129
+ servicesSpread.push(
1130
+ t.objectProperty(
1131
+ t.identifier(local),
1132
+ t.identifier(imported),
1133
+ )
1134
+ )
1135
+ }
1136
+
1137
+ // export const __register = ({ Router, app }) => { ... }
1138
+ const exportDeclaration = t.exportNamedDeclaration(
1139
+ t.variableDeclaration('const', [
1140
+ t.variableDeclarator(
1141
+ t.identifier('__register'),
1142
+ t.arrowFunctionExpression(
1143
+ [
1144
+ t.objectPattern(servicesSpread)
1145
+ ],
1146
+ t.blockStatement([
1147
+ // const { Events } = app;
1148
+ t.variableDeclaration('const', [
1149
+ t.variableDeclarator(
1150
+ t.objectPattern(appSpread),
1151
+ t.identifier('app')
1152
+ )
1153
+ ]),
1154
+ // Router.post(....)
1155
+ ...definitions,
1156
+ ])
1157
+ )
1158
+ )
1159
+ ])
1160
+ );
1161
+
1162
+
1163
+ // (file.path.includes('clients/prospect/search') && side === 'client')
1164
+ // && console.log( file.path, generate(exportDeclaration).code );
1165
+
1166
+ return exportDeclaration;
1167
+ }
1168
+
1169
+ return plugin;
1170
+ }