Skip to content

Commit a3f345a

Browse files
chore: wip
1 parent c2fe0d4 commit a3f345a

File tree

5 files changed

+403
-19
lines changed

5 files changed

+403
-19
lines changed

packages/router/src/client.ts

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,21 +99,49 @@ else {
9999
var curStyles=document.querySelectorAll('head style');
100100
var newStyles=doc.querySelectorAll('head style');
101101
102+
// Merge crosswind styles instead of replacing — persistent elements
103+
// (nav, footer) outside <main> still need their utility classes
104+
var curCrosswind=document.querySelector('head style[data-crosswind]');
105+
var newCrosswind=null;
106+
newStyles.forEach(function(s){if(s.getAttribute('data-crosswind'))newCrosswind=s});
107+
108+
if(curCrosswind&&newCrosswind){
109+
// Parse existing rules to avoid duplicates
110+
var existing=curCrosswind.textContent||'';
111+
var incoming_css=newCrosswind.textContent||'';
112+
// Extract individual rule blocks from new CSS
113+
var newRules=[];
114+
incoming_css.replace(/([^{}]+\\{[^{}]*\\})/g,function(m){
115+
if(existing.indexOf(m.trim())===-1)newRules.push(m);
116+
return m;
117+
});
118+
if(newRules.length>0){
119+
curCrosswind.textContent=existing+'\\n'+newRules.join('\\n');
120+
}
121+
}
122+
102123
var incoming=[];
103124
newStyles.forEach(function(s){
104-
if(!keepIds[s.id]){
125+
if(!keepIds[s.id]&&!s.getAttribute('data-crosswind')){
105126
var ns=document.createElement('style');
106127
ns.textContent=s.textContent;
107128
ns.setAttribute('data-stx-incoming','');
108-
if(s.getAttribute('data-crosswind'))ns.setAttribute('data-crosswind',s.getAttribute('data-crosswind'));
109129
document.head.appendChild(ns);
110130
incoming.push(ns);
111131
}
112132
});
113133
114-
// Remove old styles (except persistent ones and incoming)
134+
// If no existing crosswind but new page has one, add it
135+
if(!curCrosswind&&newCrosswind){
136+
var ns=document.createElement('style');
137+
ns.textContent=newCrosswind.textContent;
138+
ns.setAttribute('data-crosswind',newCrosswind.getAttribute('data-crosswind'));
139+
document.head.appendChild(ns);
140+
}
141+
142+
// Remove old styles (except persistent ones, crosswind, and incoming)
115143
curStyles.forEach(function(s){
116-
if(!keepIds[s.id]&&!s.hasAttribute('data-stx-incoming'))s.remove();
144+
if(!keepIds[s.id]&&!s.hasAttribute('data-stx-incoming')&&!s.hasAttribute('data-crosswind'))s.remove();
117145
});
118146
119147
incoming.forEach(function(s){s.removeAttribute('data-stx-incoming')});

packages/stx/src/events.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,7 +578,11 @@ export function processEventDirectives(
578578
}
579579

580580
// SFC mode: collect bindings, don't generate standalone script
581+
// But skip if template has x-data scopes - reactive system handles events in those
581582
if (context.__stx_sfc_mode) {
583+
if (/x-data\s*=\s*["']/.test(template)) {
584+
return template
585+
}
582586
const { template: processed, bindings } = extractAndProcessEvents(template)
583587
const existing = (context.__stx_event_bindings || []) as ParsedEvent[]
584588
context.__stx_event_bindings = [...existing, ...bindings]

packages/stx/src/process.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,74 @@ export function injectRouterScript(template: string): string {
669669
return template.slice(0, bodyCloseIdx) + routerScript + '\n</body>' + template.slice(bodyCloseIdx + 7)
670670
}
671671

672+
/**
673+
* Add x-cloak to HTML elements that contain unresolved {{ }} expressions.
674+
* These expressions were preserved for client-side evaluation (signals, loop vars, etc.).
675+
* x-cloak hides them until the JS runtime processes and reveals them — prevents FOUC.
676+
*/
677+
function addCloakToUnresolvedExpressions(html: string): string {
678+
// Add x-cloak to elements whose text content contains {{ }} expressions.
679+
// We parse tag boundaries carefully to avoid breaking attributes that contain ">".
680+
const tagNames = 'div|span|p|h[1-6]|td|th|li|a|button|label|section|article|header|footer|main|nav|aside|dd|dt|figcaption|summary|caption|blockquote|pre|code|em|strong|small|sub|sup|time|mark|abbr|cite|q|s|u|b|i'
681+
const tagStartRe = new RegExp(`<(${tagNames})\\b`, 'gi')
682+
let result = ''
683+
let lastIndex = 0
684+
let tagMatch: RegExpExecArray | null
685+
686+
// eslint-disable-next-line no-cond-assign
687+
while ((tagMatch = tagStartRe.exec(html)) !== null) {
688+
const tagOpenStart = tagMatch.index
689+
690+
// Find the real closing ">" of this opening tag, skipping ">" inside quoted attributes
691+
let i = tagOpenStart + tagMatch[0].length
692+
let inSingleQuote = false
693+
let inDoubleQuote = false
694+
let tagCloseIdx = -1
695+
696+
while (i < html.length) {
697+
const ch = html[i]
698+
if (inSingleQuote) {
699+
if (ch === '\'') inSingleQuote = false
700+
}
701+
else if (inDoubleQuote) {
702+
if (ch === '"') inDoubleQuote = false
703+
}
704+
else if (ch === '\'') {
705+
inSingleQuote = true
706+
}
707+
else if (ch === '"') {
708+
inDoubleQuote = true
709+
}
710+
else if (ch === '>') {
711+
tagCloseIdx = i
712+
break
713+
}
714+
i++
715+
}
716+
717+
if (tagCloseIdx === -1) continue
718+
719+
const fullTag = html.slice(tagOpenStart, tagCloseIdx + 1)
720+
721+
// Skip if already has x-cloak
722+
if (fullTag.includes('x-cloak')) continue
723+
724+
// Look at text content after this tag until the next "</" or "<" tag
725+
const afterTag = html.slice(tagCloseIdx + 1)
726+
const closingIdx = afterTag.indexOf('</')
727+
const textContent = closingIdx >= 0 ? afterTag.slice(0, closingIdx) : afterTag.slice(0, 200)
728+
729+
if (/\{\{[\s\S]*?\}\}/.test(textContent)) {
730+
// Insert x-cloak before the closing >
731+
result += html.slice(lastIndex, tagCloseIdx) + ' x-cloak>'
732+
lastIndex = tagCloseIdx + 1
733+
}
734+
}
735+
736+
result += html.slice(lastIndex)
737+
return result
738+
}
739+
672740
/**
673741
* Process STX signals in template.
674742
* Handles script setup, runtime injection, and transforms.
@@ -1911,6 +1979,11 @@ async function processOtherDirectives(
19111979
// Process expressions now (delayed to allow other directives to generate expressions)
19121980
output = await processExpressions(output, context, filePath)
19131981

1982+
// Add x-cloak to elements containing unresolved {{ }} expressions (preserved for client-side).
1983+
// This prevents FOUC — raw mustache syntax flashing before the JS runtime processes them.
1984+
// The signals runtime removes x-cloak after binding, identical to Vue's v-cloak approach.
1985+
output = addCloakToUnresolvedExpressions(output)
1986+
19141987
// Process reactive bindings (:class, :text, stx-class, stx-bind:class, etc.)
19151988
// This generates client-side JS for store-aware reactive updates
19161989
output = processTemplateBindings(output)

0 commit comments

Comments
 (0)