Skip to content

Commit 0658d23

Browse files
authored
Merge pull request #8560 from QwikDev/fixes
Fixes
2 parents 9bb8b2e + 8b9b0c6 commit 0658d23

11 files changed

Lines changed: 345 additions & 49 deletions

File tree

.changeset/loose-lemons-read.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@qwik.dev/optimizer': minor
3+
---
4+
5+
FEAT: module-scoped `let` identifiers retain `let` when migrating
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
<!DOCTYPE html><html lang="en" q:render="static-ssr" q:route="/" q:container="paused" q:runtime="2" q:base="/build/" q:locale="" q:manifest-hash="[manifest]" q:instance="[instance]" :><head :><link rel="modulepreload" href="/build/q-xxxxxxxx.js" :><script async type="module" src="/build/q-xxxxxxxx.js" :></script><link rel="modulepreload" href="/build/q-xxxxxxxx.js" :><link rel="preload" href="/assets/xxxxxxxx-bundle-graph.json" as="fetch" crossorigin="anonymous" :><script type="module" async crossorigin="anonymous" :>let b=fetch("/assets/xxxxxxxx-bundle-graph.json");import("/build/q-xxxxxxxx.js").then(({l})=>l("/build/",b));</script><link rel="modulepreload" href="/build/q-xxxxxxxx.js" :><meta : charset="utf-8"><meta : name="viewport" content="width=device-width, initial-scale=1.0"><link : rel="canonical" href="https://snapshot.qwik.dev/"><title :="r5_0">Qwik Router SSG Snapshot</title><meta name="description" content="Deterministic SSG snapshot fixture" :="r5_1"><style :="ji2uyl-0" q:style="ji2uyl-0">@layer qwik{@supports selector(html:active-view-transition-type(type)){html:active-view-transition-type(qwik-navigation){:root{view-transition-name:none}}}@supports not selector(html:active-view-transition-type(type)){:root{view-transition-name:none}}}</style></head><body :><main :="q1_2"><h1 :>SSG Snapshot Fixture</h1><p : id="tags">routeLoader$ | useResource$ | useSignal</p><section : id="loader-json">{&quot;heading&quot;:&quot;SSG Snapshot Fixture&quot;,&quot;stats&quot;:[2,3,5,8],&quot;tags&quot;:[&quot;routeLoader$&quot;,&quot;useResource$&quot;,&quot;useSignal&quot;],&quot;profile&quot;:{&quot;name&quot;:&quot;router-state&quot;,&quot;status&quot;:&quot;stable&quot;}}</section><section : id="summary-json">{&quot;total&quot;:18,&quot;selectedCount&quot;:2,&quot;clicks&quot;:2}</section><section :="q1_0" id="resource-json">{&quot;headline&quot;:&quot;router-state:stable&quot;,&quot;selected&quot;:[&quot;alpha&quot;,&quot;beta&quot;],&quot;weighted&quot;:[2,6,15,32],&quot;summary&quot;:{&quot;total&quot;:18,&quot;selectedCount&quot;:2,&quot;clicks&quot;:2}}</section></main><script q-d:qinit="q-xxxxxxxx.js#_run#1" q-d:qrouterpopstate="q-xxxxxxxx.js#_run#2" q:p="3" :="Fn_1" q-d:qcinit="q-xxxxxxxx.js#_run#4"></script>[state omitted]
1+
<!DOCTYPE html><html lang="en" q:render="static-ssr" q:route="/" q:container="paused" q:runtime="2" q:base="/build/" q:locale="" q:manifest-hash="MANIFEST_HASH" q:instance="[instance]" :><head :><link rel="modulepreload" href="/build/q-xxxxxxxx.js" :><script async type="module" src="/build/q-xxxxxxxx.js" :></script><link rel="modulepreload" href="/build/q-xxxxxxxx.js" :><link rel="preload" href="/assets/xxxxxxxx-bundle-graph.json" as="fetch" crossorigin="anonymous" :><script type="module" async crossorigin="anonymous" :>let b=fetch("/assets/xxxxxxxx-bundle-graph.json");import("/build/q-xxxxxxxx.js").then(({l})=>l("/build/",b));</script><link rel="modulepreload" href="/build/q-xxxxxxxx.js" :><meta : charset="utf-8"><meta : name="viewport" content="width=device-width, initial-scale=1.0"><link : rel="canonical" href="https://snapshot.qwik.dev/"><title :="r5_0">Qwik Router SSG Snapshot</title><meta name="description" content="Deterministic SSG snapshot fixture" :="r5_1"><style :="ji2uyl-0" q:style="ji2uyl-0">@layer qwik{@supports selector(html:active-view-transition-type(type)){html:active-view-transition-type(qwik-navigation){:root{view-transition-name:none}}}@supports not selector(html:active-view-transition-type(type)){:root{view-transition-name:none}}}</style></head><body :><main :="q1_2"><h1 :>SSG Snapshot Fixture</h1><p : id="tags">routeLoader$ | useResource$ | useSignal</p><section : id="loader-json">{&quot;heading&quot;:&quot;SSG Snapshot Fixture&quot;,&quot;stats&quot;:[2,3,5,8],&quot;tags&quot;:[&quot;routeLoader$&quot;,&quot;useResource$&quot;,&quot;useSignal&quot;],&quot;profile&quot;:{&quot;name&quot;:&quot;router-state&quot;,&quot;status&quot;:&quot;stable&quot;}}</section><section : id="summary-json">{&quot;total&quot;:18,&quot;selectedCount&quot;:2,&quot;clicks&quot;:2}</section><section :="q1_0" id="resource-json">{&quot;headline&quot;:&quot;router-state:stable&quot;,&quot;selected&quot;:[&quot;alpha&quot;,&quot;beta&quot;],&quot;weighted&quot;:[2,6,15,32],&quot;summary&quot;:{&quot;total&quot;:18,&quot;selectedCount&quot;:2,&quot;clicks&quot;:2}}</section></main><script q-d:qinit="q-xxxxxxxx.js#_run#1" q-d:qrouterpopstate="q-xxxxxxxx.js#_run#2" q:p="3" :="Fn_1" q-d:qcinit="q-xxxxxxxx.js#_run#4"></script>[state omitted]
22
[vnode map omitted]
33
<script type="module" async="true" q:type="preload" :>window.addEventListener('load',f=>{f=_=>import("/build/q-xxxxxxxx.js").then(({p})=>p(["q-xxxxxxxx.js","q-xxxxxxxx.js","q-xxxxxxxx.js","q-xxxxxxxx.js","q-xxxxxxxx.js","q-xxxxxxxx.js","q-xxxxxxxx.js"]));try{requestIdleCallback(f,{timeout:2000})}catch(e){setTimeout(f,200)}})</script><script q:func="qwik/json" :>document["qFuncs_xxxxxx"]=[()=>{((w,h)=>{if(!w._qcs){w._qcs=!0;const s=h.state?._qRouterScroll;if(s){h.scrollRestoration="manual";w.scrollTo(s.x,s.y);}document.dispatchEvent(new Event("qcinit"));}})(window,history);},(p0)=>p0.url.href,(p0)=>p0.value.heading]</script><script :>(window._qwikEv||(window._qwikEv=[])).push("d:qinit","d:qrouterpopstate","d:qcinit")</script></body></html>

e2e/qwik-e2e/apps/qwikrouter-ssg-snapshot/expected.state.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
{string} "frontmatter"
4141
Object 0
4242
{string} "manifestHash"
43-
{string} "[manifest]"
43+
{string} "MANIFEST_HASH"
4444
]
4545
{number} 1
4646
Map [

e2e/qwik-e2e/tests/qwikrouter/ssg-snapshot.e2e.ts

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,22 +28,26 @@ test.describe('router ssg snapshot', () => {
2828
const state = extractState(html);
2929
expect(state).not.toBeNull();
3030

31-
const normalizedHtml = normalizeHtml(html);
31+
const manifestHash = extractManifestHash(html);
32+
const normalizedHtml = normalizeHtml(html, manifestHash);
3233
const normalizedState = normalizeStateDump(
33-
_dumpState(JSON.parse(state!) as unknown[], false, '', null)
34+
_dumpState(JSON.parse(state!) as unknown[], false, '', null),
35+
manifestHash
3436
);
3537

3638
if (process.env.UPDATE_SSG_SNAPSHOT === '1') {
3739
await writeFile(expectedHtmlPath, normalizedHtml, 'utf-8');
3840
await writeFile(expectedStatePath, normalizedState, 'utf-8');
3941
}
4042

41-
expect(normalizedHtml).toEqual(
42-
(await readFile(expectedHtmlPath, 'utf-8')).replace(/\r\n/g, '\n')
43-
);
44-
expect(normalizedState).toEqual(
45-
(await readFile(expectedStatePath, 'utf-8')).replace(/\r\n/g, '\n')
46-
);
43+
const expectedHtml = (await readFile(expectedHtmlPath, 'utf-8')).replace(/\r\n/g, '\n');
44+
const expectedState = (await readFile(expectedStatePath, 'utf-8')).replace(/\r\n/g, '\n');
45+
46+
warnIfSizeChanged('readable state dump', expectedState, normalizedState);
47+
warnIfSizeChanged('HTML', expectedHtml, normalizedHtml);
48+
49+
expect(normalizedState).toEqual(expectedState);
50+
expect(normalizedHtml).toEqual(expectedHtml);
4751
});
4852
});
4953

@@ -104,31 +108,59 @@ async function buildFixtureApp() {
104108
);
105109
}
106110

107-
function normalizeHtml(html: string) {
108-
return html
111+
function extractManifestHash(html: string): string | null {
112+
const match = html.match(/q:manifest-hash="([^"]*)"/);
113+
return match ? match[1] : null;
114+
}
115+
116+
function normalizeHtml(html: string, manifestHash: string | null) {
117+
let result = html;
118+
if (manifestHash) {
119+
result = result.replaceAll(manifestHash, 'MANIFEST_HASH');
120+
}
121+
result = result
109122
.replace(/\r\n/g, '\n')
110123
.replace(/ q:version="[^"]*"/g, '')
111124
.replace(/ q:instance="[^"]*"/g, ' q:instance="[instance]"')
112-
.replace(/ q:manifest-hash="[^"]*"/g, ' q:manifest-hash="[manifest]"')
113125
.replace(/\/assets\/[A-Za-z0-9_-]+-bundle-graph\.json/g, '/assets/xxxxxxxx-bundle-graph.json')
114126
.replace(/q-[A-Za-z0-9_-]+\.(js|css)/g, 'q-xxxxxxxx.$1')
115127
.replace(/qFuncs_[A-Za-z0-9_-]+/g, 'qFuncs_xxxxxx')
116128
.replace(/<script type="qwik\/state"[^>]*>[\s\S]*?<\/script>/, '[state omitted]\n')
117129
.replace(/<script type="qwik\/vnode"[^>]*>[\s\S]*?<\/script>/, '[vnode map omitted]\n');
130+
return result;
118131
}
119132

120133
function extractState(html: string) {
121134
const match = html.match(/<script type="qwik\/state"[^>]*>([\s\S]*?)<\/script>/);
122135
return match ? match[1] : null;
123136
}
124137

125-
function normalizeStateDump(stateDump: string) {
126-
return stateDump
138+
function normalizeStateDump(stateDump: string, manifestHash: string | null) {
139+
let result = stateDump
127140
.replace(/\r\n/g, '\n')
128-
.replace(/manifestHash"\n\s+\{string\} "[^"]+"/g, 'manifestHash"\n {string} "[manifest]"')
141+
.replace(/manifestHash"\n\s+\{string\} "[^"]+"/g, 'manifestHash"\n {string} "MANIFEST_HASH"')
129142
.replace(/qFuncs_[A-Za-z0-9_-]+/g, 'qFuncs_xxxxxx')
130143
.replace(/q-[A-Za-z0-9_-]+\.(js|css)/g, 'q-xxxxxxxx.$1')
131144
.replaceAll(/RootRef .*/g, 'RootRef [omitted]')
132145
.replaceAll(/QRL ".*"/g, 'QRL "[omitted]"')
133146
.replace(/^\(\d+ chars\)$/m, '');
147+
if (manifestHash) {
148+
result = result.replaceAll(manifestHash, 'MANIFEST_HASH');
149+
}
150+
return result;
151+
}
152+
153+
function warnIfSizeChanged(label: string, expected: string, actual: string) {
154+
const expectedLen = expected.length;
155+
const actualLen = actual.length;
156+
if (expectedLen === 0) {
157+
return;
158+
}
159+
const pct = Math.abs(actualLen - expectedLen) / expectedLen;
160+
if (pct > 0.01) {
161+
console.error(
162+
`\n\n[ssg-snapshot] ${label} size changed by ${(pct * 100).toFixed(1)}%: ` +
163+
`${expectedLen}${actualLen} chars`
164+
);
165+
}
134166
}

packages/optimizer/core/src/dependency_analysis.rs

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ pub struct RootVarDependency {
2121
pub is_imported: bool, // true if this is from global.imports
2222
pub is_exported: bool, // true if this is in global.exports
2323
pub depends_on: Vec<Id>, // Other identifiers this var depends on
24+
/// Original declaration kind for `Decl::Var` entries (`let`/`var`/`const`).
25+
/// `None` for function/class/enum declarations or synthesized entries.
26+
pub var_kind: Option<ast::VarDeclKind>,
2427
}
2528

2629
/// Analyzes dependencies between root-level variables and imports.
@@ -112,7 +115,14 @@ pub fn analyze_root_dependencies(
112115

113116
// First pass: collect ALL root declarations from the module body
114117
for item in &module.body {
115-
if let ast::ModuleItem::Stmt(ast::Stmt::Decl(decl)) = item {
118+
let decl = match item {
119+
ast::ModuleItem::Stmt(ast::Stmt::Decl(decl)) => Some(decl),
120+
ast::ModuleItem::ModuleDecl(ast::ModuleDecl::ExportDecl(export_decl)) => {
121+
Some(&export_decl.decl)
122+
}
123+
_ => None,
124+
};
125+
if let Some(decl) = decl {
116126
match decl {
117127
ast::Decl::Var(var_decl) => {
118128
for decl in &var_decl.decls {
@@ -139,6 +149,7 @@ pub fn analyze_root_dependencies(
139149
is_imported: false,
140150
is_exported: user_exported.contains(&var_id),
141151
depends_on: Vec::new(),
152+
var_kind: Some(var_decl.kind),
142153
},
143154
);
144155
}
@@ -154,6 +165,7 @@ pub fn analyze_root_dependencies(
154165
is_imported: false,
155166
is_exported: user_exported.contains(&var_id),
156167
depends_on: Vec::new(),
168+
var_kind: None,
157169
});
158170
}
159171
ast::Decl::Class(class) => {
@@ -165,6 +177,7 @@ pub fn analyze_root_dependencies(
165177
is_imported: false,
166178
is_exported: user_exported.contains(&var_id),
167179
depends_on: Vec::new(),
180+
var_kind: None,
168181
});
169182
}
170183
ast::Decl::TsEnum(enu) => {
@@ -176,6 +189,7 @@ pub fn analyze_root_dependencies(
176189
is_imported: false,
177190
is_exported: user_exported.contains(&var_id),
178191
depends_on: Vec::new(),
192+
var_kind: None,
179193
});
180194
}
181195
_ => {}
@@ -197,6 +211,7 @@ pub fn analyze_root_dependencies(
197211
init: None,
198212
definite: false,
199213
}),
214+
var_kind: None,
200215
is_imported: false,
201216
is_exported: user_exported.contains(var_id),
202217
depends_on: Vec::new(),
@@ -205,7 +220,14 @@ pub fn analyze_root_dependencies(
205220

206221
// Second pass: extract variable declarations and analyze dependencies
207222
for item in &module.body {
208-
if let ast::ModuleItem::Stmt(ast::Stmt::Decl(decl)) = item {
223+
let decl = match item {
224+
ast::ModuleItem::Stmt(ast::Stmt::Decl(decl)) => Some(decl),
225+
ast::ModuleItem::ModuleDecl(ast::ModuleDecl::ExportDecl(export_decl)) => {
226+
Some(&export_decl.decl)
227+
}
228+
_ => None,
229+
};
230+
if let Some(decl) = decl {
209231
match decl {
210232
ast::Decl::Var(var_decl) => {
211233
for decl in &var_decl.decls {
@@ -593,3 +615,55 @@ fn collect_decl_idents_from_decl(decl: &ast::Decl, idents: &mut Vec<Id>) {
593615
_ => {}
594616
}
595617
}
618+
619+
#[cfg(test)]
620+
mod tests {
621+
use super::*;
622+
use crate::collector::global_collect;
623+
use swc_atoms::atom;
624+
use swc_common::{SyntaxContext, DUMMY_SP};
625+
626+
#[test]
627+
fn analyze_root_dependencies_tracks_export_decl_var_kind() {
628+
let id = ast::Ident::new(atom!("counter"), DUMMY_SP, SyntaxContext::empty());
629+
let module = ast::Module {
630+
span: DUMMY_SP,
631+
shebang: None,
632+
body: vec![ast::ModuleItem::ModuleDecl(ast::ModuleDecl::ExportDecl(
633+
ast::ExportDecl {
634+
span: DUMMY_SP,
635+
decl: ast::Decl::Var(Box::new(ast::VarDecl {
636+
span: DUMMY_SP,
637+
ctxt: SyntaxContext::empty(),
638+
kind: ast::VarDeclKind::Let,
639+
declare: false,
640+
decls: vec![ast::VarDeclarator {
641+
span: DUMMY_SP,
642+
name: ast::Pat::Ident(ast::BindingIdent { id, type_ann: None }),
643+
init: Some(Box::new(ast::Expr::Lit(ast::Lit::Num(ast::Number {
644+
span: DUMMY_SP,
645+
value: 0.0,
646+
raw: None,
647+
})))),
648+
definite: false,
649+
}],
650+
})),
651+
},
652+
))],
653+
};
654+
655+
let collect = global_collect(&ast::Program::Module(module.clone()));
656+
let deps = analyze_root_dependencies(&module, &collect);
657+
let dep = deps
658+
.get(&(atom!("counter"), SyntaxContext::empty()))
659+
.expect("counter dependency should exist");
660+
661+
assert_eq!(dep.var_kind, Some(ast::VarDeclKind::Let));
662+
match &dep.decl {
663+
RootVarDecl::Var(decl) => {
664+
assert!(decl.init.is_some());
665+
}
666+
_ => panic!("expected var declarator"),
667+
}
668+
}
669+
}

packages/optimizer/core/src/parse.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1043,9 +1043,10 @@ fn apply_variable_migration(
10431043
if let Some(items) = create_cyclic_var_items(decl) {
10441044
unique_module_items.extend(items);
10451045
} else {
1046+
let kind = dep_info.var_kind.unwrap_or(ast::VarDeclKind::Const);
10461047
let var_decl = ast::VarDecl {
10471048
span: swc_common::DUMMY_SP,
1048-
kind: ast::VarDeclKind::Const,
1049+
kind,
10491050
decls: vec![decl.clone()],
10501051
declare: false,
10511052
ctxt: swc_common::SyntaxContext::empty(),
@@ -1056,9 +1057,10 @@ fn apply_variable_migration(
10561057
}
10571058
}
10581059
RootVarDecl::Var(decl) => {
1060+
let kind = dep_info.var_kind.unwrap_or(ast::VarDeclKind::Const);
10591061
let var_decl = ast::VarDecl {
10601062
span: swc_common::DUMMY_SP,
1061-
kind: ast::VarDeclKind::Const,
1063+
kind,
10621064
decls: vec![decl.clone()],
10631065
declare: false,
10641066
ctxt: swc_common::SyntaxContext::empty(),
@@ -1738,6 +1740,7 @@ mod migration_cleanup_tests {
17381740
decl: mk_decl("a"),
17391741
is_imported: false,
17401742
is_exported: false,
1743+
var_kind: None,
17411744
depends_on: vec![id_a.clone()],
17421745
},
17431746
);
@@ -1747,6 +1750,7 @@ mod migration_cleanup_tests {
17471750
decl: mk_decl("b"),
17481751
is_imported: false,
17491752
is_exported: false,
1753+
var_kind: None,
17501754
depends_on: vec![id_c.clone()],
17511755
},
17521756
);
@@ -1756,6 +1760,7 @@ mod migration_cleanup_tests {
17561760
decl: mk_decl("c"),
17571761
is_imported: false,
17581762
is_exported: false,
1763+
var_kind: None,
17591764
depends_on: vec![id_b.clone()],
17601765
},
17611766
);
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
---
2+
source: packages/optimizer/core/src/test.rs
3+
assertion_line: 7318
4+
expression: output
5+
---
6+
==INPUT==
7+
8+
9+
import { $ } from '@qwik.dev/core';
10+
11+
let counter = 0;
12+
13+
export function bumpFromRoot() {
14+
counter++;
15+
}
16+
17+
export const readFromSegment = $(() => {
18+
console.log(counter);
19+
});
20+
21+
============================= test.js ==
22+
23+
import { qrl } from "@qwik.dev/core";
24+
//
25+
const q_readFromSegment_gyZ53wYYLXs = /*#__PURE__*/ qrl(()=>import("./test.tsx_readFromSegment_gyZ53wYYLXs"), "readFromSegment_gyZ53wYYLXs");
26+
//
27+
let counter = 0;
28+
export function bumpFromRoot() {
29+
counter++;
30+
}
31+
export const readFromSegment = q_readFromSegment_gyZ53wYYLXs;
32+
export { counter as _auto_counter };
33+
34+
35+
Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;AAGA,IAAI,UAAU;AAEd,OAAO,SAAS;IACd;AACF;AAEA,OAAO,MAAM,gDAEV\"}")
36+
============================= test.tsx_readFromSegment_gyZ53wYYLXs.js (ENTRY POINT)==
37+
38+
import { _auto_counter as counter } from "./test";
39+
//
40+
export const readFromSegment_gyZ53wYYLXs = ()=>{
41+
console.log(counter);
42+
};
43+
44+
45+
Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;2CASiC;IAC/B,QAAQ,GAAG,CAAC;AACd\"}")
46+
/*
47+
{
48+
"origin": "test.tsx",
49+
"name": "readFromSegment_gyZ53wYYLXs",
50+
"entry": null,
51+
"displayName": "test.tsx_readFromSegment",
52+
"hash": "gyZ53wYYLXs",
53+
"canonicalFilename": "test.tsx_readFromSegment_gyZ53wYYLXs",
54+
"path": "",
55+
"extension": "js",
56+
"parent": null,
57+
"ctxKind": "function",
58+
"ctxName": "$",
59+
"captures": false,
60+
"loc": [
61+
139,
62+
172
63+
]
64+
}
65+
*/
66+
== DIAGNOSTICS ==
67+
68+
[]

0 commit comments

Comments
 (0)