5htp 0.6.3 → 0.6.4

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.
@@ -5,7 +5,6 @@
5
5
  // Npm
6
6
  import * as types from '@babel/types'
7
7
  import type { PluginObj, NodePath } from '@babel/core';
8
- import generate from '@babel/generator';
9
8
 
10
9
  // Core
11
10
  import cli from '@cli';
@@ -53,6 +52,13 @@ function Plugin(babel, { app, side, debug }: TOptions) {
53
52
 
54
53
  const t = babel.types as typeof types;
55
54
 
55
+ type TPluginState = {
56
+ filename: string,
57
+ file: TFileInfos,
58
+ apiInjectedRootFunctions: WeakSet<types.Node>,
59
+ needsUseContextImport: boolean
60
+ }
61
+
56
62
  /*
57
63
  - Wrap route.get(...) with (app: Application) => { }
58
64
  - Inject chunk ID into client route options
@@ -70,14 +76,14 @@ function Plugin(babel, { app, side, debug }: TOptions) {
70
76
  const stats = page.data.stats;
71
77
  */
72
78
 
73
- const plugin: PluginObj<{
74
- filename: string,
75
- file: TFileInfos
76
- }> = {
79
+ const plugin: PluginObj<TPluginState> = {
77
80
  pre(state) {
78
81
  this.filename = state.opts.filename as string;
79
82
 
80
83
  this.file = getFileInfos(this.filename);
84
+
85
+ this.apiInjectedRootFunctions = new WeakSet();
86
+ this.needsUseContextImport = false;
81
87
  },
82
88
  visitor: {
83
89
  // Find @app imports
@@ -184,9 +190,7 @@ function Plugin(babel, { app, side, debug }: TOptions) {
184
190
  // Delete the route def since it will be replaced by a wrapper
185
191
  path.replaceWithMultiple([]);
186
192
 
187
- /*
188
- Any api call via the front pages / components
189
- */
193
+
190
194
  } else if (this.file.side === 'front') {
191
195
 
192
196
  const isAService = (
@@ -201,10 +205,12 @@ function Plugin(babel, { app, side, debug }: TOptions) {
201
205
 
202
206
  Events.Create( form.data ).then(res => toast.success(res.message))
203
207
  =>
204
- api.post( '/api/events/create', form.data ).then(res => toast.success(res.message))
208
+ api.post( '/api/events/create', form.data ).then(res => toast.success(res.message)).catch(app.handleError)
205
209
  */
206
210
  if (side === 'client' && !clientServices.includes(serviceName)) {
207
211
 
212
+ ensureApiExposedInRootFunction(path, this);
213
+
208
214
  // Get complete call path
209
215
  const apiPath = '/api/' + completePath.join('/');
210
216
 
@@ -220,6 +226,8 @@ function Plugin(babel, { app, side, debug }: TOptions) {
220
226
  ), apiPostArgs
221
227
  )
222
228
  )
229
+
230
+ ensureBackendServicePromiseCatch(path);
223
231
 
224
232
  /* [server] Backend Service calls
225
233
 
@@ -257,6 +265,8 @@ function Plugin(babel, { app, side, debug }: TOptions) {
257
265
 
258
266
  if (!this.file.process)
259
267
  return;
268
+
269
+ ensureUseContextImport(path, this);
260
270
 
261
271
  const wrappedrouteDefs = wrapRouteDefs( this.file );
262
272
  if (wrappedrouteDefs)
@@ -266,6 +276,485 @@ function Plugin(babel, { app, side, debug }: TOptions) {
266
276
  }
267
277
  }
268
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
+
269
758
  function getFileInfos( filename: string ): TFileInfos {
270
759
 
271
760
  const file: TFileInfos = {
@@ -282,8 +771,6 @@ function Plugin(babel, { app, side, debug }: TOptions) {
282
771
  relativeFileName = filename.substring( cli.paths.appRoot.length );
283
772
  if (filename.startsWith( cli.paths.coreRoot ))
284
773
  relativeFileName = filename.substring( cli.paths.coreRoot.length );
285
- /*if (filename.startsWith('/node_modules/5htp-core/'))
286
- relativeFileName = filename.substring( '/node_modules/5htp-core/'.length );*/
287
774
 
288
775
  // The file isn't a route definition
289
776
  if (relativeFileName === undefined) {
@@ -304,7 +791,7 @@ function Plugin(babel, { app, side, debug }: TOptions) {
304
791
 
305
792
  function transformDataFetchers(
306
793
  path: NodePath<types.CallExpression>,
307
- routerDefContext: PluginObj,
794
+ routerDefContext: TPluginState,
308
795
  routeDef: TRouteDefinition
309
796
  ) {
310
797
  path.traverse({
@@ -640,4 +1127,4 @@ function Plugin(babel, { app, side, debug }: TOptions) {
640
1127
  }
641
1128
 
642
1129
  return plugin;
643
- }
1130
+ }
@@ -8,9 +8,6 @@ import dayjs from 'dayjs';
8
8
 
9
9
  // Plugins
10
10
  const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
11
- import PluginIndexage from './plugins/indexage';
12
- import IconesSvg from './plugins/indexage/icones-svg';
13
- import InjectDeps from './plugins/indexage/injection-dependances';
14
11
 
15
12
  // Core
16
13
  import cli from '../..';
@@ -48,6 +45,7 @@ export default function createCommonConfig(
48
45
  ): webpack.Configuration {
49
46
 
50
47
  const dev = mode === 'dev';
48
+ const buildCheck = dev && cli.commandName === 'build';
51
49
  const config: webpack.Configuration = {
52
50
 
53
51
  // Project root
@@ -88,12 +86,6 @@ export default function createCommonConfig(
88
86
 
89
87
  }),
90
88
 
91
- new PluginIndexage(side === 'client' ? {
92
- 'icones-svg': new IconesSvg(app),
93
- } : {
94
- //'injection-dependances': new InjectDeps,
95
- }),
96
-
97
89
  ...(side === 'client' && cli.args.analyze ? [
98
90
 
99
91
  new BundleAnalyzerPlugin({
@@ -136,13 +128,17 @@ export default function createCommonConfig(
136
128
  // "webpack" The "path" argument must be of type string. Received undefined
137
129
  // https://github.com/webpack/webpack/issues/12616
138
130
  // Update: Hum it's fixed, just had to update webpack deps
139
- cache: dev,
131
+ cache: dev && !buildCheck,
140
132
 
141
- profile: true,
133
+ profile: !buildCheck,
142
134
 
143
135
  // Pour bundle-stats
144
136
  // https://github.com/relative-ci/bundle-stats/tree/master/packages/cli#webpack-configuration
145
- stats: {
137
+ stats: buildCheck ? {
138
+ colors: true,
139
+ errorDetails: true,
140
+ timings: true,
141
+ } : {
146
142
  cached: dev,
147
143
  cachedAssets: dev,
148
144
  chunks: dev,
@@ -160,4 +156,4 @@ export default function createCommonConfig(
160
156
 
161
157
  return config;
162
158
 
163
- }
159
+ }