pawa-ssr 1.2.5 → 1.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import {getServerInstance, setServer} from 'pawajs/server.js'
1
+ import {getServerInstance, setServer} from '../src/pawajs/server.js'
2
2
  import { DOMParser,parseHTML, HTMLElement} from 'linkedom'
3
3
  import PawaComponent from './pawaComponent.js'
4
4
  import { propsValidator, evaluateExpr,extractAtExpressions, reArrangeAttri,resumeAttribute, pawaGenerateId } from './utils.js'
@@ -22,15 +22,39 @@ const useInsert=(obj={})=>{
22
22
  Object.assign(appContext.stateContext.insert, obj);
23
23
  }
24
24
  }catch(error){
25
- if(isDevelopment){
25
+ if(__pawaDev){
26
26
  console.log(error.message,error.stack)
27
27
  }
28
28
  }
29
29
  }
30
-
30
+ const useAsync=()=>{
31
+ try {
32
+ const appContext=store.getStore();
33
+ if (appContext?.stateContext) {
34
+ const keep=appContext?.stateContext
35
+ return {
36
+ $async:(callback)=>{
37
+ if (typeof callback === 'function') {
38
+ store.getStore().stateContext=keep
39
+ const res=callback()
40
+ return res
41
+ }
42
+ },
43
+ onSuspense:(html)=>{
44
+ if (typeof html === 'string') {
45
+ keep.suspense=html
46
+ }
47
+ }
48
+ }
49
+ }
50
+ } catch (error) {
51
+
52
+ }
53
+ }
31
54
  const setContext=()=>{
32
55
  const id=crypto.randomUUID()
33
56
  const setValue=(context={})=>{
57
+
34
58
  try{
35
59
  const appContext = store.getStore();
36
60
  if(appContext?.stateContext?.transportContext){
@@ -60,18 +84,20 @@ const accessChild=()=>{
60
84
  }
61
85
  }
62
86
  const useServer=()=>{
87
+ const appContext = store.getStore().stateContext;
63
88
  try{
64
- const appContext = store.getStore();
65
- if (appContext?.stateContext) {
66
- appContext.stateContext.serializeData = true;
89
+ if (appContext) {
90
+ appContext.useServer=true
91
+ appContext.serializeData = true;
67
92
  const setServerData=(data={})=>{
93
+
68
94
  for (const [key,value] of Object.entries(data)) {
69
95
  if (typeof value !== 'function') {
70
- appContext.stateContext.fromSerialized[key]=value
96
+ appContext.fromSerialized[key]=value
71
97
  }
72
98
  }
73
99
  }
74
- const getServerData=()=>appContext.stateContext.fromSerialized
100
+ const getServerData=()=>appContext.fromSerialized
75
101
  return {setServerData,getServerData}
76
102
  }
77
103
  }catch(error){
@@ -85,6 +111,7 @@ const useContext=(context)=>{
85
111
  const appContext = store.getStore();
86
112
  const id=context?.id
87
113
  if(appContext?.stateContext?.transportContext && id){
114
+
88
115
  return appContext.stateContext.transportContext[id];
89
116
  }
90
117
  return {}
@@ -129,9 +156,10 @@ setServer({
129
156
  setContext,
130
157
  $state,
131
158
  accessChild,
132
- useServer
159
+ useServer,
160
+ useAsync
133
161
  })
134
-
162
+ export const pawaForServer=setServer
135
163
  const components = new Map();
136
164
  export const getPawaComponentsMap =()=>{
137
165
  return components ;
@@ -267,7 +295,7 @@ export const useValidateComponent=(component,object)=>{
267
295
  * @param {import('./pawaElement.js').default} el
268
296
  * @returns
269
297
  */
270
- const component=async (el)=>{
298
+ const component=async (el,stream)=>{
271
299
  if(el._running){
272
300
  return
273
301
  }
@@ -276,7 +304,8 @@ const component=async (el)=>{
276
304
  const document = el.ownerDocument
277
305
  const slots={}
278
306
  /**@type {AppStateContextType} */
279
- const oldAppContext=getStore().stateContext
307
+ const oldAppContext=store.getStore().stateContext
308
+ // console.log(oldAppContext)
280
309
  let stateContext={}
281
310
  let appContext={
282
311
  transportContext: {},
@@ -289,7 +318,12 @@ const component=async (el)=>{
289
318
  accessChild:false,
290
319
  serializeData:false,
291
320
  serialize:{},
292
- fromSerialized:{}
321
+ fromSerialized:{},
322
+ useServer:false,
323
+ suspense:'',
324
+ carrier:'',
325
+ pc:el.getAttribute('p:c')
326
+
293
327
  }
294
328
  Object.assign(appContext.transportContext, oldAppContext?.transportContext || {})
295
329
  const slotHydrates={}
@@ -341,22 +375,22 @@ appContext.component._prop={children,...el._props,...slots}
341
375
  ...el._props
342
376
  }
343
377
  for (const fn of compoBeforeCall) {
344
- try {
378
+ try {
345
379
  await fn(stateContext,app)
346
- } catch (error) {
347
- console.error(`Error in beforeCall for ${el._componentName}:`, error.message,error.stack)
348
- __pawaDev.setError({
380
+ } catch (error) {
381
+ console.error(`Error in beforeCall for ${el._componentName}:`, error.message,error.stack)
382
+ __pawaDev.setError({
349
383
  el:el,
350
384
  msg:`from compoBeforeCall${el._componentName}:`+ error.message + error.stack,
351
385
  directives:'plugin',
352
386
  stack:error.stack,
353
387
  template:el?._template,
354
388
  })
355
- }
356
- }
357
-
358
- const div=document.createElement('div')
359
- el._setResumeAttr(`c-compo-${el._componentName}-${id}`)
389
+ }
390
+ }
391
+
392
+ const div=document.createElement('div')
393
+ el._setResumeAttr(`c-compo-${el._componentName}-${id}`)
360
394
  let compo=""
361
395
  try{
362
396
  if(done){
@@ -375,11 +409,11 @@ appContext.component._prop={children,...el._props,...slots}
375
409
  template:el?._template,
376
410
  })
377
411
  }
378
- if (appContext?.insert){
412
+ if (appContext?.insert){
379
413
  Object.assign(el._context,appContext.insert)
380
414
  }
381
- // if(compo instanceOf Promise){}
382
- if(typeof compo !== 'boolean' && compo){
415
+
416
+ if(typeof compo !== 'boolean' && typeof compo === 'string'){
383
417
  div.innerHTML=compo
384
418
  }
385
419
  const findElement=div.querySelector('[--]') || div.querySelector('[r-]')
@@ -403,7 +437,7 @@ appContext.component._prop={children,...el._props,...slots}
403
437
  }
404
438
  }
405
439
 
406
-
440
+
407
441
  hydrate.data=null
408
442
  if (appContext.serializeData) { // get serialized data
409
443
  for (const [key,value] of Object.entries(appContext.fromSerialized)) {
@@ -417,12 +451,232 @@ appContext.component._prop={children,...el._props,...slots}
417
451
  for (const [key] of Object.entries(appContext.insert)) {
418
452
  hydrate.context.push(key)
419
453
  }
454
+
420
455
  comment.data=`component+${id}+${el._componentName}+${encodeJSON(hydrate)}`
421
456
 
422
457
  for (const newElement of newElements) {
423
458
  comment.parentElement.insertBefore(newElement, endComment)
424
- newElement.setAttribute('p:c', el.getAttribute('p:c'))
459
+
460
+ newElement.setAttribute('p:c', el.getAttribute('p:c'))
461
+ Array.from(el.attributes).forEach((value) => {
462
+ if (value.name.startsWith('c-')) {
463
+ newElement.setAttribute(value.name, value.value)
464
+ }
465
+ })
466
+
467
+ newElement.setAttribute(`c-c-${el._componentName}-${id}`, id)
468
+ await render(newElement, el._context,stream)
469
+
470
+ }
471
+ store.getStore().stateContext=appContext.formerContext
472
+ } catch (error) {
473
+ console.log(error.message,error.stack);
474
+ __pawaDev.setError({
475
+ el:el,
476
+ msg:`from ${el.tagName} component`,
477
+ directives:'component',
478
+ stack:error.stack,
479
+ template:el?._template,
480
+ })
481
+
482
+ }
483
+ }
484
+ /**
485
+ *
486
+ * @param {import('./pawaElement.js').default} el
487
+ * @returns
488
+ */
489
+ const streamingComponent=async (el,stream)=>{
490
+ if(el._running){
491
+ return
492
+ }
493
+ try {
494
+ const slot=el._slots
495
+ const document = el.ownerDocument
496
+ const slots={}
497
+ /**@type {AppStateContextType} */
498
+ const oldAppContext=store.getStore().stateContext
499
+ // console.log(oldAppContext)
500
+ let stateContext={}
501
+ let appContext={
502
+ transportContext: {},
503
+ innerContext:el._context,
504
+ mount:[],
505
+ formerContext:oldAppContext,
506
+ name:el._componentName,
507
+ insert:{},
508
+ component:el._component,
509
+ accessChild:false,
510
+ serializeData:false,
511
+ serialize:{},
512
+ fromSerialized:{},
513
+ useServer:false,
514
+ suspense:'',
515
+ carrier:'',
516
+ pc:el.getAttribute('p:c')
517
+
518
+ }
519
+ Object.assign(appContext.transportContext, oldAppContext?.transportContext || {})
520
+ const slotHydrates={}
521
+ Array.from(slot.children).forEach(prop =>{
522
+ if (prop.getAttribute('prop')) {
523
+ const html=prop.innerHTML
524
+ slots[prop.getAttribute('prop')]=()=>html
525
+ slotHydrates[prop.getAttribute('prop')]=html
526
+ }else{
527
+ console.warn('sloting props must have prop attribute')
528
+ }
529
+ })
530
+ const children=el._componentChildren
531
+ const hydrate={
532
+ children:children,
533
+ props:{
534
+ ...el._hydrateProps,
535
+ },
536
+ slots:{...slotHydrates},
537
+ }
538
+
539
+ const id=pawaGenerateId(10)
540
+ const encodeJSON = (obj) => Buffer.from(JSON.stringify(obj)).toString('base64').replace(/\+/g, '-');
541
+ const comment = document.createComment(`component+${id}`);
542
+ const endComment=document.createComment(`end component+${id}+${el._componentName}`)
543
+ el.replaceWith(endComment)
544
+ endComment.parentElement.insertBefore(comment,endComment)
545
+ /**
546
+ * @type {import('./pawaComponent.js').default}
547
+ */
548
+ const component =el._component
549
+ stateContext=component
550
+ /**
551
+ *
552
+ * @param {object} props
553
+ * @returns {object}
554
+ */
555
+ stateContext._name=el._componentName
556
+
557
+ let done=true
558
+ if(Object.entries(el._component.validPropRule).length > 0){
559
+ done=propsValidator(el._component.validPropRule,{...el._props,...slots},appContext.component._name,el.toString(),el)
560
+ }
561
+ appContext.component._prop={children,...el._props,...slots}
562
+
563
+ const app = {
564
+ children,
565
+ ...slots,
566
+ ...el._props
567
+ }
568
+ for (const fn of compoBeforeCall) {
569
+ try {
570
+ await fn(stateContext,app)
571
+ } catch (error) {
572
+ console.error(`Error in beforeCall for ${el._componentName}:`, error.message,error.stack)
573
+ __pawaDev.setError({
574
+ el:el,
575
+ msg:`from compoBeforeCall${el._componentName}:`+ error.message + error.stack,
576
+ directives:'plugin',
577
+ stack:error.stack,
578
+ template:el?._template,
579
+ })
580
+ }
581
+ }
582
+
583
+ const div=document.createElement('div')
584
+ el._setResumeAttr(`c-compo-${el._componentName}-${id}`)
585
+ let compo=""
586
+ let isBoundary=false
587
+ try{
588
+ if(done){
589
+
590
+ store.getStore().stateContext=appContext
591
+ compo=component.component(app)
592
+ isBoundary=appContext.suspense && appContext.useServer
593
+ if (compo instanceof Promise && !isBoundary) {
594
+ compo=await compo.then((res)=>res)
595
+ }
596
+ }
597
+
598
+ }catch(error){
599
+ console.error(`error from PawaComponent.${appContext.component._name}`,error.message,error.stack)
600
+ __pawaDev.setError({
601
+ el:el,
602
+ msg:`error from PawaComponent.${appContext.component._name}`+ error.message + error.stack,
603
+ directives:el.tagName,
604
+ stack:error.stack,
605
+ template:el?._template,
606
+ })
607
+ }
608
+ if (appContext?.insert){
609
+ Object.assign(el._context,appContext.insert)
610
+ }
611
+ if (isBoundary) {
612
+ store.getStore().batch.push({
613
+ component:compo,
614
+ id:id,
615
+ comment:{
616
+ hydrate:hydrate,
617
+ encodeJSON:encodeJSON,
618
+ name:el._componentName
619
+ },
620
+ appContext:appContext,
621
+ context:{...el._context},
622
+ restProps:el._restProps,
623
+ hydrate:hydrate
624
+ })
625
+
626
+ }
627
+ if(isBoundary){
628
+ compo=`<div id="p${id}">${appContext.suspense}</div>`
629
+ }
630
+ if(typeof compo !== 'boolean' && typeof compo === 'string'){
631
+ div.innerHTML=compo
632
+ }
633
+ const findElement=div.querySelector('[--]') || div.querySelector('[r-]')
634
+ if (findElement) {
635
+ for (const [key,value] of Object.entries(el?._restProps)) {
636
+ findElement.setAttribute(value.name,value.value)
637
+ }
638
+ findElement.removeAttribute('--')
639
+ findElement.removeAttribute('r-')
640
+ }
641
+
642
+ // Handle multiple root nodes (Fragments)
643
+ const newElements = Array.from(div.children)
644
+ for (const fn of compoAfterCall) {
645
+ try {
646
+ // Note: passing the first child might be limiting if there are multiple,
647
+ // but keeping API consistent for now.
648
+ await fn(appContext, newElements[0], el)
649
+ } catch (error) {
650
+ console.error(error.message,error.stack)
651
+ }
652
+ }
653
+
654
+
655
+ hydrate.data=null
656
+ if (appContext.serializeData) { // get serialized data
657
+ for (const [key,value] of Object.entries(appContext.fromSerialized)) {
658
+ if (typeof value !== 'function') {
659
+ appContext.serialize[key]=value
660
+ }
661
+ }
662
+ hydrate.data=appContext.serialize
663
+ }
664
+ hydrate.context=[]
665
+ for (const [key] of Object.entries(appContext.insert)) {
666
+ hydrate.context.push(key)
667
+ }
425
668
 
669
+ comment.data=`component+${id}+${el._componentName}+${encodeJSON(hydrate)}`
670
+ stream(`<!--${comment.data}-->`)
671
+
672
+ for (const newElement of newElements) {
673
+ comment.parentElement.insertBefore(newElement, endComment)
674
+ if (!isBoundary) {
675
+ newElement.setAttribute('p:c', el.getAttribute('p:c'))
676
+ }else{
677
+ newElement.setAttribute('p:c',el.getAttribute('p:c'))
678
+ newElement.setAttribute('p-async', el.getAttribute('p:c'))
679
+ }
426
680
  Array.from(el.attributes).forEach((value) => {
427
681
  if (value.name.startsWith('c-')) {
428
682
  newElement.setAttribute(value.name, value.value)
@@ -430,25 +684,33 @@ appContext.component._prop={children,...el._props,...slots}
430
684
  })
431
685
 
432
686
  newElement.setAttribute(`c-c-${el._componentName}-${id}`, id)
433
- await render(newElement, el._context)
687
+
688
+
689
+ await render(newElement, el._context,stream)
690
+
434
691
  }
692
+ stream(`<!--${endComment.data}-->`)
435
693
 
436
694
  appContext.mount.forEach(async(call)=>{
437
695
  await call()
438
696
  })
697
+
698
+
699
+
439
700
  store.getStore().stateContext=appContext.formerContext
440
701
  } catch (error) {
441
- console.log(error.message,error.stack,`at ${el.tagName} component`);
442
- __pawaDev.setError({
702
+ console.log(error.message,error.stack);
703
+ __pawaDev.setError({
443
704
  el:el,
444
705
  msg:`from ${el.tagName} component`,
445
706
  directives:'component',
446
707
  stack:error.stack,
447
708
  template:el?._template,
448
709
  })
710
+
449
711
  }
450
712
  }
451
- const templates=async(el,context)=>{
713
+ const templates=async(el,stream)=>{
452
714
  if(el._running)return
453
715
  const document = el.ownerDocument
454
716
  const comment=document.createComment(`<template>`)
@@ -456,6 +718,7 @@ const templates=async(el,context)=>{
456
718
  el.replaceWith(endComment)
457
719
 
458
720
  endComment.parentElement.insertBefore(comment,endComment)
721
+ stream(`<!--${comment.data}-->`)
459
722
  let element=[]
460
723
  for (const child of Array.from(el.content.children)) {
461
724
  endComment.parentElement.insertBefore(child,endComment)
@@ -471,8 +734,9 @@ const templates=async(el,context)=>{
471
734
  }
472
735
  }
473
736
  }
474
- await render(child,el._context)
737
+ await render(child,el._context,stream)
475
738
  }
739
+ stream(`<!--${endComment.data}-->`)
476
740
  }
477
741
  const textContentHandler = async(el) => {
478
742
  if (el._running || el._componentName) return;
@@ -546,7 +810,7 @@ const attributeHandler =async (el, attr) => {
546
810
  const expressions = extractAtExpressions(value);
547
811
 
548
812
  expressions.forEach(({ fullMatch, expression }) => {
549
- const func = evaluateExpr(
813
+ const func =el._evaluateExpr(
550
814
  expression,
551
815
  el._context,
552
816
  `from text interpolation @{} - ${expression} at ${currentHtmlString} attribute ${attr.name}`
@@ -564,7 +828,7 @@ const attributeHandler =async (el, attr) => {
564
828
  }
565
829
  } catch (error) {
566
830
  console.log(error.message, error.stack);
567
- __pawaDev.setError({
831
+ __pawaDev.setError({
568
832
  el:el,
569
833
  msg:`error from Attribute Handler`+ error.message + error.stack,
570
834
  directives:'Attribute Handler',
@@ -577,13 +841,18 @@ const attributeHandler =async (el, attr) => {
577
841
  evaluate();
578
842
 
579
843
  };
580
-
844
+ const singleElement=new Set()
845
+ const setSingle=(...string)=>{
846
+ string.forEach(v => singleElement.add(v))
847
+ }
848
+ setSingle('img','br')
581
849
  /**
582
850
  *
583
851
  * @param {PawaElement | HTMLElement} el
584
852
  * @param {object} contexts
585
853
  */
586
- export const render =async (el, contexts = {}) => {
854
+ export const render =async (el, contexts = {},stream) => {
855
+ const isStream=store.getStore().stream
587
856
  if(el.hasAttribute('only-client')){
588
857
  el.removeAttribute('only-client')
589
858
  const template=el.ownerDocument.createElement('template')
@@ -616,7 +885,6 @@ export const render =async (el, contexts = {}) => {
616
885
  }
617
886
 
618
887
  PawaElement.Element(el,context)
619
-
620
888
  if(el.childNodes.some(node=>node.nodeType === 3 && node.nodeValue.includes('@{')) && !el._avoidPawaRender){
621
889
  await textContentHandler(el)
622
890
  }
@@ -646,11 +914,11 @@ export const render =async (el, contexts = {}) => {
646
914
  const attributes = Array.from(el.attributes);
647
915
  for(const attr of attributes){
648
916
  if (directives[attr.name]) {
649
- await directives[attr.name](el,attr)
917
+ await directives[attr.name](el,attr,stream)
650
918
  }else if(attr.value.includes('@{')){
651
919
  await attributeHandler(el,attr)
652
920
  }else if (attr.name.startsWith('state-')) {
653
- await directives['state-'](el,attr)
921
+ directives['state-'](el,attr)
654
922
  }
655
923
  else if(fullNamePlugin.has(attr.name)) {
656
924
  if(externalPlugin[attr.name]){
@@ -683,12 +951,18 @@ export const render =async (el, contexts = {}) => {
683
951
 
684
952
  }
685
953
  if(el.tagName === 'TEMPLATE'){
686
- await templates(el)
954
+ await templates(el,stream)
687
955
  return
688
956
  }
689
957
  if (el._componentName) {
690
- await component(el)
958
+ if(isStream){
959
+ await streamingComponent(el,stream)
960
+ return
961
+ }else{
962
+ await component(el,stream)
691
963
  return
964
+ }
965
+ return
692
966
  }
693
967
  }
694
968
  for (const fn of renderBeforeChild) {
@@ -699,10 +973,26 @@ export const render =async (el, contexts = {}) => {
699
973
  }
700
974
  }
701
975
  if(!el._running){
702
- const children = Array.from(el.children);
976
+ const attr=Array.from(el.attributes).map(att=>`${att.name}="${att.value}"`).join(' ')
977
+ const isSingle=singleElement.has(el.tagName.toLowerCase())
978
+ if (isSingle) {
979
+ stream(`<${el.tagName.toLowerCase()} ${attr} />`)
980
+ }else{
981
+ stream(`<${el.tagName.toLowerCase()} ${attr} >`)
982
+ }
983
+ const children = el.childNodes;
703
984
  for(const child of children){
704
- await render(child, el._context);
985
+ if (child.nodeType === 3) {
986
+ stream(child.nodeValue)
987
+ }else if (child.nodeType === 8) {
988
+ stream(`<!--${child.nodeValue}-->`)
989
+ }else if (child.nodeType === 1){
990
+ await render(child, el._context,stream);
991
+ }
705
992
  };
993
+ if (!isSingle) {
994
+ stream(`</${el.tagName.toLowerCase()}>`)
995
+ }
706
996
 
707
997
  }
708
998
 
@@ -717,11 +1007,11 @@ const directives={
717
1007
  'switch':Switch,
718
1008
  'key':Key
719
1009
  }
720
-
721
1010
  export const startApp = async (html, context = {}, devlopment = false) => {
722
1011
  const appContext = {
723
1012
  context: context,
724
1013
  stateContext: null,
1014
+ stream:false
725
1015
  };
726
1016
  isDevelopment = devlopment;
727
1017
  const app = new DOMParser();
@@ -731,9 +1021,9 @@ export const startApp = async (html, context = {}, devlopment = false) => {
731
1021
  const div = body.firstElementChild; // Original app div
732
1022
  const root = body.createElement('div'); // New root to track transformations
733
1023
  root.appendChild(div?.cloneNode(true)); // Clone to preserve original structure
734
-
1024
+ const fakeStream=(html)=>{}
735
1025
  await store.run(appContext, async () => {
736
- await render(root.firstElementChild, context); // Render into cloned div
1026
+ await render(root.firstElementChild, context,fakeStream); // Render into cloned div
737
1027
  });
738
1028
  return {
739
1029
  element: root?.firstElementChild, // Return the transformed element
@@ -741,3 +1031,217 @@ export const startApp = async (html, context = {}, devlopment = false) => {
741
1031
  head:body.head.innerHTML
742
1032
  };
743
1033
  };
1034
+
1035
+ export const startStreamApp = async (html, context = {},stream,{templateStart,templateEnd}) => {
1036
+ const appContext = {
1037
+ context: context,
1038
+ stateContext: null,
1039
+ useServer:false,
1040
+ batch:[],
1041
+ stream:true
1042
+ };
1043
+ const app = new DOMParser();
1044
+
1045
+ const body = app.parseFromString(html, 'text/html');
1046
+
1047
+ const div = body.firstElementChild; // Original app div
1048
+
1049
+ const root = body.createElement('div'); // New root to track transformations
1050
+ root.appendChild(div?.cloneNode(true)); // Clone to preserve original structure
1051
+ await store.run(appContext, async () => {
1052
+ stream(templateStart)
1053
+ stream('<div id="app">')
1054
+ await render(root.firstElementChild, context,stream); // Render into cloned div
1055
+ stream('</div>')
1056
+ const batchs=store.getStore().batch
1057
+
1058
+ // stream(`<script>console.log('first run') </script>`)
1059
+ stream(`<script>
1060
+ if (window?.__pawaDev && !window?.__pawaHasStarted) {
1061
+ window?.__startClient()
1062
+ window.__pawaHasStarted=true
1063
+ __startClient=null
1064
+ } </script>`)
1065
+ // ===== RESOLVE BATCHED ASYNC COMPONENTS =====
1066
+ let batch = store.getStore().batch;
1067
+ let maxDepth = 10; // Prevent infinite loops
1068
+ let depth = 0;
1069
+
1070
+ while (batch.length > 0 && depth < maxDepth) {
1071
+ // console.log(`Resolving batch depth ${depth}: ${batch.length} components`);
1072
+
1073
+ // Clear the batch for this iteration
1074
+ const currentBatch = [...batch];
1075
+ store.getStore().batch = [];
1076
+
1077
+ // Resolve all in parallel
1078
+ await Promise.allSettled(
1079
+ currentBatch.map(item => resolvesAsync({...item}, body, stream))
1080
+ );
1081
+
1082
+ // Check if new async components were discovered
1083
+ batch = store.getStore().batch;
1084
+ depth++;
1085
+ }
1086
+ stream(`<script>
1087
+ if (window?.__pawaDev && !window?.__pawaHasStarted && window?.__startClient !== null) {
1088
+ window?.__startClient();
1089
+ window.__pawaHasStarted = true;
1090
+ __startClient = null;
1091
+ } else {
1092
+ window.__shouldStart = true;
1093
+ }
1094
+ </script>`);
1095
+ });
1096
+ const errors=__pawaDev.errors
1097
+ const errorHtml = errors?.length ? `
1098
+ <div pawa-avoid style="position:fixed; top:0; left:0; width:100vw; height:100vh; background-color:#1a1a1a; z-index:99999; overflow-y:auto; font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; padding:2rem; box-sizing:border-box;">
1099
+ <h1 style="color:#ff6b6b; margin-top:0; border-bottom:1px solid #333; padding-bottom:1rem;">PawaJS Error Overlay</h1>
1100
+ ${errors.map((err, i) => `
1101
+ <div style="background-color:#242424; border:1px solid #333; border-radius:0.5rem; padding:1.5rem; margin-bottom:1.5rem; box-shadow:0 4px 6px -1px rgba(0, 0, 0, 0.5);">
1102
+ <div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem;">
1103
+ <h2 style="color:#e0e0e0; margin:0; font-size:1.25rem;">${err.directives || 'Runtime Error'}</h2>
1104
+ <span style="background-color:#333; color:#888; padding:0.25rem 0.5rem; border-radius:0.25rem; font-size:0.75rem;">Error #${i + 1}</span>
1105
+ </div>
1106
+
1107
+ <div style="margin-bottom:1rem;">
1108
+ <p style="color:#ff8787; margin:0; font-weight:bold;">${err.msg}</p>
1109
+ </div>
1110
+
1111
+ ${err.template ? `
1112
+ <div style="margin-bottom:1rem;">
1113
+ <h3 style="color:#888; font-size:0.875rem; text-transform:uppercase; margin:0 0 0.5rem 0;">Template Context</h3>
1114
+ <pre style="background-color:#111; color:#a8a8a8; padding:1rem; border-radius:0.25rem; overflow-x:auto; margin:0; border:1px solid #333;"><code>${err.template.replace(/</g, '&lt;').replace(/>/g, '&gt;')}</code></pre>
1115
+ </div>
1116
+ ` : ''}
1117
+
1118
+ <div>
1119
+ <h3 style="color:#888; font-size:0.875rem; text-transform:uppercase; margin:0 0 0.5rem 0;">Stack Trace</h3>
1120
+ <pre style="background-color:#111; color:#888; padding:1rem; border-radius:0.25rem; overflow-x:auto; margin:0; border:1px solid #333; font-size:0.875rem; line-height:1.5;"><code>${err.stack.split('\n').map(line => {
1121
+ const escaped = line.replace(/</g, '&lt;').replace(/>/g, '&gt;');
1122
+ return escaped.replace(/((?:[a-zA-Z]:\\|\/)[^:)]+):(\d+):(\d+)/g, (match, path, l, c) => {
1123
+ return `<a href="/__open-in-editor?file=${encodeURIComponent(path + ':' + l + ':' + c)}" onclick="event.preventDefault(); fetch(this.href);" style="color:#64b5f6; text-decoration:underline; cursor:pointer;">${match}</a>`
1124
+ })
1125
+ }).join('\n')}</code></pre>
1126
+ </div>
1127
+ </div>
1128
+ `).join('')}
1129
+ </div>
1130
+ ` : ''
1131
+ __pawaDev.errors=[]
1132
+ stream(errorHtml)
1133
+ stream(templateEnd)
1134
+ };
1135
+
1136
+ const resolvesAsync=async({component,
1137
+ id:id,
1138
+ comment:{
1139
+ encodeJSON,
1140
+ name
1141
+ },
1142
+ appContext,context,restProps,hydrate},root,stream,index)=>{
1143
+ let chunk=''
1144
+ const bufferStream=(string)=>{
1145
+ chunk+=string
1146
+ }
1147
+ bufferStream(`<div id="p${id}" hidden>`)
1148
+ store.getStore().stateContext=appContext
1149
+ const compo=await component.then((res)=>res)
1150
+ const div=root.createElement('div')
1151
+ let commentData=''
1152
+ if(typeof compo !== 'boolean' && compo){
1153
+ div.innerHTML=compo
1154
+ }
1155
+ const findElement=div.querySelector('[--]') || div.querySelector('[r-]')
1156
+ if (findElement) {
1157
+ for (const [key,value] of Object.entries(restProps)) {
1158
+ findElement.setAttribute(value.name,value.value)
1159
+ }
1160
+ findElement.removeAttribute('--')
1161
+ findElement.removeAttribute('r-')
1162
+ }
1163
+
1164
+ const newElement=div.firstElementChild
1165
+ hydrate.data=null
1166
+ if (appContext.serializeData) { // get serialized data
1167
+ for (const [key,value] of Object.entries(appContext.fromSerialized)) {
1168
+ if (typeof value !== 'function') {
1169
+ appContext.serialize[key]=value
1170
+ }
1171
+ }
1172
+ hydrate.data=appContext.serialize
1173
+ }
1174
+ hydrate.context=[]
1175
+ for (const [key] of Object.entries(appContext.insert)) {
1176
+ hydrate.context.push(key)
1177
+ }
1178
+ commentData=`component+${id}+${name}+${encodeJSON(hydrate)}`
1179
+ if (newElement) {
1180
+ await render(newElement,{context,...appContext.insert},bufferStream)
1181
+ }
1182
+ bufferStream(`</div>`)
1183
+ bufferStream(`
1184
+ <script class="p${id}">
1185
+ const s${id}=()=>{
1186
+ let p=document.querySelectorAll("#p${id}")
1187
+ if(p.length < 2){
1188
+ if(p[0]) p[0].remove()
1189
+ document.querySelector('.p${id}').remove()
1190
+ return
1191
+ }
1192
+ let c=p[0].previousSibling
1193
+ c.data='${commentData}'
1194
+ p[0].remove()
1195
+ const sc=p[0]?._stateContext
1196
+ let ec=c.nextSibling
1197
+ let fc=p[1].firstElementChild
1198
+ p[0].removeAttribute('pawa-avoid')
1199
+ Array.from(p[0].attributes).forEach(a => {
1200
+ if(a.name === 'p-async') return
1201
+ if (a.name === 'p:c') {
1202
+ let o=a.value + (fc.getAttribute('p:c') || '')
1203
+ fc.setAttribute('p:c',o)
1204
+ }else{
1205
+ fc.setAttribute(a.name,a.value)
1206
+ }
1207
+ });
1208
+ p[1].childNodes.forEach(e => {
1209
+ c.parentElement.insertBefore(e,ec)
1210
+ });
1211
+ if (window?.__pawaDev) {
1212
+ window.__pawaStream(fc,p[0]._context,sc)
1213
+ }
1214
+ p[1].remove()
1215
+ }
1216
+ s${id}()
1217
+ document.querySelector('.p${id}').remove()
1218
+ </script>
1219
+ `)
1220
+ stream(chunk)
1221
+ }
1222
+
1223
+ function streamErrorFallback(id, stream) {
1224
+ stream(`
1225
+ <div hidden id="R${id}">
1226
+ <div class="async-error">
1227
+ <p>⚠️ Failed to load content</p>
1228
+ <button onclick="location.reload()">Retry</button>
1229
+ </div>
1230
+ </div>
1231
+ <script>
1232
+ (function() {
1233
+ try {
1234
+ var p = document.getElementById("p${id}");
1235
+ var r = document.getElementById("R${id}");
1236
+ if (p && r) {
1237
+ p.innerHTML = r.innerHTML;
1238
+ p.classList.add('error-state');
1239
+ r.remove();
1240
+ }
1241
+ } catch (e) {
1242
+ console.error('Error handling fallback:', e);
1243
+ }
1244
+ })();
1245
+ </script>
1246
+ `);
1247
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pawa-ssr",
3
- "version": "1.2.5",
3
+ "version": "1.2.6",
4
4
  "type":"module",
5
5
  "description": "pawajs ssr libary",
6
6
  "main": "index.js",
package/pawaElement.js CHANGED
@@ -200,7 +200,10 @@ class PawaElement {
200
200
  return prop
201
201
  }
202
202
  `,this._context,`setting props at ${attr.name} - ${attr.value} : ${this._template}`)
203
- const name=attr.name.slice(1)
203
+ let name=attr.name.slice(1)
204
+ if(name.includes('-')){
205
+ name=name.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
206
+ }
204
207
  this._props[name]=func
205
208
  } catch (error) {
206
209
  console.log(error.message,error.stack)
package/power.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {render} from "./index.js";
2
2
  import { convertToNumber,evaluateExpr, processNode, reArrangeAttri, pawaGenerateId } from "./utils.js";
3
3
  import {parseHTML} from "linkedom"
4
- export const If = async(el, attr) => {
4
+ export const If = async(el, attr,stream) => {
5
5
  if (el._running) return;
6
6
  el._running = true;
7
7
 
@@ -68,6 +68,7 @@ let latestChain
68
68
  const store=document.createElement('template')
69
69
  chained.forEach((item,index) =>{
70
70
  const clone=item.element.cloneNode(true)
71
+ clone._avoidPawaRender = true
71
72
  if (index === 0) {
72
73
  Array.from(clone.attributes).forEach(at => {
73
74
  if (at.name.startsWith('c-')) {
@@ -103,18 +104,23 @@ let latestChain
103
104
  newElement.removeAttribute(latestChain.condition)
104
105
  newElement.setAttribute('pawa-same',true)
105
106
  endComment.parentElement.insertBefore(comment,endComment)
107
+ stream(`<!--${comment.data}-->`)
106
108
  store.setAttribute('p:store-if',id)
107
109
  store.setAttribute('p:store','')
108
110
  comment.parentElement.insertBefore(store,endComment)
111
+ stream(store.outerHTML)
109
112
  comment.parentElement.insertBefore(newElement,endComment)
110
- await render(newElement, el._context);
113
+ await render(newElement, el._context,stream);
114
+ stream(`<!--${endComment.data}-->`)
111
115
  } else {
112
116
 
113
117
  template.setAttribute('pawa-render',true)
114
118
  endComment.replaceWith(template);
119
+ stream(`${template.outerHTML}`)
120
+ // await render(template, el._context,stream);
115
121
  }
116
122
  };
117
- export const Switch = async(el, attr) => {
123
+ export const Switch = async(el, attr,stream) => {
118
124
  if (el._running) return;
119
125
  el._running = true;
120
126
 
@@ -184,6 +190,7 @@ const switchFunc=el._evaluateExpr(attr.value,el._context,`at switch directive ${
184
190
  let index=0
185
191
  chained.forEach(item =>{
186
192
  const clone=item.element.cloneNode(true)
193
+ clone._avoidPawaRender = true
187
194
  if (index === 0) {
188
195
  Array.from(clone.attributes).forEach(at => {
189
196
  if (at.name.startsWith('c-')) {
@@ -220,19 +227,26 @@ const switchFunc=el._evaluateExpr(attr.value,el._context,`at switch directive ${
220
227
  store.setAttribute('p:store-switch',id)
221
228
  store.setAttribute('p:store','')
222
229
  endComment.parentElement.insertBefore(comment,endComment)
223
- comment.parentElement.insertBefore(template,endComment)
230
+ stream(`<!--${comment.data}-->`)
231
+ comment.parentElement.insertBefore(store,endComment)
232
+ stream(store.outerHTML)
224
233
  comment.parentElement.insertBefore(newElement,endComment)
225
- await render(newElement, el._context);
234
+ await render(newElement, el._context,stream);
235
+ stream(`<!--${endComment.data}-->`)
226
236
  } else {
227
237
  // If no case matches and no default, we just leave the comments
228
238
  template.setAttribute('pawa-render',true)
229
239
  endComment.parentElement.insertBefore(comment, endComment);
240
+ stream(`${template.outerHTML}`)
241
+ // stream(`<!--${comment.data}-->`)
230
242
  comment.parentElement.insertBefore(template, endComment);
243
+ // await render(template, el._context,stream);
231
244
  // No element rendered
245
+ // stream(`<!--${endComment.data}-->`)
232
246
  }
233
247
  };
234
248
 
235
- export const For=async(el,attr)=>{
249
+ export const For=async(el,attr,stream)=>{
236
250
  if(el._running){
237
251
  return
238
252
  }
@@ -246,7 +260,6 @@ export const For=async(el,attr)=>{
246
260
  el.replaceWith(endComment)
247
261
  const hasKey=el.getAttribute('for-key')
248
262
  endComment.parentElement.insertBefore(comment,endComment)
249
-
250
263
  // More robust split using regex to find the last occurrence of ' in ' or handle simple cases
251
264
  const match = value.match(/^(.*?) in (.*)$/);
252
265
  if (!match) throw new Error(`Invalid for loop syntax: ${value}`);
@@ -265,16 +278,20 @@ export const For=async(el,attr)=>{
265
278
  })
266
279
 
267
280
  const template=document.createElement('template')
268
- template.appendChild(copyElement.cloneNode(true))
281
+ const storeClone = copyElement.cloneNode(true)
282
+ storeClone._avoidPawaRender = true
283
+ template.appendChild(storeClone)
269
284
  store.forEach(at=>{
270
285
  copyElement.setAttribute(at.name,at.value)
271
286
  })
272
287
  const componentAttr={}
273
288
  if(Array.isArray(array)){
274
289
  if (array.length > 0) {
290
+ stream(`<!--${comment.data}-->`)
275
291
  template.setAttribute('p:store-for',dirId)
276
292
  template.setAttribute('p:store','')
277
293
  endComment.parentElement.insertBefore(template,endComment)
294
+ stream(template.outerHTML)
278
295
  el.attributes.forEach(attri =>{
279
296
  if(attri.name.startsWith('c-')){
280
297
  componentAttr[attri.name]=attri.value
@@ -309,12 +326,15 @@ export const For=async(el,attr)=>{
309
326
  const endKeyComment=document.createComment(`endForKey@-$@-$@${dirId}@-$@-$@${key || index}`)
310
327
  endComment.parentElement.insertBefore(endKeyComment,endComment)
311
328
  endKeyComment.parentElement.insertBefore(keyComment,endKeyComment)
329
+ stream(`<!--${keyComment.data}-->`)
312
330
  endKeyComment.parentElement.insertBefore(newElement,endKeyComment)
313
- await render(newElement,itemContext)
331
+ await render(newElement,itemContext,stream)
332
+ stream(`<!--${endKeyComment.data}-->`)
314
333
  }
315
-
334
+ stream(`<!--${endComment.data}-->`)
316
335
  }else{
317
336
  template.setAttribute('pawa-render',true)
337
+ stream(`<template pawa-render="true">${el.outerHTML}</template>`)
318
338
  template.appendChild(el)
319
339
  comment.replaceWith(template)
320
340
  endComment.remove()
@@ -358,7 +378,7 @@ export const State=async(el,attr)=>{
358
378
 
359
379
  }
360
380
 
361
- export const Key=async(el,attr)=>{
381
+ export const Key=async(el,attr,stream)=>{
362
382
  if(el._running){
363
383
  return
364
384
  }
@@ -370,6 +390,7 @@ export const Key=async(el,attr)=>{
370
390
  const comment=document.createComment(`key`)
371
391
  const endComment=document.createComment(`key`)
372
392
  const clone=el.cloneNode(true)
393
+ clone._avoidPawaRender = true
373
394
  const template=document.createElement('template')
374
395
  template.setAttribute('p:store-key',dirId)
375
396
  template.setAttribute('p:store','')
@@ -385,11 +406,14 @@ export const Key=async(el,attr)=>{
385
406
  endComment.parentElement.insertBefore(template,endComment)
386
407
  comment.data=`key(${func})@-$@-$@${dirId}`
387
408
  endComment.data=`key(${func})@-$@-$@${dirId}`
409
+ stream(`<!--${comment.data}-->`)
410
+ stream(template.outerHTML)
388
411
  el._replaceResumeAttr('key',`c-key-${dirId}`,typeof func === 'string'?`'${func}'`:func)
389
412
  el.removeAttribute(attr.name)
390
413
  const newElement=el.cloneNode(true)
391
414
  endComment.parentElement.insertBefore(newElement,endComment)
392
- await render(newElement,el._context)
415
+ await render(newElement,el._context,stream)
416
+ stream(`<!--${endComment.data}-->`)
393
417
  }catch(error){
394
418
  console.error(error.message,error.stack)
395
419
  __pawaDev.setError({