Skip to content

Feat/ts optimizer package - #8872

Open
scottweaver wants to merge 1225 commits into
QwikDev:mainfrom
scottweaver:feat/ts-optimizer-package
Open

Feat/ts optimizer package#8872
scottweaver wants to merge 1225 commits into
QwikDev:mainfrom
scottweaver:feat/ts-optimizer-package

Conversation

@scottweaver

Copy link
Copy Markdown

This set of changes sees the experimental Typescript optimizer from https://github.com/thejackshelton/TS-Optimizer to a Qwik sub-package project.

github-actions Bot and others added 30 commits July 15, 2026 16:58
…QwikDev#350)

Post-merge wrap-up: main -> 5b301e7, full baseline 1160 -> 1172,
three merged branches deleted, qds dist re-synced, OPTIMIZER.md audit
clean (contract-preserving JSX partition fix). Progress log trimmed to 10.


Claude-Session: https://claude.ai/code/session_01CJEPKkGz766mEd1Y7pHi8v

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…Dev#351)

sync$ bodies stay inline as _qrlSync(<live fn>, "<serialized string>").
The live fn is TS-stripped by the downstream module/segment transform,
but the serialized string is a JS string literal that pass never
descends into — so annotations like (e: KeyboardEvent): void survived
verbatim. At runtime the framework inlines that string into SSR HTML and
eval's it, where a leaked annotation is a SyntaxError that silently kills
the inline sync handler.

Transpile the text used for the serialized string through oxc-transform
(wrapped as a const-declaration, then sliced back out); the first, live
argument stays raw so existing snapshot formatting is preserved. The
strip is a proven no-op after minify for TS-free bodies, so it is always
applied rather than gated on a flag.

Verification: typecheck clean; convergence 203/9 (same failing set); full
suite 0 new failures, +9 new tests; the three existing sync$ snapshots
stay byte-identical.


Claude-Session: https://claude.ai/code/session_01CJEPKkGz766mEd1Y7pHi8v

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…ion (QwikDev#352)

The pre-transformed `_jsxDEV(type, props, key, isStatic, source, self)` path
in `buildJsxSortedCall` unconditionally sliced `args[2]` as the key, so every
element emitted the dev form's `undefined` key placeholder. The old `isNested`
gate was dead code for dev-form input (always >= 3 args). Nested components
with `undefined` keys break runtime reconciliation (`insertBefore` crash).

New keying rule: a component element is always keyed; an HTML element is
keyed only when it is not a direct jsx child of another jsx element — a
render root, or an element reached through an expression boundary
(`&&` / ternary / `.map` / an arrow or loop body). A direct HTML child takes
`null`. An explicit key is reused only from the 3-argument
`jsx(tag, props, key)` form; the 6-argument dev form's trailing key/source
arguments are ignored.

Replaced the tag-stack / nearest-HTML-ancestor machinery with a
`markDirectJsxChildren` pass that records each call's direct jsx children at
enter (before the reactive-binding early return, so purely-static components
are covered). Renamed the `buildJsxSortedCall` `isNested` param to
`isDirectJsxChild`.

Convergence unchanged (203/9, same set; `example_qwik_react` and
`example_parsed_inlined_qrls` still byte-identical). Adds 6 tests for the
key-generation rule. No 7th dev-metadata arg is emitted (out of scope).


Claude-Session: https://claude.ai/code/session_01CJEPKkGz766mEd1Y7pHi8v

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…/535 Done, OSS-532 parked (QwikDev#353)

Claude-Session: https://claude.ai/code/session_01CJEPKkGz766mEd1Y7pHi8v

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…ect crash cleared (QwikDev#354)

Claude-Session: https://claude.ai/code/session_01CJEPKkGz766mEd1Y7pHi8v

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…mode) (QwikDev#355)

* fix(OSS-536): inject _useHmr into inline/hoist component bodies (hmr mode)

In hmr mode the optimizer injected `_useHmr(devFile)` into `component$`
bodies only on the segment (client) strategy path (post-process.ts). The
inline/hoist strategy — which the dev SSR render uses — emitted the body
with no HMR hook.

`_useHmr` is a Qwik hook: its presence changes a component's serialized
hook/vnode layout. The SSR render (inline/hoist, no hook) serialized ref
offsets one way; the client (segment, with hook) re-rendered differently
under interaction, so vnode ref offsets desynced → an intermittent
`Missing refElement` assert under DOM churn on every ref-bound component
(carousel, checkbox, checklist, collapsible, tree in qwik-design-system).
TS-only; the SWC reference injects the hook in both paths and stays
consistent.

Inject `_useHmr` in the inline/hoist emission path (output-assembly.ts),
gated identically to the segment path: `mode === 'hmr' && devFilePath &&
isAnyComponentCtx(ctxName)`. Extracted the block/expression body-injection
core out of `injectUseHmr` and added `injectUseHmrIntoInlineBody` for the
bare-body shape the inline/hoist path carries.

Browser head-to-head (qwik-design-system dev SSR, TS :5200 vs SWC :5201):
the refElement assert is gone across all 5 routes (carousel 3/3→0,
checklist 4/4→0, checkbox 2/15→0, collapsible 3/3→0, tree 1/8→0); TS SSR
HTML now emits q-d:q-hmr markers + q:template projection elements matching
SWC.

Convergence unchanged (203/9 — no inline/hoist fixture runs in hmr mode).
Full baseline green. +5 regression tests. OPTIMIZER.md useHmr emission-site
note updated.

Closes OSS-536.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJEPKkGz766mEd1Y7pHi8v

* docs(state): record OSS-536 root-cause + fix

Server/client HMR-hook desync fixed; refElement assert gone on all 5
ref-bound qds routes. Branch entry + measurements + progress log updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJEPKkGz766mEd1Y7pHi8v

* refactor(OSS-536): drop what-comments from useHmr inline-injection path

Remove the inline WHY comment on the inline/hoist _useHmr gate plus the two doc comments on injectHmrCallIntoFunctionBody / injectUseHmrIntoInlineBody added by the fix. The gate mirrors the uncommented segment-path gate; the function names state intent and the behavior is guarded by inline-hoist-usehmr.test.ts. The serialized-layout desync rationale stays in the fix commit message where it can't rot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01CJEPKkGz766mEd1Y7pHi8v

* docs(state): record OSS-536 full post-fix qds re-sweep (clean)

Full 29-route head-to-head re-sweep post-fix: 0 TS-only divergences across all 23 rendering routes; all 5 fixed routes crash->0; 9 prior-MATCH routes no regression; modal symmetric, 6 identical 404s. Refresh header, progress log (trim to 10), and the what-to-do-next section.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01CJEPKkGz766mEd1Y7pHi8v

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…v#356)

PR QwikDev#355 (OSS-536: inject _useHmr into inline/hoist component bodies in hmr
mode) merged to main (c4091ea; baseline 2bdf578). Post-merge wrap-up:
branch fix/oss-536-inline-hoist-usehmr deleted local+remote, qds dist
rebuilt + re-synced, OSS-536 flipped to Done.

STATE refresh: header PR-status + tail, branches table (main head SHA
2bdf578, OSS-536 row removed), measurements (main baseline 203/9
convergence + 1192 full, +5 absorbed), progress-log prepend + trim to 10,
what-to-do-next OSS-536 marked Done. OPTIMIZER.md useHmr emission-site
note already updated in the fix PR — no further audit.


Claude-Session: https://claude.ai/code/session_01CJEPKkGz766mEd1Y7pHi8v

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…ves, wireMigration split (QwikDev#357)

* refactor: remove dead code + an exact-dup helper (hygiene batch A)

What: pure deletions + one import repoint across 7 files.
- applyRawPropsTransformDetailed + RawPropsTransformResult — zero callers.
- collectIdentifiersFromExpr — orphaned (collectSignalDeps uses the
  inlined fallbackCollectIdents); fixed its one stale doc reference.
- findArrowIndex — whole chain dead: canonical in text-scanning.ts, its
  lone import + re-export in body-transforms.ts, no consumers repo-wide.
- addBindingNamesToSet — one-line passthrough; call the already-imported
  addBindingNamesFromPatternToSet directly at all 4 sites.
- partsHaveImport — import-collection.ts kept a byte-identical private
  clone of the body-transforms.ts export it already imports from.

Isolation: removes silently-dead surface area and one duplicate that
could drift from its source. No behavior change.
Foundation: clears the "free wins" tranche ahead of the shared-primitive
consolidation (batch B).
Risk: deletions + one import repoint. typecheck clean; convergence
203/203 and full 1192/1192 baseline-passing tests unchanged. Net -91 LOC.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJEPKkGz766mEd1Y7pHi8v

* refactor: share edit/AST primitives across passes (hygiene batch B)

What: replace hand-rolled duplicates with shared primitives.
- 6 reverse-splice apply loops (dead-code, module-cleanup,
  const-propagation) → the existing applyReplacements; ranges are
  disjoint by construction at every site.
- resolveConstLiterals / resolveConstLiteralsInClosure: extract the
  shared inner walk as collectConstLiteralValues(root, source, offset, …);
  the two now differ only by wrapper-offset vs source-absolute.
- containsJsx / containsUnknownCall / containsImportedReference: fold the
  three deep-existential walks onto a new someAstDescendant primitive in
  ast/guards (sibling of someAstChild); drop containsJsx's vestigial
  array-input branch (no caller passes an array).
- getPropertyName / staticPropName → shared memberStaticPropName; the two
  q_<sym>.w() capture-wrap arms (jsx.ts, jsx-props.ts) → shared
  isCaptureWrappingQrlCall.
- inlineConstCaptures guard + isRealRef → the canonical
  isReplaceableIdentifierPosition (adds a params-position exclusion the
  hand-rolled copies lacked; params shadow, so it can only be more
  correct).

Isolation: one home per operation instead of 2-6 drifting copies; the
divergent-guard risk (each reverse-splice/position-guard copy could rot
independently) is removed.
Foundation: someAstDescendant + memberStaticPropName + the shared
predicates are reused by later hygiene batches (C/D).
Risk: behavior-preserving. typecheck clean; convergence 203/203 and full
1192/1192 baseline-passing tests unchanged. Net -97 LOC.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJEPKkGz766mEd1Y7pHi8v

* refactor: dedupe _noopQrl builders + dev-meta literal (hygiene batch C)

What: the inline/hoist strategy had four near-identical _noopQrl string
builders differing only in the LHS var name (q_<sym> vs sentinel
q_qrl_<counter>), and the {file,lo,hi,displayName} dev-meta object
literal was hand-emitted three times (twice here, once in dev-mode.ts).
- Two private core builders buildNoopQrl(varName, symbolName) /
  buildNoopQrlDev(varName, symbolName, devMeta); the four public builders
  become one-line delegations.
- NoopQrlDevMeta type + formatDevMeta() in dev-mode.ts, now the single
  source of the dev-meta literal for both _noopQrlDEV and qrlDEV.
- Trimmed the redundant format-example doc blocks the delegations no
  longer need.

Isolation: one place each defines the noop-QRL template and the dev-meta
literal; output is byte-identical (snapshot-pinned).
Foundation: formatDevMeta is the reuse point if a third dev-meta emitter
appears.
Risk: string builders are snapshot-pinned; convergence 203/203 and full
1192/1192 baseline-passing tests unchanged (byte-identical). Net -34 LOC.

Deferred (NOT behavior-preserving as pure refactors — the "duplicates"
have diverged):
- Import-before-separator idiom (5 sites): guards genuinely differ
  (substring vs partsHaveImport-structured vs startsWith-import) and the
  canonical helper unshifts where these skip when no separator exists.
- Same-file import ladder (2 sites): different data sources
  (reexportedNames set vs migrationDecisions lookup), skip conditions,
  and default-vs-reexport ordering. Unifying either would normalize
  behavior in edge cases the fixtures may not cover.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJEPKkGz766mEd1Y7pHi8v

* refactor: extract lib-mode marker predicate (hygiene batch D)

What: the identical `name.length > 1 && name.endsWith('$')` lib-mode
marker test appeared at 3 sites (rewrite/index, output-assembly,
module-cleanup), each behind a paragraph-long comment explaining the
same rationale. Extract isLibModePreservedMarker() in qwik/qrl-naming;
the rationale now lives once on the predicate's doc, and the three site
comments (which also referenced the reference implementation, against the
comment convention) are gone.

Isolation: one named predicate for "a $-suffixed marker other than the
bare $"; the intent is stated once instead of re-explained per site.
Foundation: the marker-name predicate is the reuse point if a fourth
lib-mode gate appears.
Risk: identical boolean, surrounding conditions unchanged — behavior
provably preserved. convergence 203/203 and full 1192/1192 unchanged.

Deferred (NOT behavior-preserving as pure refactors):
- isHtmlTagName (4 copies): three genuinely different rules — any-lowercasing
  char vs strict ASCII a-z vs not-uppercase-letter. They disagree on
  digit/non-ASCII-initial tags; unifying normalizes behavior.
- signal-result const/var classification (2 sites): diverge on
  param/in-loop handling — sharing would close a parity gap = change output.
- createTransformSession adoption (5 sites): a parse-mechanism swap with
  wrapper prefix/suffix offset math to verify per site; too much risk
  layered on batch B's const-propagation changes for a boilerplate win.
- marker->Qrl slice dup: the diagnostic caller may not want getQrlCalleeName's
  $/sync$ special-casing; needs verification, not a blind swap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJEPKkGz766mEd1Y7pHi8v

* refactor: split wireMigration into a helper sequencer (hygiene batch E)

What: wireMigration was a ~200-line function interleaving five concerns.
Extract three named helpers, mirroring the generateAllSegmentModules
sequencer pattern (OSS-343):
- resolveMovedDeclImportDeps — a moved decl's import dependencies.
- emitMovedDeclaration — marker-QRL vs plain-decl emission.
- filterMigratedCaptures — drop migrated names from captures + reconcile
  the captures flag against paramNames.
wireMigration is now a ~40-line orchestrator: auto-imports, pre-scan
sets, the move loop (calling the two per-decl helpers), then the capture
filter. Also folded the move-loop's reexport guard to an early-continue.

Isolation: each concern is independently readable and named; the move
loop reads as "resolve deps, emit" instead of 100 lines inline. The
shared-mutation surface (captureInfo, movedQrlSymbols) is now threaded
explicitly through helper parameters rather than closed over.
Foundation: the per-decl helpers are the natural extension points for
the deferred SWC transitive-ordering work (topological emit) noted in
OPTIMIZER.md's migration deep dive.
Risk: pure code motion. typecheck clean; convergence 203/203 and full
1192/1192 baseline-passing tests unchanged. Net -5 LOC.

Also updates the OPTIMIZER.md line refs this move invalidated
(wireMigration/buildDefaultStrategySegment/generateAllSegmentModules).
The doc's other segment-generation.ts refs carry pre-existing cumulative
drift and are left for a dedicated line-ref refresh.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJEPKkGz766mEd1Y7pHi8v

* refactor: drop self-evident docs on two batch-B helpers

collectConstLiteralValues and isCaptureWrappingQrlCall each carried a doc
block that only restated what the name + logic already say (the offset
parameter's two modes are shown by its two call sites; the q_<sym>.w
shape is exactly what the predicate checks, and the const-classification
rationale belongs at the call site, not on the shape detector). Comment-
only; no behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJEPKkGz766mEd1Y7pHi8v

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Deletions (assert nothing / exact duplicate):
- extract.test.ts: two disambiguateExtractions stubs — one had zero
  expect() calls, the other only asserted length on an input that by
  construction produces no disambiguation. Both superseded by the working
  "disambiguates multiple extractions with same display name" test.
- fnsignal-nonreactive-map.test.ts: a byte-identical duplicate of
  fnsignal-computed-key.test.ts's computed-key test.
- segment-only-debug.test.ts: a debug harness ending in
  expect(true).toBe(true) that ran a full-corpus transform each CI run to
  console.log a report — an investigation artifact, not a test, and not
  referenced by any doc.

Strengthened in place (kept the test IDs; each now fails on real
regression instead of passing vacuously):
- failure-families.test.ts: had zero assertions (a categorize-and-log
  harness). Added corpus-present + complete-partition guards so a missing
  or dropped snapshot surfaces. Kept as a test since docs reference it as
  a secondary signal.
- transform.test.ts: two ctxKind tests asserted inside `if (seg)` guards
  that vanish if extraction stops emitting the segment — now assert the
  segment is found first. The const-vs-var test only checked a
  `_jsxSorted("div"` call existed — now also asserts both prop values
  survive. Dropped a reference-implementation comment.
- rewrite-parent.test.ts Test 9: named "parent-child relationship" but
  only asserted extraction count — now asserts a nested extraction's
  parent resolves to another extraction's symbol.

Baseline: the four deletions drop four passing IDs and the total. Mapped
.ci/baseline.json from the stored (CI-true) baseline — removed exactly
those four IDs, decremented full.total 1217 -> 1213 — rather than
regenerating locally (local totals carry +2 from the QWIK_HOME benchmark
skips). Verified: convergence 203/203, full 1188/1188, 1:1 with zero
unaccounted adds/removes.


Claude-Session: https://claude.ai/code/session_01CJEPKkGz766mEd1Y7pHi8v

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(OSS-540): extract plainQrlName primitive (finding QwikDev#6)

What: extract the special-case-free `$`→`Qrl` slice into `plainQrlName`;
`getQrlCalleeName` now layers its bare-`$`/`sync$` cases on top of it, and
the C05 diagnostic export-name check uses `plainQrlName` directly instead of
its own inline `slice(0,-1)+'Qrl'`.

Why: removes the duplicated slice while preserving the diagnostic's exact
current behavior — it intentionally does NOT want getQrlCalleeName's marker
special-casing (a user `sync$`/`$` export must still be checked against
`syncQrl`/`Qrl`, not `_qrlSync`/``). Naively sharing getQrlCalleeName would
have changed that; the primitive avoids the change.

Foundation: resolves the OSS-540 "marker→Qrl slice dup" finding as a safe
extraction rather than a behavior-changing merge.

Risk: none — pure extraction. Typecheck clean; +3 plainQrlName pins;
42/42 in rewrite-calls + diagnostics suites, unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJEPKkGz766mEd1Y7pHi8v

* refactor(OSS-540): unify raw-name HTML predicate on isHtmlElement (finding QwikDev#3)

What: point segment-generation's local `isHtmlElementName` at the canonical
`isHtmlElement`, and drop the redundant a-z re-check in jsx-elements-core's
`tagIsHtml` (its `tag.startsWith('"')` already encodes processJsxTag's
isHtmlElement decision). Left jsx-children's `isComponent` alone — it is a
distinct uppercase check; expressing it as `!isHtmlElement` would flip
member-expression children (empty tagStr → HTML).

Why: the audit flagged "4 copies" of the HTML-tag test, but only one was a
divergent copy of the raw-name predicate. segment-generation used a strict
ASCII a-z rule while the rest of the pipeline uses Rule L (first char equals
its lowercase); they disagreed on digit/non-ASCII-initial tags. Unifying
removes the latent inconsistency and leaves one canonical predicate.

Foundation: resolves the OSS-540 isHtmlTagName finding — single source of
truth for "is this raw tag name an HTML element".

Risk: none on real input. The two rules agree on every valid tag (a-z
lowercase → HTML, A-Z → component); they diverge only on digit/non-ASCII
initials, which are invalid/absent in the fixture corpus. Convergence
203/9 unchanged; +4 edge-case pins (hyphenated, caseless-initial, empty).
The 3 unrelated failures in segment/jsx dirs are pre-existing on main.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJEPKkGz766mEd1Y7pHi8v

* refactor(OSS-540): adopt createTransformSession in 4 body-reparse sites (finding QwikDev#5)

What: replace hand-rolled wrapper-prefix + parseSync + offset-math in four
body-text reparse sites with the canonical `createTransformSession`:
- const-propagation.ts ×3 (resolveConstLiterals, inlineConstCaptures,
  propagateConstLiteralsInBody) — read AST positions, edit the unwrapped
  body via applyReplacements; now use session.program + session.offset.
- lib-mode-collapse.ts substituteInnerQVarsInText — edits a MagicString on
  the wrapped source; now uses session.edits + session.toSource(), which
  also removes the fragile manual `.slice(offset, len-1)` suffix trimming.

Why: every one of these reimplemented the same wrap/parse/offset idiom with
its own prefix (`__rl__`/`__ic__`/`__pb__`/`__body__`) and its own filename,
so their wrapped-source strings never matched the canonical session's — no
shared parse memo, and four subtly different offset computations to keep
correct. Routing them through createTransformSession gives one wrapper idiom
and lets repeated parses of the same body version share a cached parse.

Foundation: resolves the OSS-540 createTransformSession-adoption finding for
the clean-swap sites; single canonical body-reparse path.

Deferred (documented): inline-body.ts's JSX-in-inline-body reparse keeps its
hand-rolled wrapper — its prefix length escapes into the JSX transform's
dev-info `sourcePosition.wrapperPrefixLen`, so adopting the session would
have to thread session.wrapperPrefix.length through source-map positioning;
bounded benefit, real source-map risk, left for a dedicated pass.

Risk: none. parseWithRawTransfer (the session's parse) is exactly the
parseSync(RAW_TRANSFER_PARSER_OPTIONS) these sites used; the offset shift
(15/17 → 16) is applied consistently within each session. Typecheck clean;
const-propagation + lib-mode suites 23/23; convergence 203/9 unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJEPKkGz766mEd1Y7pHi8v

* refactor(OSS-540): unify ensureCoreImports onto canonical import helpers (finding QwikDev#1)

What: ensureCoreImports now uses `partsHaveImport` for its already-imported
guard and `insertImportBeforeSeparator` for the insert, replacing its
loose `startsWith('import') && includes(sym)` guard and inline
`splice(sepIdx, 0, ...)`. The dead no-separator early-return is dropped.

Why: the audit flagged three divergent guards for "is this symbol already
imported"; ensureCoreImports carried the loosest (substring-on-import-line),
the one most prone to false positives. The single caller guarantees a `//`
separator right before the call, so the early-return branch was unreachable
and the guard's precision was the only real difference. partsHaveImport is
behavior-identical here because the core symbol set has no substring
collisions.

Foundation: resolves the OSS-540 import-before-separator finding for the
provably-safe site; ensureCoreImports now shares one insert idiom with the
rest of import-collection/segment-codegen.

Deferred (documented): the segment-codegen raw-splice sites keep their own
no-separator fallbacks (append-import-then-separator / append-import / skip),
which are genuinely distinct from the canonical unshift and can't collapse
without changing behavior. One of them also has a `, sym`-substring guard
that false-positives on _noopQrl vs _noopQrlDEV; correcting it is a real
behavior change, left out of this behavior-preserving pass.

Risk: none on real input. Guard is identical for the core symbols; no-sep
branch was dead; the insert switch flips core-import order from reverse to
forward, which is not semantically checked (imports are order-independent)
and regresses nothing. Convergence 203/9, full 1188 baseline all pass;
+4 ensureCoreImports contract pins.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJEPKkGz766mEd1Y7pHi8v

* refactor(OSS-540): share the same-file import ladder (finding QwikDev#2)

What: extract `resolveSameFileImportName` (+ `formatSameFileImport`) as the
single same-file-symbol import resolver, and route both prior copies through
it: import-collection's `addSameFileImport` (which emitted import strings)
and segment-generation's `resolveMovedDeclImportDeps` (which builds
MovedImportDep objects).

Why: the audit flagged these two ladders as divergent — different data
sources (a migration-decision lookup vs a precomputed reexportedNames set)
and opposite reexport-vs-default evaluation order. On inspection they encode
the same decision: the reexport predicate is identical (`reexport &&
!isExported` at both sites), and the reexport/default arms are mutually
exclusive (a default export is always isExported, so it can never enter the
reexport arm), which makes the order difference unobservable. Unifying makes
that equivalence explicit and removes the drift risk of two ladders that
must stay in lockstep by hand.

Foundation: one canonical same-file import resolution; the two callers now
differ only in how they format the shared result (string vs dep object).

Risk: none. The resolver reproduces every arm of both ladders; the order
switch is unobservable by the mutual-exclusivity argument above. Typecheck
clean; convergence 203/9, full 1188 baseline all pass; +8 resolver/formatter
pins covering all four arms.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJEPKkGz766mEd1Y7pHi8v

* fix(OSS-540): share _fnSignal deps-const classifier, close _jsxDEV param gap (finding QwikDev#4)

What: extract `fnSignalDepsAllConst` (jsx.ts) as the single deps-const
predicate for _fnSignal prop classification, and route both classifiers
through it — the raw-JSX path (jsx-props `processProps`, two call sites) and
the pre-transformed `_jsxDEV` path (jsx-call-transform `classifyProp`).

Why (behavior change): `classifyProp` was missing the raw path's
component-parameter arm — a dep that's a closure parameter counts as
const-eligible on a *component* element (not on HTML). So on the `_jsxDEV`
path a param-dependent _fnSignal prop on a component element was wrongly
placed in the var-props bag instead of the const bag. Confirmed by
byte-level probe:
  before: _jsxSorted(Inner, { title: _fnSignal(_hf0, [props], …) }, null, null, …)
  after:  _jsxSorted(Inner, null, { title: _fnSignal(_hf0, [props], …) }, null, …)
Now the `_jsxDEV` path classifies identically to the raw-JSX path. The raw
path's other arm (`!inLoop`) is not portable — the `_jsxDEV` path has no loop
tracking — so classifyProp gains only the param arm.

Scope: convergence is unaffected (every fixture is raw JSX; the `_jsxDEV`
path is never exercised there). This path is covered by the jsxdev unit tier
and real bundler builds (qds/router). A qds head-to-head is the definitive
validation — recommended follow-up before relying on the qds output shift.

Also: dropped the SWC-reference comments from the edited processProps block
(the param/component rationale now lives once on fnSignalDepsAllConst's doc).

Risk: full suite + convergence green (203/9, 1188 baseline all pass); no
existing test regressed. +2 jsxdev pins: component element → const bag (the
fix), HTML element → var bag (param arm correctly HTML-gated, unchanged).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJEPKkGz766mEd1Y7pHi8v

* refactor(OSS-540): drop what-comments on small helpers (PR QwikDev#359 review)

Remove WHAT-comments that restated what the name and body already say:
- plainQrlName: doc deleted — name + the getQrlCalleeName body below are
  self-evident, and a test pins the special-case-free contract.
- fnSignalDepsAllConst: trimmed to the one non-obvious WHY (params are
  const-eligible only on component elements).
- ensureCoreImports: doc deleted — name + body say it.

Also restructured processProps's _fnSignal block: the two empty
guard branches (each carrying a fall-through comment) collapse into named
`isRawWhenNonConst` / `excludedFromHoist` booleans, so the code speaks for
itself with no comments.

Behavior-preserving: typecheck clean; convergence 203/9, full 1188 baseline
all pass; no test changed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJEPKkGz766mEd1Y7pHi8v

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…SS-537 hygiene sweep complete (QwikDev#360)

- main → 36417d2; OSS-538/539/540 Done (PRs QwikDev#357/QwikDev#358/QwikDev#359 merged).
- Header rewritten to the current state: OSS-540's six consolidations, with
  QwikDev#4 flagged as a `_jsxDEV`-path parity change (not a convergence flip) whose
  definitive validation is a still-pending qds head-to-head.
- Measurements: 203/9 convergence, 1188 baseline all pass (+20 absorbed).
- Branches: main head bumped; merged OSS-540 branch removed.
- Progress log: prepend QwikDev#359 + QwikDev#357/QwikDev#358 entries, trim oldest 2.
- Regenerated .cursor/rules mirrors (STATE.mdc; OPTIMIZER.mdc drift corrected).


Claude-Session: https://claude.ai/code/session_01CJEPKkGz766mEd1Y7pHi8v

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…Dev#361)

* refactor: de-any jsx-transform.test.ts AST-extraction helpers

What: replace the two `any`-returning parse helpers (`parseExpr` plus its
dead byte-identical twin `parseJsx`) with four typed helpers — `parseExpr`,
`parseExprStatement`, `parseJsxElement`, `parseJsxFragment` — and collapse
the 18 inline `(program.body[0] as any).expression` node extractions into
single typed-helper calls. Drop the `collectScopeAwareBindings(program as
any)` cast (assignable directly). 20 `any`s and one dead helper removed.

Why (isolation): the helpers now return `Expression` / `JSXElement` /
`JSXFragment` and assert node structure, so a malformed parse throws a named
error instead of silently propagating `undefined` into the assertion — a
strictly stronger test. Upholds the hard "no `any`" type-discipline rule.

Why (foundation): the typed-extraction pattern is the reusable template for
the deferred `tests/optimizer` de-any effort (STATE.md "Held / deferred") —
the same `(body[0] as any).expression` idiom recurs across sibling specs
(capture-analysis, jsx-arrow-prop-value, …) and adopts these helpers next.

Risk: test-only, one file, touches no src. Verified behavior-identical —
62 pass / 2 fail before and after (the 2 are pre-existing SWC-parity
known-failures, not in the baseline passing set, untouched). typecheck
clean; local gate green: convergence 203/203, full 1208/1208 baseline-
passing, zero regressions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJEPKkGz766mEd1Y7pHi8v

* refactor: extract shared typed AST-parse helpers for tests

What: promote the four typed parse helpers (`parseExpr`,
`parseExprWithSource`, `parseJsxElement`, `parseJsxFragment`) from
jsx-transform.test.ts into a shared `tests/optimizer/helpers/parse-nodes.ts`
module, and adopt them in signal-analysis.test.ts — removing its last two
`any`s (its own `{ node, source }` parse helper duplicated the pattern).

Why (isolation): a second consumer appeared, so the helpers move to one home
instead of being copied. One typed definition of "parse a string into an AST
node", asserting structure at the boundary, now serves both specs.

Why (foundation): this is the reusable surface the deferred `tests/optimizer`
de-any effort (STATE.md "Held / deferred") builds on — sibling specs that
share the `(body[0] as any).expression` idiom adopt these helpers next
instead of re-deriving them.

Risk: test-only, touches no src; the new module is a non-test `.ts` (adds no
tests). Verified behavior-identical — jsx-transform 62/2, signal-analysis
25/1, unchanged before/after (the 3 fails are pre-existing SWC-parity
known-failures outside the baseline passing set). typecheck clean; gate
green: convergence 203/203, full 1208/1208, zero regressions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJEPKkGz766mEd1Y7pHi8v

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…lla filed (QwikDev#362)

PR QwikDev#361 (test-infrastructure de-any) merged to main (d30c4ab):
jsx-transform.test.ts + signal-analysis.test.ts de-anied, shared
tests/optimizer/helpers/parse-nodes.ts extracted. Test-only; gate green
(convergence 203/9 unchanged, full 1208 baseline all pass).

- Bump Last updated + main head SHA (36417d2 -> d30c4ab) + measurements.
- Prepend progress entry; trim oldest (Stranded-QwikDev#348).
- Correct the "Held / deferred" de-any note: now tracked as umbrella
  OSS-541 + subs OSS-542 (tests/optimizer + oxc-walker) / OSS-543
  (ast-compare oracle narrowing -- NOT mechanical as previously billed).
- Regenerate .cursor/rules/STATE.mdc mirror.

OPTIMIZER.md audit: PR touched only tests/ -- no trigger-checklist file,
no update needed.


Claude-Session: https://claude.ai/code/session_01CJEPKkGz766mEd1Y7pHi8v

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
De-any the low-risk test-spec track of the OSS-541 umbrella (test-only).

What changed:
- Typed the oxc-walker pattern in capture-analysis.test.ts (~19 sites) and
  loop-hoisting.test.ts: AstNode/AstFunction callbacks, this.skip() via the
  walker's contextual this, dropped every `program as any`.
- capture-analysis.test.ts: unified the two near-identical dollar-arg helpers
  behind typed collectors (asClosureArg/collectDollarArgs/collectParentScopeIds/
  collectImportedNames/buildDollarArgFacts), removing the duplication.
- Extracted the duplicated AST position-stripping normalizer from
  convergence-breakdown + failure-families into tests/optimizer/helpers/
  ast-normalize.ts (unknown + isRecord narrowing, no casts).
- Typed findFirstCall/findFirstNode to return CallExpression/AstNode and throw
  on absence (the parse-nodes.ts convention); every callsite supplies valid
  input.
- transform.test.ts: `as any` -> `as SegmentMetadataInternal`, the sanctioned
  test idiom already used in stripped-qrl-cleanup.test.ts for reaching the
  internal captureNames field.

Beneficial in isolation: removes the actionable `any` from tests/optimizer,
upholding the "any is never the right answer" rule; kills two duplicated `strip`
normalizers; typed walker callbacks surface AST-shape mistakes at compile time.

Foundation: extends the parse-nodes.ts typed-helper pattern (PR QwikDev#361);
ast-normalize.ts becomes the shared normalizer for breakdown-style tests.
Completes Sub A of OSS-541, leaving OSS-543 (ast-compare.ts oracle) as the only
remaining de-any.

Risk: test-only, behavior-preserving. Casts are erased at runtime; refactored
helpers are semantically identical. Convergence 203/9 and full 1208 baseline all
pass, 0 regressions; the 6 pre-existing transform.test.ts failures are unchanged
(verified against main).

Refs: OSS-542 (sub of OSS-541).


Claude-Session: https://claude.ai/code/session_01CJEPKkGz766mEd1Y7pHi8v

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…subs landed (QwikDev#365)

OSS-542 (QwikDev#363) merged to main (0e56468); OSS-543 (QwikDev#364) In Review.
Bumped main SHA + Last verified; prepended the umbrella progress entry
and trimmed the log to ~10. Regenerated the .cursor/rules/STATE.mdc mirror.


Claude-Session: https://claude.ai/code/session_01CJEPKkGz766mEd1Y7pHi8v

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…v#364)

* refactor(OSS-543): de-any the ast-compare differential oracle

De-any the higher-risk track of the OSS-541 umbrella: the AST
differential-comparison oracle (`src/testing/ast-compare.ts`, ~164 `any`
sites across 71 normalizer functions), consumed by both the convergence
suite and production `output-assembly.ts`.

What changed:
- Every code-level `any` replaced with typed narrowing. Node params ->
  `AstCompatNode`; recursive-clone/walk params + returns -> `unknown`;
  local accumulators -> `Record<string, unknown>` / `unknown[]`.
- Three shared narrowing primitives carry the per-site work: reused
  `isAstNode` from `optimizer/ast/guards.ts`, plus local `isRecord` and
  `asArray` (returns the SAME array reference so in-place push/splice/
  sort/index-assign still mutate the real tree) and `asString`.
- Strict-oxc-typed helpers that actually receive the loose post-strip
  tree were retyped to `AstCompatNode`/`unknown`
  (`isReorderableDeclaration`, `stripFrameworkHelperImports` + its
  closures); the now-unused `Directive`/`ImportDeclaration`/
  `ModuleExportName`/`Program`/`Statement` imports were dropped.
- Removed a provably-dead `=== 'AssignmentPattern'` check in
  `inlineDestructuredBindings` (the preceding `!== 'Identifier'` break
  already covers it) rather than keep a type-defeating `: unknown` dodge.

No `as` casts introduced (the sanctioned one-cast-at-the-boundary lives
inside `guards.ts`'s `isAstNode`); narrowing is guard-based throughout.

Beneficial in isolation: closes the last `any` hole in the test
infrastructure and makes the oracle's dynamic AST access compile-time
checked. Foundation: completes OSS-541.

Risk: this is the differential oracle — a weakened comparison would
silently mask convergence regressions. Verified BIT-IDENTICAL: the full
pass/fail set is unchanged (before pass=1208 fail=25, after pass=1208
fail=25; 0 regressed, 0 newly-passing, 0 gained-fail, 0 lost-fail),
confirmed by re-running the whole suite and diffing test-id sets.
`pnpm typecheck` clean.

Closes OSS-543 (sub of OSS-541).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJEPKkGz766mEd1Y7pHi8v

* refactor(OSS-543): drop doc comments on trivial asArray/asString helpers

Per review: the one-line JSDoc on `asArray`/`asString` only restated the
name + body (WHAT-narration). The names and one-line bodies are self-evident.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJEPKkGz766mEd1Y7pHi8v

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…lla closed (QwikDev#366)

OSS-543 (QwikDev#364) merged to main (b7fa34c); OSS-541/542/543 all Done.
Bumped main SHA + Last verified; prepended the close-out entry and
trimmed the log to ~10. Regenerated the .cursor/rules/STATE.mdc mirror.


Claude-Session: https://claude.ai/code/session_01CJEPKkGz766mEd1Y7pHi8v

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…SWC parity) (QwikDev#367)

* fix(OSS-471): MOVE single-consumer server$ binding into its segment (SWC parity)

A module-level `const testServer$ = server$(…)` referenced only inside one
nested `onClick$` was re-exported (`_auto_`) instead of moved into its single
consumer, diverging from the reference optimizer. Four coupled fixes, each
verified byte-for-byte against the reference on the canonical route shape:

- Capture over-attribution: a parent extraction dropped module-level captures
  whose every occurrence falls inside a nested child's byte range. Gated to
  file-emitting strategies — under inline/hoist the child body stays inline
  (`q_X.s(IDENT)`), so the parent still references the name and the reexport
  must survive (e.g. `useStyle$(STYLES)`).
- Escaped-name match in `tryBuildMarkerDeclMove` and the parallel
  `movedMarkerSymbols` detection: a `$`-suffixed decl's displayName is escaped
  (`testServer$` → `…_testServer_server`), so a raw match missed it — leaving a
  broken raw-text move and an un-demoted parent QRL binding.
- Marker-QRL callee imports from the marker's real source (`@qwik.dev/router`),
  not hardcoded core.
- PURE annotation on the moved wrap is per-callee (`componentQrl` pure,
  `serverQrl` not), via `needsPureAnnotation`.

Reachable (non-stripped) output now matches the reference: parent emits a bare
QRL registration, the handler segment is a self-contained
`serverQrl(qrl(…))` move. The stripped variant registers via `_noopQrl` (a
`qrl(()=>import(strippedChunk))` would resolve to the chunk's `null` export);
its local var name is a known cosmetic divergence on an unreachable path.

Three baseline tests that pinned the reexport behavior updated to the MOVE
(IDs kept for the name-based gate); one focused strip-aware test added.
Convergence-neutral (203/203); full suite 1208 pass, 0 regressions, +1 new.

Refs: OSS-471 (sub of OSS-456).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJEPKkGz766mEd1Y7pHi8v

* chore(OSS-471): trim inline docs per review

Apply the comment ladder to the OSS-471 diff: delete WHAT-narration, lean on
self-documenting locals (`moduleLevelCaptures`, `usedOutsideAnyChild`) and
`expect(...)` message args, and drop comments the tests already guard (the
inline/hoist exclusion gate is pinned by the hoist-regctxname STYLES test).
Only genuinely-irrecoverable one-line WHYs remain (the module-level capture
invariant, the escapeSymbol match, the retained test-ID note). No behavior
change — typecheck clean, affected tests + convergence unchanged.

Refs: OSS-471 (sub of OSS-456).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJEPKkGz766mEd1Y7pHi8v

* chore(OSS-471): drop remaining inline comments; rename tests to match contract

Remove every inline `//` comment from the OSS-471 diff — the code, the
self-documenting locals, and the `expect(...)` messages carry the intent, and
the rationale lives in the commit/PR/Linear. The two router-integration tests
whose names described the old reexport behavior are renamed to state the MOVE
contract directly (no explanatory comment needed); the two baseline IDs are
mapped through in `.ci/baseline.json` (REGRESSION.md deliberate-migration).

No behavior change — typecheck clean, convergence 203/203, full 1208 pass, 0
regressions (verified against the mapped baseline).

Refs: OSS-471 (sub of OSS-456).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJEPKkGz766mEd1Y7pHi8v

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
… landed (QwikDev#368)

OSS-471 (single-consumer server$ binding MOVE, SWC parity) merged via PR QwikDev#367
→ main (02b2610); OSS-471 Done, closing an OSS-456 sub (OSS-473 is the last
open one). Bump Last-updated + measurements (convergence 203/9 unchanged; full
1209 after baseline auto-update), main-row head SHA, drop OSS-471 from the
Backlog list, prepend the progress entry + trim to 10. OPTIMIZER.md audited —
no update (bug-fix/type-internal; cited refs <50-line drift).


Claude-Session: https://claude.ai/code/session_01CJEPKkGz766mEd1Y7pHi8v

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…migration hygiene) (QwikDev#369)

Prepares the tree for migration into the Qwik core monorepo, where the SWC
optimizer ships as a sibling package and "matches SWC" comments become
actively misleading. Comment-only across 163 files (plus 5 test-name
rewordings); no behavior change.

- Comment volume 9,918 -> 2,617 lines (-74%): deleted WHAT-narration, section
  banners, numbered step markers, JSDoc that restated signatures, and test
  preambles (tests self-document via describe/it names). Kept only
  irrecoverable one-line WHYs (invariants, external-tool workarounds).
- Scrubbed every reference-optimizer mention from comments AND test
  descriptions (~300 across 73 files): SWC, *.rs file:line refs, Rust fn
  names, @qwik.dev/optimizer, "matches SWC"/"SWC parity". Behavioral notes
  reworded as this optimizer's own contract. NAPI retained only where it
  names the native-binding ABI the public types conform to. The benchmark's
  functional SWC references (it loads and times the native optimizer) stay.
- Renamed 7 test names that editorialized "matches SWC"/"NAPI parity";
  the 7 IDs are mapped 1:1 in .ci/baseline.json (deliberate migration).

Verification: oxc-based skeleton proof shows 163/168 changed files
code-identical and the other 5 differ only in the intentional test-name
strings; full suite 1234/1209/25 unchanged from the pre-sweep baseline;
typecheck clean; baseline gate green (203 convergence, 1209 full).


Claude-Session: https://claude.ai/code/session_01CJEPKkGz766mEd1Y7pHi8v

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Remove TS-Optimizer-repo-specific infrastructure that is redundant inside the
Qwik monorepo (which provides its own): the .ci baseline-regression system and
its scripts/, the .github workflows, .claude/.cursor agent rules, .planning
docs, the agent-sync tooling, the root-only pnpm-lock.yaml, and the
swc-reference-only pinned copy of the optimizer (packages/optimizer is the
source of truth here). Also drop the agent-facing AGENTS.md/CLAUDE.md and the
point-in-time RCA writeup. Package build/test stays self-contained (tsdown +
vitest). History for every removed file is preserved in the grafted commits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJEPKkGz766mEd1Y7pHi8v
Reconcile package.json for its new home: drop the baseline/CI scripts (whose
files were removed with the standalone infra), repoint repository/homepage/bugs
to QwikDev/qwik with a `directory` field, and remove the package-level
`packageManager` pin (the root owns it; its version differed from root). Add
the package's dependencies to the workspace lockfile via pnpm install.

Verified in-tree: typecheck clean, `tsdown` build emits dist, and the full
vitest suite runs with a byte-identical pass/fail set to standalone
(1209/1234 passing; the 25 failures are the pre-existing convergence/parity
gaps, unchanged by the move).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJEPKkGz766mEd1Y7pHi8v
…timizer

Match the monorepo convention (packages/optimizer -> @qwik.dev/optimizer): drop
the now-redundant `qwik-` prefix and scope the package under @qwik.dev. Moves
the directory to packages/ts-optimizer (history preserved via git rename
detection), updates package name + repository/homepage/directory metadata, the
README, and the index.ts header, and regenerates the workspace lockfile.

Verified in the new location: typecheck clean, build emits dist, full suite
1209/1234 passing (25 pre-existing convergence/parity failures unchanged).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJEPKkGz766mEd1Y7pHi8v
wmertens and others added 30 commits August 24, 2026 11:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants