Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/qrl-captures-singleton.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@qwik.dev/core': patch
---

BREAKING (only when using internal v2 beta API): The `_captures` variable is now a singleton object called `_capturesObj` with a single property `_` that contains the captures string. Normally this should not impact you.
7 changes: 7 additions & 0 deletions .changeset/shared-singletons-global.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@qwik.dev/core': patch
---

FIX: Move singletons to `globalThis.__qwik__`. Same-version coexistence is fine and gets to share the singleton state. On the server, only allow one Qwik version per process.

This is a necessary step to allow Qwik third party libraries to stay external on the server.
4 changes: 2 additions & 2 deletions e2e/adapters-e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@
"build.server.deno": "vite build -c adapters/deno/vite.config.ts",
"build.server.express": "vite build -c adapters/express/vite.config.ts",
"build.types": "tsc --incremental --noEmit",
"bun": "pnpm build.runtime.bun && pnpm serve.bun",
"deno": "pnpm build.runtime.deno && pnpm serve.deno",
"deploy": "vercel deploy",
"dev": "vite --mode ssr",
"dev.debug": "node --inspect-brk ./node_modules/vite/bin/vite.js --mode ssr --force",
"bun": "pnpm build.runtime.bun && pnpm serve.bun",
"deno": "pnpm build.runtime.deno && pnpm serve.deno",
"express": "pnpm build.runtime.express && pnpm serve.express",
"fmt": "prettier --write .",
"fmt.check": "prettier --check .",
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -234,9 +234,9 @@
"lint.prettier": "prettier --cache --check .",
"lint.rust": "make lint",
"lint.syncpack": "syncpack list-mismatches",
"pack.core": "node --require ./scripts/runBefore.ts scripts/pack-local-qwik-core.ts",
"preinstall": "npx only-allow pnpm",
"prepare": "simple-git-hooks",
"pack.core": "node --require ./scripts/runBefore.ts scripts/pack-local-qwik-core.ts",
"prettier.fix": "prettier --cache --write .",
"qwik-push-build-repos": "node --require ./scripts/runBefore.ts ./scripts/qwik-push-build-repos.ts",
"release": "changeset publish",
Expand Down Expand Up @@ -264,9 +264,9 @@
"test.e2e.firefox": "playwright test e2e/qwik-e2e/tests --browser=firefox --config e2e/qwik-e2e/playwright.config.ts",
"test.e2e.integrations.bun.chromium": "playwright test e2e/adapters-e2e/tests --project=chromium --config e2e/adapters-e2e/playwright.bun.config.ts",
"test.e2e.integrations.bun.webkit": "playwright test e2e/adapters-e2e/tests --project=webkit --config e2e/adapters-e2e/playwright.bun.config.ts",
"test.e2e.integrations.chromium": "playwright test e2e/adapters-e2e/tests --project=chromium --config e2e/adapters-e2e/playwright.config.ts",
"test.e2e.integrations.deno.chromium": "playwright test e2e/adapters-e2e/tests --project=chromium --config e2e/adapters-e2e/playwright.deno.config.ts",
"test.e2e.integrations.deno.webkit": "playwright test e2e/adapters-e2e/tests --project=webkit --config e2e/adapters-e2e/playwright.deno.config.ts",
"test.e2e.integrations.chromium": "playwright test e2e/adapters-e2e/tests --project=chromium --config e2e/adapters-e2e/playwright.config.ts",
"test.e2e.integrations.webkit": "playwright test e2e/adapters-e2e/tests --project=webkit --config e2e/adapters-e2e/playwright.config.ts",
"test.e2e.qwik-react.chromium": "playwright test e2e/qwik-react-e2e/tests --project=chromium --config e2e/qwik-react-e2e/playwright.config.ts",
"test.e2e.qwik-react.webkit": "playwright test e2e/qwik-react-e2e/tests --project=webkit --config e2e/qwik-react-e2e/playwright.config.ts",
Expand Down
2 changes: 1 addition & 1 deletion packages/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@
"gray-matter": "4.0.3",
"leaflet": "1.9.4",
"magic-string": "0.30.21",
"playwright": "1.57.0",
"pagefind": "1.4.0",
"playwright": "1.57.0",
"prettier": "3.7.4",
"prism-themes": "1.9.0",
"prismjs": "1.30.0",
Expand Down
4 changes: 2 additions & 2 deletions packages/docs/src/routes/api/qwik/api.json
Original file line number Diff line number Diff line change
Expand Up @@ -2786,7 +2786,7 @@
}
],
"kind": "Function",
"content": "Reruns the `taskFn` when the observed inputs change.\n\nUse `useTask` to observe changes on a set of inputs, and then re-execute the `taskFn` when those inputs change.\n\nThe `taskFn` only executes if the observed inputs change. To observe the inputs, use the `obs` function to wrap property reads. This creates subscriptions that will trigger the `taskFn` to rerun.\n\n\n```typescript\nuseTask$: (fn: TaskFn, opts?: TaskOptions) => void\n```\n\n\n<table><thead><tr><th>\n\nParameter\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\nfn\n\n\n</td><td>\n\n[TaskFn](#taskfn)\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\nopts\n\n\n</td><td>\n\n[TaskOptions](#taskoptions)\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n</tbody></table>\n\n**Returns:**\n\nvoid",
"content": "Reruns the `taskFn` when the observed inputs change.\n\nUse `useTask` to observe changes on a set of inputs, and then re-execute the `taskFn` when those inputs change.\n\nThe `taskFn` only executes if the observed inputs change. To observe the inputs, use the `obs` function to wrap property reads. This creates subscriptions that will trigger the `taskFn` to rerun.\n\nCleanup callbacks registered with `cleanup()` or returned from the task may be async. When a task reruns, Qwik waits for the previous cleanup to finish before starting the next invocation.\n\nDuring SSR, the cleanup function is called immediately after SSR completes. Therefore, it is not called on the client side after resuming, but only the second time the task runs on the client.\n\n\n```typescript\nuseTask$: (fn: TaskFn, opts?: TaskOptions) => void\n```\n\n\n<table><thead><tr><th>\n\nParameter\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\nfn\n\n\n</td><td>\n\n[TaskFn](#taskfn)\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\nopts\n\n\n</td><td>\n\n[TaskOptions](#taskoptions)\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n</tbody></table>\n\n**Returns:**\n\nvoid",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-task-dollar.ts",
"mdFile": "core.usetask_.md"
},
Expand All @@ -2800,7 +2800,7 @@
}
],
"kind": "Function",
"content": "```tsx\nconst Timer = component$(() => {\n const store = useStore({\n count: 0,\n });\n\n useVisibleTask$(() => {\n // Only runs in the client\n const timer = setInterval(() => {\n store.count++;\n }, 500);\n return () => {\n clearInterval(timer);\n };\n });\n\n return <div>{store.count}</div>;\n});\n```\n\n\n```typescript\nuseVisibleTask$: (fn: TaskFn, opts?: OnVisibleTaskOptions) => void\n```\n\n\n<table><thead><tr><th>\n\nParameter\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\nfn\n\n\n</td><td>\n\n[TaskFn](#taskfn)\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\nopts\n\n\n</td><td>\n\n[OnVisibleTaskOptions](#onvisibletaskoptions)\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n</tbody></table>\n\n**Returns:**\n\nvoid",
"content": "```tsx\nconst Timer = component$(() => {\n const store = useStore({\n count: 0,\n });\n\n useVisibleTask$(() => {\n // Only runs in the client\n const timer = setInterval(() => {\n store.count++;\n }, 500);\n return () => {\n clearInterval(timer);\n };\n });\n\n return <div>{store.count}</div>;\n});\n```\nVisible Tasks are a variant of Tasks that only run in the browser, and are registered but not executed during SSR. They are useful for running code that should only execute in the browser, such as code that interacts with the DOM or browser APIs.\n\nCleanup callbacks registered with `cleanup()` or returned from the task may be async. When a visible task reruns, Qwik waits for the previous cleanup to finish before starting the next invocation.\n\n\n```typescript\nuseVisibleTask$: (fn: TaskFn, opts?: OnVisibleTaskOptions) => void\n```\n\n\n<table><thead><tr><th>\n\nParameter\n\n\n</th><th>\n\nType\n\n\n</th><th>\n\nDescription\n\n\n</th></tr></thead>\n<tbody><tr><td>\n\nfn\n\n\n</td><td>\n\n[TaskFn](#taskfn)\n\n\n</td><td>\n\n\n</td></tr>\n<tr><td>\n\nopts\n\n\n</td><td>\n\n[OnVisibleTaskOptions](#onvisibletaskoptions)\n\n\n</td><td>\n\n_(Optional)_\n\n\n</td></tr>\n</tbody></table>\n\n**Returns:**\n\nvoid",
"editUrl": "https://github.com/QwikDev/qwik/tree/main/packages/qwik/src/core/use/use-visible-task-dollar.ts",
"mdFile": "core.usevisibletask_.md"
},
Expand Down
8 changes: 8 additions & 0 deletions packages/docs/src/routes/api/qwik/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10458,6 +10458,10 @@ Use `useTask` to observe changes on a set of inputs, and then re-execute the `ta

The `taskFn` only executes if the observed inputs change. To observe the inputs, use the `obs` function to wrap property reads. This creates subscriptions that will trigger the `taskFn` to rerun.

Cleanup callbacks registered with `cleanup()` or returned from the task may be async. When a task reruns, Qwik waits for the previous cleanup to finish before starting the next invocation.

During SSR, the cleanup function is called immediately after SSR completes. Therefore, it is not called on the client side after resuming, but only the second time the task runs on the client.

```typescript
useTask$: (fn: TaskFn, opts?: TaskOptions) => void
```
Expand Down Expand Up @@ -10529,6 +10533,10 @@ const Timer = component$(() => {
});
```

Visible Tasks are a variant of Tasks that only run in the browser, and are registered but not executed during SSR. They are useful for running code that should only execute in the browser, such as code that interacts with the DOM or browser APIs.

Cleanup callbacks registered with `cleanup()` or returned from the task may be async. When a visible task reruns, Qwik waits for the previous cleanup to finish before starting the next invocation.

```typescript
useVisibleTask$: (fn: TaskFn, opts?: OnVisibleTaskOptions) => void
```
Expand Down
12 changes: 6 additions & 6 deletions packages/optimizer/core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ useTask$(() => {
useTaskQrl(qrl(() => import('./myFile_useTask_abc123'), 's_abc123', [state]));

// Output (segment module: myFile_useTask_abc123.js)
import { _captures } from '@qwik.dev/core';
import { _capturesObj } from '@qwik.dev/core';
export const s_abc123 = () => {
const state = _captures[0];
const state = _capturesObj._[0];
console.log(state.count);
};
```
Expand All @@ -32,7 +32,7 @@ export const s_abc123 = () => {
A segment is an extracted closure with metadata. Each segment becomes a separate ES module file (in non-inline strategies). The segment module contains:

1. Imports for all externally-referenced identifiers
2. A `_captures` import if the closure captures lexical variables
2. A `_capturesObj` import if the closure captures lexical variables
3. The closure body as a named export

### Captures (Scoped Identifiers)
Expand All @@ -41,7 +41,7 @@ When a `$`-closure references variables from its enclosing lexical scope (not im

1. Identifies captured variables by walking the closure body and checking each identifier against the lexical scope stack
2. Passes them as an array argument to `qrl()`: `qrl(import, "name", [var1, var2])`
3. In the segment module, rewrites the function to read captures from `_captures`: `const var1 = _captures[0]`
3. In the segment module, rewrites the function to read captures from `_capturesObj`: `const var1 = _capturesObj._[0]`

**Exception — event handlers on native elements:** For `$`-props on native elements (e.g., `onClick$` on `<button>`), captures are instead lifted to `q:p`/`q:ps` props on the element and injected as extra function parameters. This removes the capture array from the QRL, allowing it to be hoisted to module scope. See "Event handler capture lifting" below.

Expand Down Expand Up @@ -176,13 +176,13 @@ The optimizer processes each source file through these phases in order:

15. **Build segment modules** — For each extracted segment, builds a standalone ES module:
- Resolves imports: identifiers referencing source-file imports get corresponding import declarations; identifiers referencing source-file exports get `import { _auto_name } from "./sourceFile"`
- Adds `_captures` import and rewrites function parameters if the segment has captures
- Adds `_capturesObj` import and rewrites function parameters if the segment has captures
- Includes hoisted QRL consts and migrated root variables
- Topologically sorts all declarations

16. **Segment DCE** — Runs DCE on each segment module to remove unused imports and dead code.

17. **Empty segment detection** — After DCE, checks if a segment's exported function body is empty (no statements, only `_captures[N]` accesses, `() => undefined`, or `() => void 0`). Empty segments are not emitted.
17. **Empty segment detection** — After DCE, checks if a segment's exported function body is empty (no statements, only `_capturesObj._[N]` accesses, `() => undefined`, or `() => void 0`). Empty segments are not emitted.

18. **Noop QRL replacement** — In all remaining modules (root + non-empty segments), `qrl()` calls referencing empty segments are replaced with `_noopQrl()`. The captures array is preserved. Unused import arrows (`i_*` consts) and QRL declarations (`_qrl_*` consts) are cleaned up.

Expand Down
47 changes: 28 additions & 19 deletions packages/optimizer/core/src/code_move.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,20 +132,21 @@ pub fn new_module(ctx: NewModuleCtx) -> Result<(ast::Module, SingleThreadedComme
};

let has_scoped_idents = ctx.need_transform && !ctx.scoped_idents.is_empty();
let _captures = if has_scoped_idents {
let new_local = id!(private_ident!(&*_CAPTURES.clone()));
module
.body
.push(create_synthetic_named_import(&new_local, ctx.core_module));
Some(new_local)
let captures_obj = if has_scoped_idents {
let captures_obj = id!(private_ident!(&*_CAPTURES_OBJ.clone()));
module.body.push(create_synthetic_named_import(
&captures_obj,
ctx.core_module,
));
Some(captures_obj)
} else {
None
};

let expr = if let Some(_captures) = _captures {
let expr = if let Some(captures_obj) = &captures_obj {
Box::new(transform_function_expr(
*ctx.expr,
&_captures,
captures_obj,
ctx.scoped_idents,
))
} else {
Expand Down Expand Up @@ -1110,25 +1111,29 @@ fn create_named_export(expr: Box<ast::Expr>, name: &str) -> ast::ModuleItem {
}))
}

pub fn transform_function_expr(expr: ast::Expr, _captures: &Id, scoped_idents: &[Id]) -> ast::Expr {
pub fn transform_function_expr(
expr: ast::Expr,
captures_obj: &Id,
scoped_idents: &[Id],
) -> ast::Expr {
match expr {
ast::Expr::Arrow(node) => {
ast::Expr::Arrow(transform_arrow_fn(node, _captures, scoped_idents))
ast::Expr::Arrow(transform_arrow_fn(node, captures_obj, scoped_idents))
}
ast::Expr::Fn(node) => ast::Expr::Fn(transform_fn(node, _captures, scoped_idents)),
ast::Expr::Fn(node) => ast::Expr::Fn(transform_fn(node, captures_obj, scoped_idents)),
_ => expr,
}
}

fn transform_arrow_fn(
arrow: ast::ArrowExpr,
_captures: &Id,
captures_obj: &Id,
scoped_idents: &[Id],
) -> ast::ArrowExpr {
match arrow.body {
box ast::BlockStmtOrExpr::BlockStmt(mut block) => {
let mut stmts = Vec::with_capacity(1 + block.stmts.len());
stmts.push(read_captures(_captures, scoped_idents));
stmts.push(read_captures(captures_obj, scoped_idents));
stmts.append(&mut block.stmts);
ast::ArrowExpr {
body: Box::new(ast::BlockStmtOrExpr::BlockStmt(ast::BlockStmt {
Expand All @@ -1142,7 +1147,7 @@ fn transform_arrow_fn(
box ast::BlockStmtOrExpr::Expr(expr) => {
let mut stmts = Vec::with_capacity(2);
if !scoped_idents.is_empty() {
stmts.push(read_captures(_captures, scoped_idents));
stmts.push(read_captures(captures_obj, scoped_idents));
}
stmts.push(create_return_stmt(expr));
ast::ArrowExpr {
Expand All @@ -1157,7 +1162,7 @@ fn transform_arrow_fn(
}
}

fn transform_fn(node: ast::FnExpr, _captures: &Id, scoped_idents: &[Id]) -> ast::FnExpr {
fn transform_fn(node: ast::FnExpr, captures_obj: &Id, scoped_idents: &[Id]) -> ast::FnExpr {
let mut stmts = Vec::with_capacity(
1 + node
.function
Expand All @@ -1166,7 +1171,7 @@ fn transform_fn(node: ast::FnExpr, _captures: &Id, scoped_idents: &[Id]) -> ast:
.map_or(0, |body| body.stmts.len()),
);
if !scoped_idents.is_empty() {
stmts.push(read_captures(_captures, scoped_idents));
stmts.push(read_captures(captures_obj, scoped_idents));
}
if let Some(mut body) = node.function.body {
stmts.append(&mut body.stmts);
Expand All @@ -1191,12 +1196,12 @@ pub const fn create_return_stmt(expr: Box<ast::Expr>) -> ast::Stmt {
})
}

fn read_captures(_captures: &Id, scoped_idents: &[Id]) -> ast::Stmt {
fn read_captures(captures_obj: &Id, scoped_idents: &[Id]) -> ast::Stmt {
ast::Stmt::Decl(ast::Decl::Var(Box::new(ast::VarDecl {
span: DUMMY_SP,
ctxt: Default::default(),
declare: false,
kind: ast::VarDeclKind::Const,
kind: ast::VarDeclKind::Let,
decls: scoped_idents
.iter()
.enumerate()
Expand All @@ -1205,7 +1210,11 @@ fn read_captures(_captures: &Id, scoped_idents: &[Id]) -> ast::Stmt {
span: DUMMY_SP,
init: Some(Box::new(ast::Expr::Member(ast::MemberExpr {
span: DUMMY_SP,
obj: Box::new(ast::Expr::Ident(new_ident_from_id(_captures))),
obj: Box::new(ast::Expr::Member(ast::MemberExpr {
span: DUMMY_SP,
obj: Box::new(ast::Expr::Ident(new_ident_from_id(captures_obj))),
prop: ast::MemberProp::Ident(ast::IdentName::new("_".into(), DUMMY_SP)),
})),
prop: ast::MemberProp::Computed(ast::ComputedPropName {
span: DUMMY_SP,
expr: Box::new(ast::Expr::Lit(ast::Lit::Num(ast::Number {
Expand Down
17 changes: 12 additions & 5 deletions packages/optimizer/core/src/props_destructuring.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ macro_rules! id_eq {
};
}

fn is_capture_read(member: &ast::MemberExpr) -> bool {
matches!(
&member.obj,
box ast::Expr::Member(obj)
if matches!(&obj.prop, ast::MemberProp::Ident(prop) if prop.sym.as_ref() == "_")
)
}

enum TransformInit {
Keep,
Remove,
Expand Down Expand Up @@ -88,15 +96,14 @@ impl<'a> PropsDestructuring<'a> {
}
fn transform_component_body(&mut self, body: &mut ast::BlockStmt) {
// Skip already-preprocessed QRL function bodies (from lib builds).
// These have _captures destructuring at the top that must not be inlined,
// These have _capturesObj reads at the top that must not be inlined,
// because inner QRL capture arrays reference the named variables.
if body.stmts.first().is_some_and(|stmt| {
if let ast::Stmt::Decl(ast::Decl::Var(var)) = stmt {
var.decls.iter().any(|decl| {
matches!(
&decl.init,
Some(box ast::Expr::Member(m))
if matches!(&m.obj, box ast::Expr::Ident(id) if id.sym == *_CAPTURES)
Some(box ast::Expr::Member(m)) if is_capture_read(m)
)
})
} else {
Expand Down Expand Up @@ -317,8 +324,8 @@ impl<'a> VisitMut for PropsDestructuring<'a> {
}

// Skip first arg of inlinedQrl calls — pre-compiled library code.
// The function body already has _captures destructuring and explicit
// capture arrays that reference the original variable names.
// The function body already has _capturesObj reads at the top that must not be inlined,
// and explicit capture arrays that reference the original variable names.
if id_eq!(ident, &self.inlined_qrl_ident) || id_eq!(ident, &self.inlined_qrl_dev_ident)
{
for arg in node.args.iter_mut().skip(1) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
source: packages/optimizer/core/src/test.rs
assertion_line: 5854
assertion_line: 6143
expression: output
---
==INPUT==
Expand Down Expand Up @@ -81,10 +81,10 @@ Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"ma
*/
============================= test.tsx_Foo_component_other_useAsync_fsHooibmyyE.tsx (ENTRY POINT)==

import { _captures } from "@qwik.dev/core";
import { _capturesObj } from "@qwik.dev/core";
//
export const Foo_component_other_useAsync_fsHooibmyyE = async (_rawProps)=>{
const other = _captures[0];
let other = _capturesObj._[0];
const timer = setInterval(()=>{
other.value++;
}, 900);
Expand Down Expand Up @@ -134,10 +134,10 @@ export const Foo = /*#__PURE__*/ componentQrl(q_Foo_component_HTDRsvUbLiE);
Some("{\"version\":3,\"sources\":[\"/user/qwik/src/test.tsx\"],\"names\":[],\"mappings\":\";;;;AAGA,6CAA6C;;AAC7C,OAAO,MAAM,oBAAM,0CAoBhB\"}")
============================= test.tsx_Foo_component_sig_useAsync_f0BGwWm4eeY.tsx (ENTRY POINT)==

import { _captures } from "@qwik.dev/core";
import { _capturesObj } from "@qwik.dev/core";
//
export const Foo_component_sig_useAsync_f0BGwWm4eeY = async (_rawProps)=>{
const sig = _captures[0];
let sig = _capturesObj._[0];
const timer = setInterval(()=>{
sig.value++;
}, 1000);
Expand Down
Loading
Loading