feat(repl): Lynx REPL — interactive playground with GitHub Pages deployment#2292
feat(repl): Lynx REPL — interactive playground with GitHub Pages deployment#2292Huxpro wants to merge 16 commits intolynx-family:mainfrom
Conversation
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds a new browser-only Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Tip Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs). Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Pull request overview
This PR adds a new @lynx-js/repl package that provides an in-browser Lynx playground (Monaco editor + live <lynx-view> preview), and wires it into the existing GitHub Pages website build. It also hardens web-platform background disposal logic to avoid calling unsupported lifecycle teardown functions.
Changes:
- Add
packages/repl(React + Rsbuild) with editor, preview, console capture, examples, and URL/localStorage state. - Update GitHub Pages workflow to build and publish the REPL under
/repl/. - Guard
callDestroyLifetimeFunduring web background disposal usingnativeGlobal.multiApps[id]capability checks.
Reviewed changes
Copilot reviewed 81 out of 83 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-workspace.yaml | Adds packages/repl to the workspace. |
| packages/web-platform/web-worker-runtime/src/backgroundThread/background-apis/startBackgroundThread.ts | Passes nativeGlobal into dispose handler wiring. |
| packages/web-platform/web-worker-runtime/src/backgroundThread/background-apis/crossThreadHandlers/registerDisposeHandler.ts | Guards callDestroyLifetimeFun behind runtime capability check. |
| packages/web-platform/web-core-wasm/ts/client/background/background-apis/startBackgroundThread.ts | Passes nativeGlobal into dispose handler wiring (WASM client). |
| packages/web-platform/web-core-wasm/ts/client/background/background-apis/crossThreadHandlers/registerDisposeHandler.ts | Guards callDestroyLifetimeFun behind runtime capability check (WASM client). |
| packages/repl/tsconfig.json | TS config for the new REPL package. |
| packages/repl/src/url-state.ts | Implements URL-based state serialization for sharing. |
| packages/repl/src/samples.ts | Registers built-in REPL examples (raw code via ?raw). |
| packages/repl/src/local-storage.ts | Persists editor state in localStorage. |
| packages/repl/src/index.tsx | REPL entry point; loads web-core/elements and renders <App />. |
| packages/repl/src/globals.css | Global theme tokens and REPL layout/handle styles. |
| packages/repl/src/examples/ui-tree-tree-traversal/main-thread.js | Adds UI tree traversal example. |
| packages/repl/src/examples/ui-tree-tree-mutations/main-thread.js | Adds UI tree mutations example. |
| packages/repl/src/examples/ui-tree-element-types/main-thread.js | Adds element types/props example. |
| packages/repl/src/examples/ui-tree-creating-elements/main-thread.js | Adds element factory example. |
| packages/repl/src/examples/threading-cross-thread/main-thread.js | Adds cross-thread communication example (main). |
| packages/repl/src/examples/threading-cross-thread/background.js | Adds cross-thread communication example (bg). |
| packages/repl/src/examples/styling-inline-styles/main-thread.js | Adds inline styles example. |
| packages/repl/src/examples/styling-css-classes-and-scoping/main-thread.js | Adds CSS classes + scoping example (main). |
| packages/repl/src/examples/styling-css-classes-and-scoping/index.css | Adds CSS file for CSS classes example. |
| packages/repl/src/examples/list-virtualization/main-thread.js | Adds list virtualization example. |
| packages/repl/src/examples/lifecycle-render/main-thread.js | Adds render/update lifecycle example (main). |
| packages/repl/src/examples/lifecycle-render/background.js | Adds lifecycle signal observation + updates (bg). |
| packages/repl/src/examples/lifecycle-raf/main-thread.js | Adds requestAnimationFrame example. |
| packages/repl/src/examples/lifecycle-events/main-thread.js | Adds lifecycle events + page root example. |
| packages/repl/src/examples/interactivity-selector-query/main-thread.js | Adds selector query + measurement example. |
| packages/repl/src/examples/interactivity-refs-main-thread/main-thread.js | Adds main-thread refs/measurement example. |
| packages/repl/src/examples/interactivity-refs-bg/main-thread.js | Adds background-thread identity via event metadata example (main). |
| packages/repl/src/examples/interactivity-refs-bg/background.js | Adds background-thread event routing + state example (bg). |
| packages/repl/src/examples/interactivity-get-element-by-id/main-thread.js | Adds ID lookup example. |
| packages/repl/src/examples/interactivity-event-main-thread/main-thread.js | Adds worklet/main-thread event handling example. |
| packages/repl/src/examples/interactivity-event-handling/main-thread.js | Adds bind vs catch event propagation example (main). |
| packages/repl/src/examples/interactivity-event-handling/background.js | Adds doc-only background notes for propagation example. |
| packages/repl/src/examples/interactivity-event-bg/main-thread.js | Adds background-thread string handler event example (main). |
| packages/repl/src/examples/interactivity-event-bg/background.js | Adds publishEvent routing + counter state (bg). |
| packages/repl/src/examples/global-system-info/main-thread.js | Adds SystemInfo API example. |
| packages/repl/src/examples/global-props/main-thread.js | Adds lynx.__globalProps example. |
| packages/repl/src/examples/global-props-theme/main-thread.js | Adds theme-via-globalProps example (main). |
| packages/repl/src/examples/global-props-theme/background.js | Adds GlobalEventEmitter theme forwarding (bg). |
| packages/repl/src/examples/global-fetch/main-thread.js | Adds fetch result rendering example (main). |
| packages/repl/src/examples/global-fetch/background.js | Adds fetch execution example (bg). |
| packages/repl/src/examples/global-console/main-thread.js | Adds console output demo (main). |
| packages/repl/src/examples/global-console/background.js | Adds console output demo (bg). |
| packages/repl/src/examples/demo-data-driven-list/main-thread.js | Adds data-driven list demo (main). |
| packages/repl/src/examples/demo-data-driven-list/background.js | Adds data source simulation (bg). |
| packages/repl/src/examples/demo-background-counter/main-thread.js | Adds background-driven counter demo (main). |
| packages/repl/src/examples/demo-background-counter/background.js | Adds counter tick producer (bg). |
| packages/repl/src/examples/attributes-set-and-get/main-thread.js | Adds attributes read/write example. |
| packages/repl/src/examples/attributes-dataset/main-thread.js | Adds dataset APIs example. |
| packages/repl/src/env.d.ts | Adds ?raw module typings for examples/types injection. |
| packages/repl/src/editor.ts | Monaco setup, models, workers, and type injection. |
| packages/repl/src/defaults/main-thread.js.txt | Adds a default main-thread snippet (text file). |
| packages/repl/src/defaults/background.js.txt | Adds a default background snippet (text file). |
| packages/repl/src/defaults.ts | Adds default template strings for editor initialization. |
| packages/repl/src/console/useConsole.ts | React hook to collect console messages via BroadcastChannel. |
| packages/repl/src/console/types.ts | Console message/entry types. |
| packages/repl/src/console/console-wrapper.ts | Generates injected console wrapper code for preview threads. |
| packages/repl/src/components/ui/separator.tsx | UI separator primitive. |
| packages/repl/src/components/ui/select.tsx | UI select primitives (Radix Select). |
| packages/repl/src/components/ui/resizable.tsx | Wrapper around react-resizable-panels. |
| packages/repl/src/components/ui/button.tsx | UI button primitive (CVA variants). |
| packages/repl/src/components/PreviewPane.tsx | Preview + console panel layout (resizable). |
| packages/repl/src/components/LynxPreview.tsx | <lynx-view> host + boot/reload + error overlay. |
| packages/repl/src/components/Header.tsx | Header controls (layout/theme/share/sample picker). |
| packages/repl/src/components/EditorWindow.tsx | Collapsible editor window header/body components. |
| packages/repl/src/components/EditorPane.tsx | Monaco panel group with collapse/distribution logic. |
| packages/repl/src/components/ConsolePanel.tsx | Console UI with filtering and auto-scroll. |
| packages/repl/src/bundler/template-builder.ts | Builds LynxTemplate and injects console wrappers + CSS processing. |
| packages/repl/src/bundler/css-processor.ts | In-browser CSS parsing and StyleInfo generation. |
| packages/repl/src/App.tsx | App orchestration: state resolution, rebuild loop, layout, sharing. |
| packages/repl/scripts/collect-lynx-types.mjs | Generates a lynx-types-map.json for Monaco type injection. |
| packages/repl/rsbuild.config.ts | Rsbuild config including Monaco warning filters and assetPrefix support. |
| packages/repl/postcss.config.mjs | PostCSS setup for Tailwind. |
| packages/repl/package.json | Declares REPL package deps/scripts. |
| packages/repl/index.html | Base HTML shell (critical background CSS). |
| packages/repl/README.md | REPL architecture and capability documentation. |
| packages/repl/.gitignore | Ignores generated type map output directory. |
| eslint.config.js | Excludes REPL examples/config from linting. |
| biome.jsonc | Excludes REPL examples from Biome checks. |
| .github/workflows/workflow-website.yml | Builds REPL and copies it into Pages artifact under /repl/. |
| .github/web-background-dispose.instructions.md | Documents dispose-handler teardown guard requirement. |
| .github/repl.instructions.md | Documents REPL-specific implementation conventions. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| export function encodeCode(code: CodeState): string { | ||
| const json = JSON.stringify({ | ||
| mainThread: code.mainThread, | ||
| background: code.background, | ||
| css: code.css, | ||
| }); | ||
| return btoa(unescape(encodeURIComponent(json))); | ||
| } | ||
|
|
||
| /** | ||
| * Base64-decode a CodeState object. Returns null on failure. | ||
| */ | ||
| function decodeCode(encoded: string): CodeState | null { | ||
| try { | ||
| const json = decodeURIComponent(escape(atob(encoded))); | ||
| const parsed = JSON.parse(json) as CodeState; | ||
| if (typeof parsed.mainThread === 'string') { |
There was a problem hiding this comment.
encodeCode/decodeCode rely on escape/unescape for UTF-8 handling. These APIs are deprecated and can behave unexpectedly for some Unicode inputs. Prefer a TextEncoder/TextDecoder-based approach (and ideally base64url) to avoid deprecated globals and improve robustness.
| <ResizablePanel | ||
| id={win.id} | ||
| defaultSize='33%' | ||
| minSize={60} | ||
| collapsible | ||
| collapsedSize={COLLAPSED_SIZE_PX} | ||
| onResize={makeResizeHandler(i)} |
There was a problem hiding this comment.
ResizablePanel sizing looks inconsistent with the rest of the codebase: defaultSize is a string ('33%') while other panels pass numbers, and minSize/collapsedSize are treated as pixel constants elsewhere in this component. This is likely to break react-resizable-panels sizing/constraints (or even fail TS props validation). Consider using numeric sizes consistently (e.g., defaultSize={33}) and confirm that minSize/collapsedSize/resize() are using the units expected by react-resizable-panels.
| if (!scheduled) { | ||
| scheduled = true; | ||
| requestAnimationFrame(() => { | ||
| setEntries(prev => [...prev, ...buffer.splice(0)]); | ||
| scheduled = false; | ||
| }); |
There was a problem hiding this comment.
useConsole batches messages via requestAnimationFrame, but the scheduled callback isn’t cancelled/guarded on cleanup. If the component unmounts between scheduling and the rAF firing, setEntries can run after unmount. Track the rAF id and cancel it in the effect cleanup (or guard with an isMounted flag).
| env: | ||
| ASSET_PREFIX: ${{ steps.pages.outputs.base_path }}/repl/ | ||
| - name: Copy REPL into website output | ||
| run: cp -r packages/repl/dist website/doc_build/repl |
There was a problem hiding this comment.
The copy step can produce an unexpected directory layout if website/doc_build/repl already exists (cp will nest dist under repl/dist), and it may fail if website/doc_build isn’t created by the docs build. Safer approach is to ensure the target dir exists and remove any previous repl output (e.g., rm -rf website/doc_build/repl && mkdir -p website/doc_build && cp -r packages/repl/dist website/doc_build/repl).
| run: cp -r packages/repl/dist website/doc_build/repl | |
| run: | | |
| rm -rf website/doc_build/repl | |
| mkdir -p website/doc_build | |
| cp -r packages/repl/dist website/doc_build/repl |
| - **Step 3' (planned):** Parse user CSS in-browser via `@lynx-js/css-serializer` (pure JS, based on `csstree`) and `genStyleInfo` (pure TS) to produce `styleInfo`. | ||
| - **Step 6':** Directly construct a `LynxTemplate` JS object from the user's code strings and the processed `styleInfo`. No webpack compilation, no asset pipeline. | ||
| - **Delivery:** The template object is handed to `<lynx-view>` via the `customTemplateLoader` callback — no encoding, no file I/O, no URL fetch. | ||
|
|
||
| Steps 1, 2, 4, 5 are skipped entirely because the user writes final-form JS (no JSX, no imports, no multi-file). Steps 7–8 are skipped because `<lynx-view>` can consume a `LynxTemplate` object directly. | ||
|
|
||
| ## Functional Boundaries | ||
|
|
||
| What this REPL **can** do: | ||
|
|
||
| - All Element PAPIs (`__CreateElement`, `__AppendElement`, `__AddInlineStyle`, etc.) | ||
| - CSS class selectors, `@keyframes`, `@font-face`, CSS variables (after L1 is implemented) | ||
| - Inline styles via `__AddInlineStyle` | ||
| - Real-time preview with Lynx Web runtime (`<lynx-view>`) |
There was a problem hiding this comment.
README describes CSS class selector support as "planned"/"after L1 is implemented", but this PR already includes an in-browser CSS processor (processCSS) and enables CSS selector support in the generated template. Update this section so it reflects the current capabilities (or clarify what’s still missing) to avoid misleading users.
| ...props | ||
| }: React.ComponentProps<typeof Group>) { | ||
| const directionClass = props.orientation === 'vertical' | ||
| ? 'flex flex-col' | ||
| : 'flex flex-row'; | ||
| return ( | ||
| <Group | ||
| className={cn('h-full w-full', directionClass, className)} | ||
| {...props} |
There was a problem hiding this comment.
This wrapper uses props.orientation and passes it through to , but the repo’s own REPL guidance says to use the react-resizable-panels "direction" prop on ResizablePanelGroup. Please reconcile this (either map orientation -> direction here, or update call sites/docs) so panel layout is driven by the correct prop consistently.
| ...props | |
| }: React.ComponentProps<typeof Group>) { | |
| const directionClass = props.orientation === 'vertical' | |
| ? 'flex flex-col' | |
| : 'flex flex-row'; | |
| return ( | |
| <Group | |
| className={cn('h-full w-full', directionClass, className)} | |
| {...props} | |
| direction: directionProp, | |
| orientation, | |
| ...props | |
| }: React.ComponentProps<typeof Group> & { | |
| orientation?: 'horizontal' | 'vertical'; | |
| }) { | |
| // Prefer the official `direction` prop (react-resizable-panels), | |
| // but fall back to legacy `orientation` for backwards compatibility. | |
| const direction = | |
| directionProp ?? | |
| (orientation === 'vertical' ? 'vertical' : 'horizontal'); | |
| const directionClass = direction === 'vertical' | |
| ? 'flex flex-col' | |
| : 'flex flex-row'; | |
| return ( | |
| <Group | |
| {...props} | |
| direction={direction} | |
| className={cn('h-full w-full', directionClass, className)} |
| export function getInitialState(): InitialState { | ||
| const params = new URLSearchParams(window.location.search); | ||
|
|
||
| const encodedCode = params.get('c'); | ||
| if (encodedCode) { | ||
| const code = decodeCode(encodedCode); | ||
| if (code) return { type: 'custom', code }; | ||
| } | ||
|
|
||
| const sampleParam = params.get('s'); | ||
| if (sampleParam !== null) { | ||
| const index = Number(sampleParam); | ||
| if (Number.isInteger(index) && index >= 0 && index < samples.length) { | ||
| return { type: 'sample', sampleIndex: index }; | ||
| } | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| /** | ||
| * Update the URL with encoded custom code (replaceState to avoid history spam). | ||
| */ | ||
| export function saveToUrl(code: CodeState): void { | ||
| const encoded = encodeCode(code); | ||
| const url = new URL(window.location.href); | ||
| url.searchParams.delete('s'); | ||
| url.searchParams.set('c', encoded); | ||
| window.history.replaceState({}, '', url); |
There was a problem hiding this comment.
PR description says shareable state is stored in the URL hash, but this implementation reads/writes query params (window.location.search / url.searchParams). This mismatch can confuse users and also affects how static hosts treat URLs. Either switch to encoding state into location.hash (e.g., #c=.../#s=...) or update the PR description/docs to match the query-string behavior.
...core-wasm/ts/client/background/background-apis/crossThreadHandlers/registerDisposeHandler.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 16
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
🟡 Minor comments (11)
packages/repl/src/examples/global-system-info/main-thread.js-23-24 (1)
23-24:⚠️ Potential issue | 🟡 MinorAvoid hard failure when
SystemInfois unavailable at call time.
const info = SystemInfo;will throw if the global isn’t injected yet. Since the note already referenceslynx.SystemInfo, use a safe fallback chain.Suggested fix
- const info = SystemInfo; + const info = globalThis.SystemInfo ?? globalThis.lynx?.SystemInfo ?? {};Use this read-only check to confirm whether
SystemInfois guaranteed globally beforerenderPageruns:#!/bin/bash set -euo pipefail echo "== SystemInfo declarations / injections ==" rg -n -C3 'SystemInfo|lynx\.SystemInfo|globalThis\.SystemInfo|globalThis\.lynx' --type js --type jsx --type ts --type tsx echo echo "== Ambient type declarations ==" rg -n -C2 'declare (const|var) SystemInfo|interface SystemInfo|type SystemInfo' --type ts --type dtsExpected result: clear evidence of guaranteed injection timing + ambient declaration; otherwise keep the fallback.
Also applies to: 50-53
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/src/examples/global-system-info/main-thread.js` around lines 23 - 24, Replace the direct global access that throws (const info = SystemInfo) with a safe, read-only fallback chain that checks globalThis.SystemInfo, globalThis.lynx?.SystemInfo and falls back to a safe empty/read-only object so renderPage and any downstream code never throws when the global isn’t injected; update the same pattern where SystemInfo is read elsewhere (the other occurrences around the render logic) and keep any ambient type checks or runtime guards intact so behavior is unchanged when the global is present.packages/repl/src/examples/interactivity-selector-query/main-thread.js-39-58 (1)
39-58:⚠️ Potential issue | 🟡 MinorAdd error handling to prevent stuck "Measuring..." UI state.
The code at lines 41 and 45 lacks error handling: if
__QuerySelectorfails to find the element orboundingClientRectreturns a non-zero code, the UI remains in the loading state indefinitely. Add explicit error messages for both failure paths.Suggested patch
// After layout, query and measure setTimeout(() => { // __QuerySelector — find element by CSS selector const found = __QuerySelector(page, "#measure-target"); - if (found) { - // __InvokeUIMethod — call "boundingClientRect" to measure layout - __InvokeUIMethod(found, "boundingClientRect", {}, (result) => { - if (result.code === 0) { - const r = result.data; - __SetAttribute(resultRaw, "text", - "boundingClientRect:\n" + - " width: " + r.width.toFixed(0) + "px\n" + - " height: " + r.height.toFixed(0) + "px\n" + - " top: " + r.top.toFixed(0) + "px\n" + - " left: " + r.left.toFixed(0) + "px" - ); - __FlushElementTree(); - } - }); - } + if (!found) { + __SetAttribute(resultRaw, "text", "Error: `#measure-target` not found"); + __FlushElementTree(); + return; + } + + // __InvokeUIMethod — call "boundingClientRect" to measure layout + __InvokeUIMethod(found, "boundingClientRect", {}, (result) => { + if (result.code !== 0) { + __SetAttribute( + resultRaw, + "text", + `Error: boundingClientRect failed (code ${result.code})`, + ); + __FlushElementTree(); + return; + } + + const r = result.data; + __SetAttribute(resultRaw, "text", + "boundingClientRect:\n" + + " width: " + r.width.toFixed(0) + "px\n" + + " height: " + r.height.toFixed(0) + "px\n" + + " top: " + r.top.toFixed(0) + "px\n" + + " left: " + r.left.toFixed(0) + "px" + ); + __FlushElementTree(); + }); }, 100);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/src/examples/interactivity-selector-query/main-thread.js` around lines 39 - 58, The UI can stay stuck showing "Measuring..." because there's no error handling when __QuerySelector(page, "#measure-target") returns null/undefined or when __InvokeUIMethod callback returns result.code !== 0; update the setTimeout block to detect those failure paths: if __QuerySelector returns falsy, call __SetAttribute(resultRaw, "text", "Error: element not found") and __FlushElementTree(); and within the __InvokeUIMethod callback handle result.code !== 0 by setting a clear error string via __SetAttribute(resultRaw, "text", "Error measuring element: " + (result.error || result.code)) and __FlushElementTree(); ensure both branches short-circuit so the loading state is cleared.packages/repl/README.md-19-30 (1)
19-30:⚠️ Potential issue | 🟡 MinorAdd fence languages to keep markdown lint passing.
Line 19 and Line 34 use fenced code blocks without a language label, which triggers
MD040.🔧 Proposed fix
-``` +```text Step What happens Tool / Plugin ──── ────────────────────────────────── ───────────────────────────── 1. Source Transform (JSX/TS → JS) rspack loader (SWC) 2. Module Resolution (import/require) rspack resolver 3. CSS Processing (CSS → LynxStyleNode) `@lynx-js/css-serializer` 4. Bundling (multi-file → chunks) rspack 5. Asset Tagging (lynx:main-thread) MarkMainThreadPlugin 6. Template Assembly (assets → data) LynxTemplatePlugin 7. Encoding (data → binary/JSON) `@lynx-js/tasm` / WebEncodePlugin 8. Emit (write to disk) rspack compilation@@
-+text
rspeedy dev (full pipeline):
[1] → [2] → [3] → [4] → [5] → [6] → [7] → [8] → lynx-view (via URL fetch)This REPL (pure browser):
[3'] → [6'] ─────────→ lynx-view (via callback)Also applies to: 34-40
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/README.md` around lines 19 - 30, The README contains fenced code blocks without language labels which triggers MD040; update the two fences shown (the ASCII pipeline table and the rspeedy/dev snippet) to include a language tag (e.g., change ``` to ```text) so the markdown linter stops flagging them, ensuring both the top table block and the later pipeline example use ```text fences.packages/repl/scripts/collect-lynx-types.mjs-45-86 (1)
45-86:⚠️ Potential issue | 🟡 MinorNormalize generated declaration paths to POSIX separators.
relative()is platform-dependent and produces\on Windows, which breaks Monaco import resolution for virtual files since they expect/separators.Proposed fix
results.push({ - relativePath: relative(baseDir, fullPath), + relativePath: relative(baseDir, fullPath).replaceAll('\\', '/'), content: readFileSync(fullPath, 'utf-8'), });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/scripts/collect-lynx-types.mjs` around lines 45 - 86, The relative paths produced by collectDtsFiles are platform-dependent (backslashes on Windows) which breaks Monaco; normalize paths to POSIX separators. Update collectDtsFiles to set relativePath = relative(baseDir, fullPath).replace(/\\+/g, '/') (or use path.posix.join/relative) so file.relativePath uses '/' and ensure when building typeMap keys (the loop that creates node_modules/@lynx-js/types/types/${file.relativePath}) you also use the normalized forward-slash path.packages/repl/src/components/ui/button.tsx-33-40 (1)
33-40:⚠️ Potential issue | 🟡 MinorSet a safe default
typefor button.The button component doesn't explicitly set a
typeattribute, allowing it to inherit the native HTML default ofsubmit. This can trigger unintended form submissions when the button is used within forms in embedded contexts. Defaulting totype="button"prevents this while still allowing callers to override the type if needed.Proposed fix
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( - ({ className, variant, size, ...props }, ref) => { + ({ className, variant, size, type = 'button', ...props }, ref) => { return ( <button + type={type} className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/src/components/ui/button.tsx` around lines 33 - 40, The Button component defined via React.forwardRef (the Button function using buttonVariants and cn) lacks an explicit type, causing it to default to "submit" inside forms; update the JSX <button> to provide a default type="button" while still allowing callers to override via the incoming props (i.e., derive type from props with a fallback of "button"), ensuring ref, className, variant, size, and other props remain unaffected.packages/repl/src/components/ConsolePanel.tsx-58-67 (1)
58-67:⚠️ Potential issue | 🟡 MinorAdd an explicit accessible name to the clear button.
Icon-only controls should include
aria-label(title alone is not a reliable accessible name across assistive tech).Proposed fix
<Button variant='ghost' size='icon' className='h-5 w-5' onClick={onClear} title='Clear console' + aria-label='Clear console' style={{ color: 'var(--repl-text-faint)' }} >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/src/components/ConsolePanel.tsx` around lines 58 - 67, The Clear Console button in ConsolePanel (the <Button> containing <Trash2 /> and using onClick={onClear}) is icon-only and currently only uses title; add an explicit accessible name by adding an aria-label (e.g., aria-label="Clear console") to that Button component so assistive tech receives a reliable label; ensure the aria-label string matches the visible intent and keep the existing title prop if desired for tooltips.packages/repl/src/examples/interactivity-refs-bg/main-thread.js-91-113 (1)
91-113:⚠️ Potential issue | 🟡 MinorAdd cleanup for
selectionUpdatelistener to prevent duplication ifrenderPageis called multiple times.The listener is added inside
renderPage()(lines 91–113) without removal or idempotency guard. IfrenderPageexecutes more than once in the same JS context, the listener will be registered multiple times, causing duplicate event handling. Either remove the old listener before adding a new one, move the listener registration outsiderenderPage, or add a guard to prevent re-registration.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/src/examples/interactivity-refs-bg/main-thread.js` around lines 91 - 113, The selectionUpdate listener added via lynx.getJSContext().addEventListener inside renderPage causes duplicate handlers if renderPage runs multiple times; change to register it idempotently by using a named handler and removing any existing one before adding (or store the handler reference on a module/global variable and check it). Specifically, create a function (e.g., selectionUpdateHandler) that contains the current callback logic, check for an existing handler reference (e.g., window._selectionUpdateHandler or module-scoped variable) and if present remove it with removeEventListener on lynx.getJSContext(), then assign and add the new handler with addEventListener; alternatively move the addEventListener call outside renderPage so it runs only once.packages/repl/src/console/useConsole.ts-33-40 (1)
33-40:⚠️ Potential issue | 🟡 MinorCancel pending animation frame and clean up message handler during cleanup.
The cleanup function only closes the channel, but leaves a pending RAF callback that could execute after unmount and keeps the
onmessagehandler attached. WhileBroadcastChannel.close()prevents future message delivery, it does not detach the handler reference, which unnecessarily keeps the effect closure alive.Proposed fix
const buffer: ConsoleEntry[] = []; let scheduled = false; + let rafId: number | null = null; @@ if (!scheduled) { scheduled = true; - requestAnimationFrame(() => { + rafId = requestAnimationFrame(() => { setEntries(prev => [...prev, ...buffer.splice(0)]); scheduled = false; + rafId = null; }); } }; - return () => channel.close(); + return () => { + channel.onmessage = null; + if (rafId !== null) { + cancelAnimationFrame(rafId); + } + channel.close(); + }; }, [sessionId]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/src/console/useConsole.ts` around lines 33 - 40, The effect leaves a pending requestAnimationFrame and the onmessage handler attached; store the RAF id returned by requestAnimationFrame (when scheduling the callback that calls setEntries and clears scheduled) and cancel it with cancelAnimationFrame in the cleanup, and also detach the message handler by keeping a reference to the handler (the function assigned to channel.onmessage) and removing it via channel.removeEventListener('message', handler) or setting channel.onmessage = null before calling channel.close(); ensure you still call channel.close() last.packages/repl/src/components/LynxPreview.tsx-79-117 (1)
79-117:⚠️ Potential issue | 🟡 MinorPotential re-render loop if
onLoadcallback is not memoized.The
onLoadcallback is in the dependency array (Line 117). If the parent component doesn't memoize this callback withuseCallback, every parent re-render will create a new function reference, triggering this effect unnecessarily and potentially causing repeated template reloads.Ensure the parent component wraps
onLoadinuseCallback, or consider removingonLoadfrom the dependency array and accessing it via a ref:🛠️ Alternative: Use ref for onLoad
+ const onLoadRef = useRef(onLoad); + onLoadRef.current = onLoad; + useEffect(() => { const lynxView = viewRef.current; if (!template || !lynxView) return; // ... rest of effect - onLoad?.(); + onLoadRef.current?.(); // ... - }, [template, onLoad]); + }, [template]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/src/components/LynxPreview.tsx` around lines 79 - 117, The effect in LynxPreview that sets up lynxView loading depends on the onLoad callback causing potential re-render loops if parent doesn't memoize it; fix by converting onLoad to a ref (e.g., create const onLoadRef = useRef(onLoad); update onLoadRef.current in a small effect when onLoad changes) and then use onLoadRef.current() inside the main useEffect instead of referencing onLoad directly, removing onLoad from the dependency array so the effect only depends on template (or template and renderCounter if used); alternatively, ensure callers pass a memoized onLoad via useCallback.packages/repl/src/App.tsx-237-238 (1)
237-238:⚠️ Potential issue | 🟡 MinorHandle clipboard write rejection instead of leaving a floating promise.
A rejected clipboard write currently becomes an unhandled rejection.
Suggested fix
- // eslint-disable-next-line n/no-unsupported-features/node-builtins, `@typescript-eslint/no-floating-promises` - navigator.clipboard.writeText(window.location.href); + void navigator.clipboard.writeText(window.location.href).catch(() => { + // TODO: surface copy failure in UI + });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/src/App.tsx` around lines 237 - 238, The floating promise comes from calling navigator.clipboard.writeText(window.location.href) (the line currently preceded by the eslint-disable comment); handle its rejection by awaiting it in an async handler or adding a .catch to it and log or surface the error (e.g., processLogger or UI notification) so the promise rejection is not unhandled—update the code around that call (where navigator.clipboard.writeText is invoked) to properly catch and handle errors.packages/repl/src/bundler/css-processor.ts-145-150 (1)
145-150:⚠️ Potential issue | 🟡 MinorAvoid rebuilding URL text with manual quoting.
url('${node.value}')can break valid URLs containing quotes/escaped content. Preserving parser-generated text is safer.Suggested fix
if (node.type === 'Url') { item.data = { ...node, type: 'Raw', - value: `url('${node.value}')`, + value: csstree.generate(node), }; } else if (node.type === 'Comment') {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/src/bundler/css-processor.ts` around lines 145 - 150, The code reconstructs URL text with url('${node.value}') inside the Url branch (when node.type === 'Url') which can mangle quotes/escapes; instead set item.data.value to the parser-produced/raw text (e.g. use the node's raw/text representation such as node.valueRaw or node.toString()/node.raw if available) and keep item.data.type = 'Raw' — update the assignment in the Url handling block that sets item.data so it uses the original parser-generated text rather than manually wrapping node.value in quotes.
🧹 Nitpick comments (12)
packages/repl/src/examples/attributes-dataset/main-thread.js (1)
33-33: Guard the fixed index for future-proofing.At Line 33,
elements[1]is currently safe with the hardcoded sample list, but a small guard avoids accidental runtime errors if the sample list is edited later.Suggested patch
- __AddDataset(elements[1], "extra", "added-via-AddDataset"); + if (elements.length > 1) { + __AddDataset(elements[1], "extra", "added-via-AddDataset"); + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/src/examples/attributes-dataset/main-thread.js` at line 33, Guard the hard-coded elements[1] access before calling __AddDataset to avoid runtime errors if the samples list changes: before calling __AddDataset(elements[1], "extra", "added-via-AddDataset"), check elements.length > 1 and only call with elements[1] when true, otherwise either fall back to a safe element (e.g., elements[0]) or skip/log an error; update the call site where __AddDataset is invoked to use the guarded value or early-return to protect AddDataset from an undefined argument.packages/repl/src/examples/interactivity-get-element-by-id/main-thread.js (1)
51-60: Replace fixed delay lookup with deterministic execution.At Line 51,
setTimeout(..., 100)makes this example timing-dependent and can fire after quick reload/example switches. Prefer immediate post-flush lookup to keep behavior deterministic.Suggested change
__FlushElementTree(); - // After layout, query each element by its ID - setTimeout(() => { - const results = []; - for (const { id } of boxes) { - // __QuerySelector with "#id" — equivalent to getElementById - const el = __QuerySelector(page, "#" + id); - results.push(id + ": " + (el ? "found" : "not found")); - } - __SetAttribute(resultRaw, "text", results.join("\n")); - __FlushElementTree(); - }, 100); + // Query each element by its ID after initial flush + const results = []; + for (const { id } of boxes) { + // __QuerySelector with "#id" — equivalent to getElementById + const el = __QuerySelector(page, "#" + id); + results.push(id + ": " + (el ? "found" : "not found")); + } + __SetAttribute(resultRaw, "text", results.join("\n")); + __FlushElementTree(); };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/src/examples/interactivity-get-element-by-id/main-thread.js` around lines 51 - 60, Remove the setTimeout-based delay and make the lookup run immediately after flushing the element tree: call __FlushElementTree() first (instead of inside the timeout), then iterate boxes and call __QuerySelector(page, "#" + id) for each id, collect results into results[], and finally call __SetAttribute(resultRaw, "text", results.join("\n")); this keeps the behavior deterministic and avoids the timing-dependent setTimeout wrapper.packages/repl/src/components/Header.tsx (2)
126-144: Edge case:Number('')returns0for the disabled option.If the "Custom code" disabled option is somehow programmatically selected or manipulated,
Number(e.target.value)on an empty string would return0, inadvertently selecting the first sample. Since it's a disabled option, this shouldn't occur in normal user interaction, but defensive parsing could prevent unexpected behavior.🛡️ Defensive parsing
value={sampleIndex ?? ''} - onChange={(e) => onSampleChange(Number(e.target.value))} + onChange={(e) => { + const val = e.target.value; + if (val !== '') onSampleChange(Number(val)); + }} title='Choose example'🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/src/components/Header.tsx` around lines 126 - 144, The select change handler can convert an empty-string disabled option into 0 because Number('') === 0; update the onChange logic in the Header component to defensively parse e.target.value and call onSampleChange with a null (or other sentinel) when the value is '' instead of Number(''); locate the select using sampleIndex, onSampleChange and groupedOptions and replace the direct Number(e.target.value) call with a conditional that checks === '' and maps that to null, otherwise parses to Number so the "Custom code" disabled option cannot accidentally select the first sample.
44-48: Consider cleaning up the timeout on unmount.If the component unmounts before the 1.5-second timeout completes,
setCopied(false)will still fire. While React 19 handles this gracefully without warnings, clearing the timeout is a best practice to avoid stale closures and potential issues if the callback logic becomes more complex.♻️ Optional cleanup pattern using useEffect
+import { useMemo, useState, useCallback, useRef, useEffect } from 'react'; export function Header({ ... }: HeaderProps) { const [copied, setCopied] = useState(false); + const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null); + + useEffect(() => { + return () => { + if (timeoutRef.current) clearTimeout(timeoutRef.current); + }; + }, []); const handleShare = useCallback(() => { onShare(); setCopied(true); - setTimeout(() => setCopied(false), 1500); + if (timeoutRef.current) clearTimeout(timeoutRef.current); + timeoutRef.current = setTimeout(() => setCopied(false), 1500); }, [onShare]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/src/components/Header.tsx` around lines 44 - 48, The handleShare callback sets a 1.5s timeout to clear setCopied but never clears it on unmount; modify handleShare to save the timeout id (e.g., in a ref like shareTimeoutRef) and clear any existing timeout before creating a new one, and add a useEffect cleanup that clears shareTimeoutRef.current on unmount so setCopied(false) won't run after the component is torn down; reference the handleShare function, onShare prop, setCopied state updater, and the new shareTimeoutRef/useEffect cleanup to locate where to change the code.packages/repl/src/examples/ui-tree-element-types/main-thread.js (1)
23-30: Consider extracting repeated section-label construction into a helper.The same create/append/style sequence is repeated 3 times; extracting it will reduce drift and make future example edits easier.
♻️ Suggested refactor
+const SECTION_LABEL_STYLE = 'font-size:14px; font-weight:600; margin-top:20px;'; + +function appendSectionLabel(container, text) { + const label = __CreateText(0); + __AppendElement(container, label); + __AppendElement(label, __CreateRawText(text)); + __SetInlineStyles(label, SECTION_LABEL_STYLE); + return label; +} + - const viewLabel = __CreateText(0); - __AppendElement(container, viewLabel); - __AppendElement(viewLabel, __CreateRawText('View — layout container')); - __SetInlineStyles( - viewLabel, - 'font-size:14px; font-weight:600; margin-top:20px;', - ); + appendSectionLabel(container, 'View — layout container'); ... - const textLabel = __CreateText(0); - __AppendElement(container, textLabel); - __AppendElement(textLabel, __CreateRawText('Text — styled text display')); - __SetInlineStyles( - textLabel, - 'font-size:14px; font-weight:600; margin-top:20px;', - ); + appendSectionLabel(container, 'Text — styled text display'); ... - const imgLabel = __CreateText(0); - __AppendElement(container, imgLabel); - __AppendElement(imgLabel, __CreateRawText('Image — src attribute')); - __SetInlineStyles( - imgLabel, - 'font-size:14px; font-weight:600; margin-top:20px;', - ); + appendSectionLabel(container, 'Image — src attribute');Also applies to: 54-60, 85-91
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/src/examples/ui-tree-element-types/main-thread.js` around lines 23 - 30, Several blocks repeat the sequence __CreateText -> __AppendElement -> __AppendElement(__CreateRawText(...)) -> __SetInlineStyles (seen for viewLabel and at the other ranges); extract this into a small helper (e.g., createSectionLabel or createLabeledSection) that accepts the parent container, the label text, and optional styles, and inside performs __CreateText, appends it to the parent, appends __CreateRawText(text) into the created text node, and calls __SetInlineStyles; replace the three repeated sequences with calls to that helper passing container, the label string, and the style string.packages/repl/src/examples/styling-inline-styles/main-thread.js (1)
30-31: Factor shared style fragments to prevent string drift.The same box style chunks are repeated across initial render and delayed updates. Extracting shared parts keeps this example easier to maintain.
♻️ Optional cleanup
+const BOX_BASE_STYLE = + "width:200px; height:60px; border-radius:8px; align-items:center; justify-content:center;"; +const BOX_WITH_MARGIN_STYLE = `${BOX_BASE_STYLE} margin-top:12px;`; + globalThis.renderPage = function renderPage() { @@ - __SetInlineStyles(box2, "width:200px; height:60px; background-color:`#8b5cf6`; border-radius:8px; margin-top:12px; align-items:center; justify-content:center;"); + __SetInlineStyles(box2, `${BOX_WITH_MARGIN_STYLE} background-color:`#8b5cf6`;`); @@ - __SetInlineStyles(box1, "width:200px; height:60px; background-color:`#ef4444`; border-radius:8px; align-items:center; justify-content:center;"); - __SetInlineStyles(box2, "width:200px; height:60px; background-color:`#10b981`; border-radius:8px; margin-top:12px; align-items:center; justify-content:center;"); + __SetInlineStyles(box1, `${BOX_BASE_STYLE} background-color:`#ef4444`;`); + __SetInlineStyles(box2, `${BOX_WITH_MARGIN_STYLE} background-color:`#10b981`;`);Also applies to: 40-41
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/src/examples/styling-inline-styles/main-thread.js` around lines 30 - 31, The inline style string used with __SetInlineStyles for box2 is duplicated across the initial render and later updates; extract the common style fragments into a single constant (e.g., const BOX_BASE_STYLE) and use that constant when calling __SetInlineStyles (referencing __SetInlineStyles and box2) both in the initial creation and in the delayed update blocks (also apply the same extraction for the repeated styles around the other occurrence noted at the block that affects label2/box2), so updates only change the variable parts while shared style chunks are referenced once.packages/repl/src/examples/list-virtualization/main-thread.js (1)
52-58: Use immediate async scheduling instead of 100ms delay for consistency with other examples.The 100ms timeout is the only instance of a fixed delay in the REPL examples. All other
setTimeoutcalls use 0ms (or implicit) scheduling. For example/test code, this maintains consistency and removes unnecessary latency without functional impact.🛠️ Proposed refactor
- }, 100); + }, 0);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/src/examples/list-virtualization/main-thread.js` around lines 52 - 58, Replace the fixed 100ms delay with immediate async scheduling to match other examples: in the block that calls setTimeout(() => { __SetAttribute(list, "update-list-info", { ... }); __FlushElementTree(); }, 100) change the scheduling to a 0ms (or similarly immediate) async callback so __SetAttribute and __FlushElementTree on the list run without the unnecessary 100ms latency; keep the same payload structure and callsites (list, __SetAttribute, __FlushElementTree) unchanged.packages/repl/src/examples/interactivity-event-main-thread/main-thread.js (1)
52-56: Extract repeated color-box style construction.The same long inline style string is duplicated in initial render and tap handler. A tiny helper (e.g.,
setColorBoxStyle(color)) reduces drift risk.🧹 Suggested cleanup
+ function setColorBoxStyle(color) { + __SetInlineStyles( + colorBox, + 'width:200px; height:200px; background-color:' + color + + '; border-radius:16px; align-items:center; justify-content:center; margin-bottom:16px;', + ); + } + - __SetInlineStyles( - colorBox, - 'width:200px; height:200px; background-color:' + colors[0] - + '; border-radius:16px; align-items:center; justify-content:center; margin-bottom:16px;', - ); + setColorBoxStyle(colors[0]); ... - __SetInlineStyles( - colorBox, - 'width:200px; height:200px; background-color:' + colors[colorIndex] - + '; border-radius:16px; align-items:center; justify-content:center; margin-bottom:16px;', - ); + setColorBoxStyle(colors[colorIndex]);Also applies to: 84-88
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/src/examples/interactivity-event-main-thread/main-thread.js` around lines 52 - 56, The inline style string for colorBox is duplicated; create a small helper function (e.g., setColorBoxStyle(color)) that constructs the style string and calls __SetInlineStyles(colorBox, style) so both the initial render and the tap handler reuse it; update calls in the initial render block and the tap handler to call setColorBoxStyle(colors[0]) / setColorBoxStyle(color) respectively and remove the duplicated concatenation logic.packages/repl/tsconfig.json (1)
1-18: Extend root tsconfig to inherit strict configuration and avoid divergence.packages/repl should extend the root tsconfig.json like all other packages in the monorepo. This ensures it inherits the strictest mode configuration and avoids configuration drift. The root config provides essential flags like
verbatimModuleSyntax,isolatedDeclarations, andforceConsistentCasingInFileNamesthat should apply uniformly across the codebase.♻️ Suggested change
{ + "extends": "../../tsconfig.json", "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "bundler", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, "jsx": "react-jsx", "outDir": "./dist", "rootDir": "./src", "baseUrl": ".", "paths": { "@/*": ["./src/*"] } }, "include": ["src"] }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/tsconfig.json` around lines 1 - 18, Add an "extends" entry to this package tsconfig to inherit the root configuration and remove duplicated strict flags: replace the top-level "compilerOptions" block by adding "extends": "<root tsconfig path>" (pointing to the workspace root tsconfig) so the package inherits verbatimModuleSyntax, isolatedDeclarations, forceConsistentCasingInFileNames, etc.; keep or override only package-specific options such as "outDir", "rootDir", and the "paths" mapping, and ensure "include": ["src"] remains.packages/repl/src/local-storage.ts (1)
3-7: Avoid duplicatingCodeStatein multiple modules.This type is already exported from
packages/repl/src/url-state.ts(Line 3-7). Reusing the shared type avoids drift.Proposed refactor
+import type { CodeState } from './url-state.js'; -type CodeState = { - mainThread: string; - background: string; - css: string; -};🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/src/local-storage.ts` around lines 3 - 7, Remove the duplicated CodeState type definition in local-storage.ts and import the shared CodeState exported from url-state.ts instead; update the top of local-storage.ts to import { CodeState } from the module that exports it, remove the local type declaration, and ensure all references in functions/variables within local-storage.ts still use CodeState; run type-check/build to verify the import path is correct and update any relative import path if necessary.packages/repl/src/components/ConsolePanel.tsx (1)
1-1: Avoid broad file-level ESLint disables here.Please narrow this to only truly unavoidable rules and fix local violations inline. Blanket disables make regressions harder to catch.
As per coding guidelines, "Follow ESLint rules as configured in eslint.config.js, including React and TypeScript-specific rules".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/src/components/ConsolePanel.tsx` at line 1, This file contains a blanket file-level ESLint disable (the top comment in ConsolePanel.tsx) — remove that broad disable and either fix the offending lint violations or scope disables to the exact lines/rules; specifically drop the file-wide "/* eslint-disable headers/header-format, sort-imports, import/order, n/file-extension-in-import, `@typescript-eslint/no-unnecessary-type-assertion` */", run the linter to identify concrete failures (imports ordering, header format, unnecessary type assertions, etc.), fix them inside the ConsolePanel component and its imports (e.g., adjust import order, add the required header, remove unnecessary type assertions in functions or JSX), and for any true false-positive or unavoidable cases add minimal inline disables like // eslint-disable-next-line <rule-name> immediately above the specific statement instead of a file-level disable.packages/repl/src/components/LynxPreview.tsx (1)
6-6: Consider instance-scoped render counter.The module-level
renderCounteris shared across allLynxPreviewinstances. While this works for a single REPL, if multiple instances exist or during HMR, the counter state could lead to unexpected behavior. Consider using a ref instead:♻️ Optional: Use instance-scoped counter
-let renderCounter = 0; - interface LynxPreviewProps {And inside the component:
const renderCounterRef = useRef(0); // Then use renderCounterRef.current++ instead of renderCounter++🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/src/components/LynxPreview.tsx` at line 6, The module-level renderCounter is shared across all LynxPreview instances; change it to an instance-scoped ref by removing or ignoring the top-level renderCounter and adding a useRef inside the LynxPreview component (e.g., renderCounterRef = useRef(0)) and replace uses of renderCounter++ with renderCounterRef.current++ so each component has its own counter and HMR/multiple-instance issues are avoided.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.github/workflows/workflow-website.yml:
- Around line 39-43: The ASSET_PREFIX environment value for the "Build REPL"
step can become "//repl/" when steps.pages.outputs.base_path is "/", so update
the ASSET_PREFIX assignment to strip a trailing slash from
steps.pages.outputs.base_path using bash parameter expansion (e.g., remove
trailing slash from the base_path output) before concatenating "/repl/"; modify
the "Build REPL" step's env ASSET_PREFIX to use that expansion so asset paths
never contain a double-slash.
In `@biome.jsonc`:
- Around line 61-62: Remove the broad exclusion "packages/repl/src/examples/**"
from biome.jsonc so example files are linted, and instead add scoped
suppressions for the REPL platform globals by creating a .biome.jsonc in
packages/repl/src/examples (or adding a per-directory "overrides" entry) that
declares the platform identifiers as globals (e.g., "__CreateElement",
"__SetAttribute", "__SetStyle", etc.) or uses targeted rule disables for those
names; this preserves linting (unused/unused-import checks) while preventing
false positives from the REPL platform APIs.
In `@packages/repl/src/App.tsx`:
- Around line 54-63: When handling the url-derived sample branch in App.tsx (the
urlState?.type === 'sample' block), validate urlState.sampleIndex before
dereferencing sample.mainThread: ensure sampleIndex is a finite integer and 0 <=
sampleIndex < samples.length; if invalid, fall back to a safe default (e.g.,
sampleIndex = 0 or return the existing default code state) rather than indexing
into samples. Update the logic around urlState, sampleIndex and samples so you
only assign sample = samples[sampleIndex] after the bounds check to prevent
crashes from malformed hashes.
In `@packages/repl/src/bundler/css-processor.ts`:
- Around line 78-93: The code only captures a single token as the var() fallback
(using varFunctionValues[2]), truncating multi-token fallbacks; update the logic
around varFunctionValues, varDefaultValueNode, defaultValueMap and item.data so
you collect all tokens from index 2 onward (e.g., slice varFunctionValues from 2
to end), reconstruct or generate the full fallback text via csstree.generate on
the combined nodes (or wrap them into a single AST node) and store that full
string in defaultValueMap[varName] and item.data.value instead of the current
single-token value.
In `@packages/repl/src/components/EditorWindow.tsx`:
- Around line 22-27: The header toggle divs in EditorWindow.tsx are only
mouse-clickable; make them keyboard-accessible by giving the toggle elements
interactive semantics (add role="button" and tabIndex={0}) and a key handler
that calls the existing onToggle when the user presses Enter or Space (handle
keyDown for 'Enter' and ' ' / 'Spacebar'), retaining the current onClick
behavior; apply the same change to the second header toggle block referenced
around lines 51-59 so both toggle elements (the one using onToggle and the other
similar header) support keyboard activation.
In `@packages/repl/src/console/console-wrapper.ts`:
- Around line 10-53: The template string embeds sessionId and source directly
(see CHANNEL_PREFIX + '${sessionId}' and source: '${source}' inside the
generated JS) which can break/enable injection if those values contain
quotes/backslashes/newlines; update the code that builds this returned string to
escape both values by passing them through JSON.stringify (or an equivalent
safe-escaping function) before interpolation so the generated JS contains valid
quoted literals for sessionId and source.
In `@packages/repl/src/editor.ts`:
- Around line 56-69: The extra libs added via
monaco.languages.typescript.javascriptDefaults.addExtraLib (for lynxElementApi
and entries from lynxTypesMap) are being registered globally on every
createEditor(), causing accumulation; change the code to capture the IDisposable
returned from each addExtraLib (or call setExtraLibs once) and store those
disposables on the editor instance created by createEditor(), then call
dispose() on each stored IDisposable inside the editor's dispose() method to
unregister the libs and prevent leaks or duplicate types.
In `@packages/repl/src/examples/demo-data-driven-list/background.js`:
- Around line 5-13: The interval created with setInterval in this demo never
gets cleared and will be re-created on REPL re-executions; fix by storing the
interval id on a persistent global (e.g. self.__demoInterval) and first clearing
any existing interval before creating a new one, then inside the interval
handler (the closure using index, fruits, and
lynx.getCoreContext().dispatchEvent) call clearInterval(self.__demoInterval)
when index >= fruits.length (and optionally null it) so the timer stops when the
stream is exhausted and duplicate producers are prevented in the Web Worker
context.
In `@packages/repl/src/examples/global-console/main-thread.js`:
- Around line 1-25: The file currently assigns a no-op to globalThis.renderPage
which prevents host-triggered renders; instead move the page construction and
__FlushElementTree plus any initial console calls into a real function assigned
to globalThis.renderPage (e.g., wrap the code that creates page/view/title/hint,
appends elements, calls __FlushElementTree, and logs to console into a function
named renderPage) and then invoke globalThis.renderPage() once for the initial
render so hosts can call globalThis.renderPage() to re-render correctly.
In `@packages/repl/src/examples/lifecycle-raf/main-thread.js`:
- Around line 35-55: The animation schedules requestAnimationFrame without
tracking IDs, so recreate a stored RAF id and cancellation: add a module-scoped
variable (e.g., rafId), set rafId = lynx.requestAnimationFrame(animate) each
time you schedule the next frame inside animate and when starting the loop,
implement a cleanup function (e.g., stopAnimation or cancelRaf) that checks
rafId and calls lynx.cancelAnimationFrame(rafId) and clears rafId, and ensure
you call that cleanup before starting a new run (before calling renderPage /
before scheduling the new lynx.requestAnimationFrame(animate)) so prior loops
are cancelled. Use the animate function name, rafId variable, and
stopAnimation/cancelRaf function to locate and update the code.
In `@packages/repl/src/examples/lifecycle-render/background.js`:
- Around line 10-11: The console.log call uses JSON.stringify(args[0]) which can
throw on circular structures or unsupported types and prevent forwarding to the
original handler; wrap the JSON.stringify in a try/catch (or use a safe
stringify fallback) inside the same scope so any stringify error is caught and
replaced with a safe fallback string (e.g., "[unserializable payload]" or use a
replacer/inspection), and ensure _orig?.apply(this, args) is always executed in
the finally/after the catch so the original function referenced by _orig is
called with the original args.
In `@packages/repl/src/examples/list-virtualization/main-thread.js`:
- Line 34: The call to __FlushElementTree() inside the componentAtIndex hot path
forces a render per item and breaks virtualization; remove the
__FlushElementTree() invocation from componentAtIndex and instead invoke a
single __FlushElementTree() once after the loop that creates all visible items
(e.g., at the end of the updateVisibleItems / onScroll/update cycle that
iterates over visible indices), so that flush happens once per frame rather than
per item.
In `@packages/repl/src/examples/styling-inline-styles/main-thread.js`:
- Line 6: The renderPage function schedules a delayed update but never cancels
previous timers, allowing stale timeouts to mutate nodes; fix this by keeping
the timeout id in a shared variable (e.g., renderTimeoutId on module/global
scope), call clearTimeout(renderTimeoutId) at the start of renderPage to cancel
any pending timer, then set renderTimeoutId = setTimeout(...) for the new
delayed update so only the latest timer can run.
In `@packages/repl/src/local-storage.ts`:
- Around line 21-27: The parsed CodeState may contain non-string values for
background and css from corrupted storage; update the return path in the block
after const parsed = JSON.parse(raw) as CodeState to explicitly validate that
parsed.background and parsed.css are strings (e.g., typeof parsed.background ===
'string' ? parsed.background : '') and use an empty-string fallback when they
are not, leaving the existing mainThread string check as-is; target the parsed
variable and the object literal that returns mainThread/background/css.
In `@packages/repl/src/url-state.ts`:
- Around line 51-52: The code currently builds URLSearchParams from
window.location.search (const params = new
URLSearchParams(window.location.search)), which stores shared state in the query
string; change all URL read/write logic to use location.hash instead (e.g.,
const params = new URLSearchParams(window.location.hash.slice(1))) and update
the corresponding save path to write to window.location.hash =
params.toString(); apply this change consistently for the read and write helpers
referenced in this file (the existing URLSearchParams usage and any functions
that serialize/deserialize state between lines ~51 and ~89, e.g., the code that
constructs/reads params and the routine that updates the URL) while preserving
current encoding/decoding behavior.
- Around line 32-38: The parsing currently trusts parsed.background and
parsed.css may be non-string; update the return in the branch that checks typeof
parsed.mainThread === 'string' to explicitly validate background and css are
strings (e.g., use typeof parsed.background === 'string' ? parsed.background :
'' and likewise for parsed.css) so non-string values from the decoded JSON fall
back to '' while keeping parsed.mainThread as returned.
---
Minor comments:
In `@packages/repl/README.md`:
- Around line 19-30: The README contains fenced code blocks without language
labels which triggers MD040; update the two fences shown (the ASCII pipeline
table and the rspeedy/dev snippet) to include a language tag (e.g., change ```
to ```text) so the markdown linter stops flagging them, ensuring both the top
table block and the later pipeline example use ```text fences.
In `@packages/repl/scripts/collect-lynx-types.mjs`:
- Around line 45-86: The relative paths produced by collectDtsFiles are
platform-dependent (backslashes on Windows) which breaks Monaco; normalize paths
to POSIX separators. Update collectDtsFiles to set relativePath =
relative(baseDir, fullPath).replace(/\\+/g, '/') (or use
path.posix.join/relative) so file.relativePath uses '/' and ensure when building
typeMap keys (the loop that creates
node_modules/@lynx-js/types/types/${file.relativePath}) you also use the
normalized forward-slash path.
In `@packages/repl/src/App.tsx`:
- Around line 237-238: The floating promise comes from calling
navigator.clipboard.writeText(window.location.href) (the line currently preceded
by the eslint-disable comment); handle its rejection by awaiting it in an async
handler or adding a .catch to it and log or surface the error (e.g.,
processLogger or UI notification) so the promise rejection is not
unhandled—update the code around that call (where navigator.clipboard.writeText
is invoked) to properly catch and handle errors.
In `@packages/repl/src/bundler/css-processor.ts`:
- Around line 145-150: The code reconstructs URL text with url('${node.value}')
inside the Url branch (when node.type === 'Url') which can mangle
quotes/escapes; instead set item.data.value to the parser-produced/raw text
(e.g. use the node's raw/text representation such as node.valueRaw or
node.toString()/node.raw if available) and keep item.data.type = 'Raw' — update
the assignment in the Url handling block that sets item.data so it uses the
original parser-generated text rather than manually wrapping node.value in
quotes.
In `@packages/repl/src/components/ConsolePanel.tsx`:
- Around line 58-67: The Clear Console button in ConsolePanel (the <Button>
containing <Trash2 /> and using onClick={onClear}) is icon-only and currently
only uses title; add an explicit accessible name by adding an aria-label (e.g.,
aria-label="Clear console") to that Button component so assistive tech receives
a reliable label; ensure the aria-label string matches the visible intent and
keep the existing title prop if desired for tooltips.
In `@packages/repl/src/components/LynxPreview.tsx`:
- Around line 79-117: The effect in LynxPreview that sets up lynxView loading
depends on the onLoad callback causing potential re-render loops if parent
doesn't memoize it; fix by converting onLoad to a ref (e.g., create const
onLoadRef = useRef(onLoad); update onLoadRef.current in a small effect when
onLoad changes) and then use onLoadRef.current() inside the main useEffect
instead of referencing onLoad directly, removing onLoad from the dependency
array so the effect only depends on template (or template and renderCounter if
used); alternatively, ensure callers pass a memoized onLoad via useCallback.
In `@packages/repl/src/components/ui/button.tsx`:
- Around line 33-40: The Button component defined via React.forwardRef (the
Button function using buttonVariants and cn) lacks an explicit type, causing it
to default to "submit" inside forms; update the JSX <button> to provide a
default type="button" while still allowing callers to override via the incoming
props (i.e., derive type from props with a fallback of "button"), ensuring ref,
className, variant, size, and other props remain unaffected.
In `@packages/repl/src/console/useConsole.ts`:
- Around line 33-40: The effect leaves a pending requestAnimationFrame and the
onmessage handler attached; store the RAF id returned by requestAnimationFrame
(when scheduling the callback that calls setEntries and clears scheduled) and
cancel it with cancelAnimationFrame in the cleanup, and also detach the message
handler by keeping a reference to the handler (the function assigned to
channel.onmessage) and removing it via channel.removeEventListener('message',
handler) or setting channel.onmessage = null before calling channel.close();
ensure you still call channel.close() last.
In `@packages/repl/src/examples/global-system-info/main-thread.js`:
- Around line 23-24: Replace the direct global access that throws (const info =
SystemInfo) with a safe, read-only fallback chain that checks
globalThis.SystemInfo, globalThis.lynx?.SystemInfo and falls back to a safe
empty/read-only object so renderPage and any downstream code never throws when
the global isn’t injected; update the same pattern where SystemInfo is read
elsewhere (the other occurrences around the render logic) and keep any ambient
type checks or runtime guards intact so behavior is unchanged when the global is
present.
In `@packages/repl/src/examples/interactivity-refs-bg/main-thread.js`:
- Around line 91-113: The selectionUpdate listener added via
lynx.getJSContext().addEventListener inside renderPage causes duplicate handlers
if renderPage runs multiple times; change to register it idempotently by using a
named handler and removing any existing one before adding (or store the handler
reference on a module/global variable and check it). Specifically, create a
function (e.g., selectionUpdateHandler) that contains the current callback
logic, check for an existing handler reference (e.g.,
window._selectionUpdateHandler or module-scoped variable) and if present remove
it with removeEventListener on lynx.getJSContext(), then assign and add the new
handler with addEventListener; alternatively move the addEventListener call
outside renderPage so it runs only once.
In `@packages/repl/src/examples/interactivity-selector-query/main-thread.js`:
- Around line 39-58: The UI can stay stuck showing "Measuring..." because
there's no error handling when __QuerySelector(page, "#measure-target") returns
null/undefined or when __InvokeUIMethod callback returns result.code !== 0;
update the setTimeout block to detect those failure paths: if __QuerySelector
returns falsy, call __SetAttribute(resultRaw, "text", "Error: element not
found") and __FlushElementTree(); and within the __InvokeUIMethod callback
handle result.code !== 0 by setting a clear error string via
__SetAttribute(resultRaw, "text", "Error measuring element: " + (result.error ||
result.code)) and __FlushElementTree(); ensure both branches short-circuit so
the loading state is cleared.
---
Nitpick comments:
In `@packages/repl/src/components/ConsolePanel.tsx`:
- Line 1: This file contains a blanket file-level ESLint disable (the top
comment in ConsolePanel.tsx) — remove that broad disable and either fix the
offending lint violations or scope disables to the exact lines/rules;
specifically drop the file-wide "/* eslint-disable headers/header-format,
sort-imports, import/order, n/file-extension-in-import,
`@typescript-eslint/no-unnecessary-type-assertion` */", run the linter to identify
concrete failures (imports ordering, header format, unnecessary type assertions,
etc.), fix them inside the ConsolePanel component and its imports (e.g., adjust
import order, add the required header, remove unnecessary type assertions in
functions or JSX), and for any true false-positive or unavoidable cases add
minimal inline disables like // eslint-disable-next-line <rule-name> immediately
above the specific statement instead of a file-level disable.
In `@packages/repl/src/components/Header.tsx`:
- Around line 126-144: The select change handler can convert an empty-string
disabled option into 0 because Number('') === 0; update the onChange logic in
the Header component to defensively parse e.target.value and call onSampleChange
with a null (or other sentinel) when the value is '' instead of Number('');
locate the select using sampleIndex, onSampleChange and groupedOptions and
replace the direct Number(e.target.value) call with a conditional that checks
=== '' and maps that to null, otherwise parses to Number so the "Custom code"
disabled option cannot accidentally select the first sample.
- Around line 44-48: The handleShare callback sets a 1.5s timeout to clear
setCopied but never clears it on unmount; modify handleShare to save the timeout
id (e.g., in a ref like shareTimeoutRef) and clear any existing timeout before
creating a new one, and add a useEffect cleanup that clears
shareTimeoutRef.current on unmount so setCopied(false) won't run after the
component is torn down; reference the handleShare function, onShare prop,
setCopied state updater, and the new shareTimeoutRef/useEffect cleanup to locate
where to change the code.
In `@packages/repl/src/components/LynxPreview.tsx`:
- Line 6: The module-level renderCounter is shared across all LynxPreview
instances; change it to an instance-scoped ref by removing or ignoring the
top-level renderCounter and adding a useRef inside the LynxPreview component
(e.g., renderCounterRef = useRef(0)) and replace uses of renderCounter++ with
renderCounterRef.current++ so each component has its own counter and
HMR/multiple-instance issues are avoided.
In `@packages/repl/src/examples/attributes-dataset/main-thread.js`:
- Line 33: Guard the hard-coded elements[1] access before calling __AddDataset
to avoid runtime errors if the samples list changes: before calling
__AddDataset(elements[1], "extra", "added-via-AddDataset"), check
elements.length > 1 and only call with elements[1] when true, otherwise either
fall back to a safe element (e.g., elements[0]) or skip/log an error; update the
call site where __AddDataset is invoked to use the guarded value or early-return
to protect AddDataset from an undefined argument.
In `@packages/repl/src/examples/interactivity-event-main-thread/main-thread.js`:
- Around line 52-56: The inline style string for colorBox is duplicated; create
a small helper function (e.g., setColorBoxStyle(color)) that constructs the
style string and calls __SetInlineStyles(colorBox, style) so both the initial
render and the tap handler reuse it; update calls in the initial render block
and the tap handler to call setColorBoxStyle(colors[0]) /
setColorBoxStyle(color) respectively and remove the duplicated concatenation
logic.
In `@packages/repl/src/examples/interactivity-get-element-by-id/main-thread.js`:
- Around line 51-60: Remove the setTimeout-based delay and make the lookup run
immediately after flushing the element tree: call __FlushElementTree() first
(instead of inside the timeout), then iterate boxes and call
__QuerySelector(page, "#" + id) for each id, collect results into results[], and
finally call __SetAttribute(resultRaw, "text", results.join("\n")); this keeps
the behavior deterministic and avoids the timing-dependent setTimeout wrapper.
In `@packages/repl/src/examples/list-virtualization/main-thread.js`:
- Around line 52-58: Replace the fixed 100ms delay with immediate async
scheduling to match other examples: in the block that calls setTimeout(() => {
__SetAttribute(list, "update-list-info", { ... }); __FlushElementTree(); }, 100)
change the scheduling to a 0ms (or similarly immediate) async callback so
__SetAttribute and __FlushElementTree on the list run without the unnecessary
100ms latency; keep the same payload structure and callsites (list,
__SetAttribute, __FlushElementTree) unchanged.
In `@packages/repl/src/examples/styling-inline-styles/main-thread.js`:
- Around line 30-31: The inline style string used with __SetInlineStyles for
box2 is duplicated across the initial render and later updates; extract the
common style fragments into a single constant (e.g., const BOX_BASE_STYLE) and
use that constant when calling __SetInlineStyles (referencing __SetInlineStyles
and box2) both in the initial creation and in the delayed update blocks (also
apply the same extraction for the repeated styles around the other occurrence
noted at the block that affects label2/box2), so updates only change the
variable parts while shared style chunks are referenced once.
In `@packages/repl/src/examples/ui-tree-element-types/main-thread.js`:
- Around line 23-30: Several blocks repeat the sequence __CreateText ->
__AppendElement -> __AppendElement(__CreateRawText(...)) -> __SetInlineStyles
(seen for viewLabel and at the other ranges); extract this into a small helper
(e.g., createSectionLabel or createLabeledSection) that accepts the parent
container, the label text, and optional styles, and inside performs
__CreateText, appends it to the parent, appends __CreateRawText(text) into the
created text node, and calls __SetInlineStyles; replace the three repeated
sequences with calls to that helper passing container, the label string, and the
style string.
In `@packages/repl/src/local-storage.ts`:
- Around line 3-7: Remove the duplicated CodeState type definition in
local-storage.ts and import the shared CodeState exported from url-state.ts
instead; update the top of local-storage.ts to import { CodeState } from the
module that exports it, remove the local type declaration, and ensure all
references in functions/variables within local-storage.ts still use CodeState;
run type-check/build to verify the import path is correct and update any
relative import path if necessary.
In `@packages/repl/tsconfig.json`:
- Around line 1-18: Add an "extends" entry to this package tsconfig to inherit
the root configuration and remove duplicated strict flags: replace the top-level
"compilerOptions" block by adding "extends": "<root tsconfig path>" (pointing to
the workspace root tsconfig) so the package inherits verbatimModuleSyntax,
isolatedDeclarations, forceConsistentCasingInFileNames, etc.; keep or override
only package-specific options such as "outDir", "rootDir", and the "paths"
mapping, and ensure "include": ["src"] remains.
| - name: Build REPL | ||
| run: pnpm --filter @lynx-js/repl build | ||
| env: | ||
| ASSET_PREFIX: ${{ steps.pages.outputs.base_path }}/repl/ | ||
| - name: Copy REPL into website output |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cd /repo && find . -name "workflow-website.yml" -type fRepository: lynx-family/lynx-stack
Length of output: 121
🏁 Script executed:
cd /repo && cat -n .github/workflows/workflow-website.yml | sed -n '30,50p'Repository: lynx-family/lynx-stack
Length of output: 121
🌐 Web query:
GitHub Pages base_path root / double slash URL behavior
💡 Result:
On GitHub Pages (Jekyll), the “double slash” (//) usually comes from setting baseurl: "/" and then concatenating it with page/post URLs that already start with / (e.g., {{ site.baseurl }}{{ page.url }} → //about/). Jekyll explicitly warns this “will result in two leading slashes, which will break links,” and recommends only prefixing with site.baseurl when it’s something other than the default. (jekyllrb.com)
What to do instead (safe patterns):
- User/Org Pages (
https://user.github.io/): setbaseurlto empty, not/(i.e.,baseurl:). (jekyllrb.com) - Project Pages (
https://user.github.io/repo/): setbaseurl: "/repo"(no trailing slash) and generate links via Jekyll’s URL filters (preferred):{{ "/assets/main.css" | relative_url }}{{ page.url | relative_url }}
These filters were introduced specifically to keep URLs correct across GitHub Pages deployments. (github.blog)
- If you manually build the homepage/root link, use
{{ site.baseurl }}/(and keepsite.baseurlempty or without a trailing slash) to avoid odd “current-page” resolution issues. (stackoverflow.com)
Citations:
- 1: https://jekyllrb.com/docs/upgrading/0-to-2/?utm_source=openai
- 2: https://jekyllrb.com/docs/upgrading/0-to-2/?utm_source=openai
- 3: https://github.blog/news-insights/what-s-new-in-github-pages-with-jekyll-3-3/?utm_source=openai
- 4: https://stackoverflow.com/questions/67567907/jekyll-site-baseurl-doesnt-link-to-homepage-when-published-via-github-page-but?utm_source=openai
🏁 Script executed:
git ls-files .github/workflows/workflow-website.ymlRepository: lynx-family/lynx-stack
Length of output: 104
🏁 Script executed:
pwd && ls -laRepository: lynx-family/lynx-stack
Length of output: 3101
🏁 Script executed:
find . -name "workflow-website.yml" -type f 2>/dev/null | head -5Repository: lynx-family/lynx-stack
Length of output: 106
🏁 Script executed:
cat -n .github/workflows/workflow-website.yml | sed -n '30,55p'Repository: lynx-family/lynx-stack
Length of output: 936
🏁 Script executed:
cat -n .github/workflows/workflow-website.yml | head -80Repository: lynx-family/lynx-stack
Length of output: 1935
🌐 Web query:
GitHub Actions configure-pages action base_path output what values
💡 Result:
actions/configure-pages sets the base_path output to the site’s “base path” portion of the Pages URL (the part after the origin), and it’s typically one of these forms:
"/<repo>"for project pages (e.g."/my-repo").""(empty string) for root sites like user/org pages or many custom domains.
These example values are documented in the action’s own action.yml. [1]
🌐 Web query:
ASSET_PREFIX environment variable build double slash path behavior
💡 Result:
In Next.js, assetPrefix (often set from an ASSET_PREFIX env var in next.config.*) is string-concatenated in front of asset URLs that already start with /_next/... (leading slash) [1]. If your ASSET_PREFIX ends with /, you can end up with a double slash in the built output:
ASSET_PREFIX=https://cdn.example.com/+/_next/static/...→https://cdn.example.com//_next/static/...
Fix: normalize the env var to remove the trailing slash (and keep the required format: leading / for path prefixes or an absolute http(s) URL) [1].
// next.config.mjs
const raw = process.env.ASSET_PREFIX;
const assetPrefix = raw ? raw.replace(/\/+$/, "") : undefined;
export default {
assetPrefix,
};Notes:
- Next’s docs show the expected result without a double slash (e.g.
https://cdn.mydomain.com/_next/static/...) [1]. - Some Next.js versions have been reported to not “normalize”
assetPrefixautomatically, so doing this trimming yourself is the safest approach [2].
Sources: [1] [2]
🏁 Script executed:
grep -r "ASSET_PREFIX" packages/repl/ --include="*.ts" --include="*.js" --include="*.json" -A 2 -B 2Repository: lynx-family/lynx-stack
Length of output: 335
🌐 Web query:
rsbuild assetPrefix double slash path behavior
💡 Result:
In Rsbuild, output.assetPrefix is normalized to end with a trailing / (“automatically appends a trailing / by default”).[1] If you then concatenate paths that also start with / (common in HTML templates like <%= assetPrefix %>/favicon.ico), you can end up with URLs containing // in the middle (e.g. /app//favicon.ico).[1][2]
How to avoid the double-slash:
- Don’t add your own extra
/in templates when usingassetPrefix. Prefer:<%= assetPrefix %>favicon.ico(instead of<%= assetPrefix %>/favicon.ico) when yourassetPrefixalready ends with/.[1][2]
- In client JS, prefer
process.env.ASSET_PREFIX, because Rsbuild removes the trailing slash from it to make concatenation easier (so${process.env.ASSET_PREFIX}/foowon’t create//).[3] - If you’re programmatically prefixing URLs, use
ensureAssetPrefix(url, assetPrefix)from@rsbuild/core(and passurlas a relative path likefoo/bar.js, not/foo/bar.js).[4]
Sources: Rsbuild output.assetPrefix docs[1], HTML template parameters (includes assetPrefix)[2], env var process.env.ASSET_PREFIX trailing-slash behavior[3], ensureAssetPrefix API[4].
🏁 Script executed:
cat packages/repl/rsbuild.config.ts | head -50Repository: lynx-family/lynx-stack
Length of output: 1609
🏁 Script executed:
grep -r "ASSET_PREFIX" packages/repl/ --include="*.tsx" --include="*.ts" --include="*.html" -B 2 -A 2Repository: lynx-family/lynx-stack
Length of output: 335
🏁 Script executed:
cat packages/repl/index.htmlRepository: lynx-family/lynx-stack
Length of output: 733
Prevent double-slash in ASSET_PREFIX when GitHub Pages base path is /.
If base_path is /, the current concatenation yields //repl/, which creates incorrect asset paths in the built output. Use bash parameter expansion to strip the trailing slash before concatenation.
Proposed fix
- name: Build REPL
- run: pnpm --filter `@lynx-js/repl` build
- env:
- ASSET_PREFIX: ${{ steps.pages.outputs.base_path }}/repl/
+ run: |
+ BASE_PATH='${{ steps.pages.outputs.base_path }}'
+ export ASSET_PREFIX="${BASE_PATH%/}/repl/"
+ pnpm --filter `@lynx-js/repl` build📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| - name: Build REPL | |
| run: pnpm --filter @lynx-js/repl build | |
| env: | |
| ASSET_PREFIX: ${{ steps.pages.outputs.base_path }}/repl/ | |
| - name: Copy REPL into website output | |
| - name: Build REPL | |
| run: | | |
| BASE_PATH='${{ steps.pages.outputs.base_path }}' | |
| export ASSET_PREFIX="${BASE_PATH%/}/repl/" | |
| pnpm --filter `@lynx-js/repl` build | |
| env: | |
| - name: Copy REPL into website output |
| - name: Build REPL | |
| run: pnpm --filter @lynx-js/repl build | |
| env: | |
| ASSET_PREFIX: ${{ steps.pages.outputs.base_path }}/repl/ | |
| - name: Copy REPL into website output | |
| - name: Build REPL | |
| run: | | |
| BASE_PATH='${{ steps.pages.outputs.base_path }}' | |
| export ASSET_PREFIX="${BASE_PATH%/}/repl/" | |
| pnpm --filter `@lynx-js/repl` build | |
| - name: Copy REPL into website output |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/workflow-website.yml around lines 39 - 43, The
ASSET_PREFIX environment value for the "Build REPL" step can become "//repl/"
when steps.pages.outputs.base_path is "/", so update the ASSET_PREFIX assignment
to strip a trailing slash from steps.pages.outputs.base_path using bash
parameter expansion (e.g., remove trailing slash from the base_path output)
before concatenating "/repl/"; modify the "Build REPL" step's env ASSET_PREFIX
to use that expansion so asset paths never contain a double-slash.
| // REPL examples use Lynx platform globals and are not subject to lint rules | ||
| "packages/repl/src/examples/**", |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Inspect biome.jsonc around the flagged lines
head -70 biome.jsonc | tail -20Repository: lynx-family/lynx-stack
Length of output: 695
🏁 Script executed:
#!/bin/bash
# Get full context of the ignore section in biome.jsonc
cat -n biome.jsonc | grep -A 10 -B 10 "examples"Repository: lynx-family/lynx-stack
Length of output: 900
🏁 Script executed:
#!/bin/bash
# Check if REPL examples directory exists and what's in it
find packages/repl/src/examples -type f 2>/dev/null | head -20Repository: lynx-family/lynx-stack
Length of output: 1326
🏁 Script executed:
#!/bin/bash
# Check if REPL examples use inline Biome suppressions elsewhere
rg "biome-ignore|eslint-disable" packages/repl/src/examples --type jsRepository: lynx-family/lynx-stack
Length of output: 354
🏁 Script executed:
#!/bin/bash
# Search for documentation on Lynx platform globals
rg -i "platform globals|lynx globals" packages/repl --type-list | head -20Repository: lynx-family/lynx-stack
Length of output: 455
🏁 Script executed:
#!/bin/bash
# Check git history/blame for why this exclusion was added
git log --oneline -n 1 -- biome.jsonc 2>/dev/null || echo "Git not available"Repository: lynx-family/lynx-stack
Length of output: 127
🏁 Script executed:
#!/bin/bash
# See if there's a README explaining REPL examples
cat packages/repl/README.md 2>/dev/null || cat packages/repl/src/examples/README.md 2>/dev/null || echo "No README found"Repository: lynx-family/lynx-stack
Length of output: 4389
🏁 Script executed:
#!/bin/bash
# Check one of the REPL example files to see what globals it uses
head -30 packages/repl/src/examples/global-console/main-thread.jsRepository: lynx-family/lynx-stack
Length of output: 1011
Use scoped suppressions for REPL platform globals instead of excluding the entire directory.
Line 62 excludes packages/repl/src/examples/** from Biome checks. However, REPL example files are regular JavaScript that benefits from linting (catching unused variables, unused imports, etc.). The inline biome-ignore comments already used in some example files demonstrate that scoped suppressions work: move the path back into linting scope and add targeted rule suppressions for platform APIs like __CreateElement, __SetAttribute, etc. that may trigger false positives.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@biome.jsonc` around lines 61 - 62, Remove the broad exclusion
"packages/repl/src/examples/**" from biome.jsonc so example files are linted,
and instead add scoped suppressions for the REPL platform globals by creating a
.biome.jsonc in packages/repl/src/examples (or adding a per-directory
"overrides" entry) that declares the platform identifiers as globals (e.g.,
"__CreateElement", "__SetAttribute", "__SetStyle", etc.) or uses targeted rule
disables for those names; this preserves linting (unused/unused-import checks)
while preventing false positives from the REPL platform APIs.
| if (urlState?.type === 'sample') { | ||
| const sample = samples[urlState.sampleIndex]; | ||
| return { | ||
| code: { | ||
| mainThread: sample.mainThread, | ||
| background: sample.background, | ||
| css: sample.css, | ||
| }, | ||
| sampleIndex: urlState.sampleIndex, | ||
| }; |
There was a problem hiding this comment.
Validate URL-derived sample indices before dereferencing.
A malformed hash can produce an out-of-range index and crash startup when sample.mainThread is accessed.
Suggested fix
if (urlState?.type === 'sample') {
const sample = samples[urlState.sampleIndex];
- return {
- code: {
- mainThread: sample.mainThread,
- background: sample.background,
- css: sample.css,
- },
- sampleIndex: urlState.sampleIndex,
- };
+ if (sample) {
+ return {
+ code: {
+ mainThread: sample.mainThread,
+ background: sample.background,
+ css: sample.css,
+ },
+ sampleIndex: urlState.sampleIndex,
+ };
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (urlState?.type === 'sample') { | |
| const sample = samples[urlState.sampleIndex]; | |
| return { | |
| code: { | |
| mainThread: sample.mainThread, | |
| background: sample.background, | |
| css: sample.css, | |
| }, | |
| sampleIndex: urlState.sampleIndex, | |
| }; | |
| if (urlState?.type === 'sample') { | |
| const sample = samples[urlState.sampleIndex]; | |
| if (sample) { | |
| return { | |
| code: { | |
| mainThread: sample.mainThread, | |
| background: sample.background, | |
| css: sample.css, | |
| }, | |
| sampleIndex: urlState.sampleIndex, | |
| }; | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/repl/src/App.tsx` around lines 54 - 63, When handling the
url-derived sample branch in App.tsx (the urlState?.type === 'sample' block),
validate urlState.sampleIndex before dereferencing sample.mainThread: ensure
sampleIndex is a finite integer and 0 <= sampleIndex < samples.length; if
invalid, fall back to a safe default (e.g., sampleIndex = 0 or return the
existing default code state) rather than indexing into samples. Update the logic
around urlState, sampleIndex and samples so you only assign sample =
samples[sampleIndex] after the bounds check to prevent crashes from malformed
hashes.
| const firstOperator = varFunctionValues[1]?.type === 'Operator' | ||
| ? varFunctionValues[1].value | ||
| : undefined; | ||
| const varDefaultValueNode = varFunctionValues[2]; | ||
|
|
||
| if (!varName || (firstOperator && firstOperator !== ',')) { | ||
| throw new Error(`illegal css value ${csstree.generate(n)}`); | ||
| } | ||
| if (varDefaultValueNode) { | ||
| const currentDefaultValueText = csstree.generate(varDefaultValueNode); | ||
| defaultValueMap[varName] = currentDefaultValueText; | ||
| item.data = { | ||
| ...n, | ||
| type: 'Raw', | ||
| value: currentDefaultValueText, | ||
| }; |
There was a problem hiding this comment.
Preserve the full var() fallback expression.
Only one fallback token is captured today, so multi-token defaults are truncated (e.g., var(--x, 1px solid red) becomes 1px).
Suggested fix
- const varDefaultValueNode = varFunctionValues[2];
+ const varDefaultValueTokens = varFunctionValues.slice(2);
if (!varName || (firstOperator && firstOperator !== ',')) {
throw new Error(`illegal css value ${csstree.generate(n)}`);
}
- if (varDefaultValueNode) {
- const currentDefaultValueText = csstree.generate(varDefaultValueNode);
+ if (varDefaultValueTokens.length > 0) {
+ const currentDefaultValueText = varDefaultValueTokens
+ .map((token) => csstree.generate(token))
+ .join('')
+ .trim();
defaultValueMap[varName] = currentDefaultValueText;
item.data = {
...n,
type: 'Raw',
value: currentDefaultValueText,
};🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/repl/src/bundler/css-processor.ts` around lines 78 - 93, The code
only captures a single token as the var() fallback (using varFunctionValues[2]),
truncating multi-token fallbacks; update the logic around varFunctionValues,
varDefaultValueNode, defaultValueMap and item.data so you collect all tokens
from index 2 onward (e.g., slice varFunctionValues from 2 to end), reconstruct
or generate the full fallback text via csstree.generate on the combined nodes
(or wrap them into a single AST node) and store that full string in
defaultValueMap[varName] and item.data.value instead of the current single-token
value.
| <div | ||
| className='flex flex-col items-center flex-1 min-h-0 pt-1.5 pb-3 select-none cursor-pointer' | ||
| style={{ background: 'var(--repl-bg-surface)' }} | ||
| onClick={onToggle} | ||
| title='Expand' | ||
| > |
There was a problem hiding this comment.
Make panel header toggles keyboard-accessible.
These clickable divs are mouse-only today. Add interactive semantics and keyboard handlers (Enter/Space) so collapse/expand is operable without a pointer.
♿ Proposed fix
<div
className='flex flex-col items-center flex-1 min-h-0 pt-1.5 pb-3 select-none cursor-pointer'
style={{ background: 'var(--repl-bg-surface)' }}
onClick={onToggle}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ onToggle();
+ }
+ }}
+ role='button'
+ tabIndex={0}
+ aria-label='Expand'
title='Expand'
>
@@
<div
className='flex items-center justify-between h-7 px-3 shrink-0 select-none cursor-pointer'
style={{
background: 'var(--repl-bg-surface)',
borderBottom: '1px solid var(--repl-border)',
}}
onClick={onToggle}
+ onKeyDown={(e) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ onToggle();
+ }
+ }}
+ role='button'
+ tabIndex={0}
+ aria-label={collapsed ? 'Expand' : 'Collapse'}
title={collapsed ? 'Expand' : 'Collapse'}
>Also applies to: 51-59
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/repl/src/components/EditorWindow.tsx` around lines 22 - 27, The
header toggle divs in EditorWindow.tsx are only mouse-clickable; make them
keyboard-accessible by giving the toggle elements interactive semantics (add
role="button" and tabIndex={0}) and a key handler that calls the existing
onToggle when the user presses Enter or Space (handle keyDown for 'Enter' and '
' / 'Spacebar'), retaining the current onClick behavior; apply the same change
to the second header toggle block referenced around lines 51-59 so both toggle
elements (the one using onToggle and the other similar header) support keyboard
activation.
| __AppendElement(text, __CreateRawText(items[index].label)); | ||
| __SetInlineStyles(row, "padding:14px 20px; background-color:" + items[index].bg + ";"); | ||
| __SetInlineStyles(text, "font-size:15px; color:#1f2937;"); | ||
| __FlushElementTree(); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# First, check the exact file and lines mentioned
echo "=== File content around line 34 ==="
cat -n packages/repl/src/examples/list-virtualization/main-thread.js | head -50
echo -e "\n=== Search for __CreateList and componentAtIndex pattern ==="
rg -n -C5 '__CreateList|componentAtIndex' packages/repl/src/examples/list-virtualization/ -A5Repository: lynx-family/lynx-stack
Length of output: 5096
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check other REPL examples to compare patterns
echo "=== Checking other examples for similar patterns ==="
fd -e js packages/repl/src/examples | head -20Repository: lynx-family/lynx-stack
Length of output: 545
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find other examples in the REPL
echo "=== Other examples in list-virtualization directory ==="
ls -la packages/repl/src/examples/list-virtualization/
echo -e "\n=== Other examples in packages/repl/src/examples ==="
find packages/repl/src/examples -type f -name "*.js" | head -20Repository: lynx-family/lynx-stack
Length of output: 1614
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check for other list examples with componentAtIndex pattern
echo "=== Search for componentAtIndex in other examples ==="
rg -l 'componentAtIndex' packages/repl/src/examples --type=js
echo -e "\n=== Search for __FlushElementTree usage patterns ==="
rg -n '__FlushElementTree' packages/repl/src/examples --type=js -B2 -A2Repository: lynx-family/lynx-stack
Length of output: 22705
Remove __FlushElementTree() from componentAtIndex hot path.
Flushing inside componentAtIndex defeats virtualization by forcing a render cycle for each visible item. This teaching example would instruct developers to replicate this anti-pattern. Move the flush outside: once after all visible items are created per scroll/update cycle.
⚡ Proposed fix
function componentAtIndex(listEl, listID, index) {
const row = __CreateView(0);
const text = __CreateText(0);
__AppendElement(row, text);
__AppendElement(text, __CreateRawText(items[index].label));
__SetInlineStyles(row, "padding:14px 20px; background-color:" + items[index].bg + ";");
__SetInlineStyles(text, "font-size:15px; color:`#1f2937`;");
- __FlushElementTree();
// Must return the element's unique ID
return __GetElementUniqueID(row);
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/repl/src/examples/list-virtualization/main-thread.js` at line 34,
The call to __FlushElementTree() inside the componentAtIndex hot path forces a
render per item and breaks virtualization; remove the __FlushElementTree()
invocation from componentAtIndex and instead invoke a single
__FlushElementTree() once after the loop that creates all visible items (e.g.,
at the end of the updateVisibleItems / onScroll/update cycle that iterates over
visible indices), so that flush happens once per frame rather than per item.
| // __AddInlineStyle sets one property at a time. | ||
| // __SetInlineStyles sets multiple properties in one call (more efficient). | ||
|
|
||
| globalThis.renderPage = function renderPage() { |
There was a problem hiding this comment.
Cancel stale delayed updates before scheduling a new one.
Line 39 creates a new timer on every renderPage call. If the example is re-run quickly, older timers can still fire and mutate stale nodes. Clear the previous timer before scheduling the next one.
💡 Proposed fix
+let styleUpdateTimerId;
+
globalThis.renderPage = function renderPage() {
+ if (styleUpdateTimerId != null) {
+ clearTimeout(styleUpdateTimerId);
+ styleUpdateTimerId = undefined;
+ }
+
const page = __CreatePage("page", 0);
const container = __CreateView(0);
__AppendElement(page, container);
@@
- setTimeout(() => {
+ styleUpdateTimerId = setTimeout(() => {
__SetInlineStyles(box1, "width:200px; height:60px; background-color:`#ef4444`; border-radius:8px; align-items:center; justify-content:center;");
__SetInlineStyles(box2, "width:200px; height:60px; background-color:`#10b981`; border-radius:8px; margin-top:12px; align-items:center; justify-content:center;");
__FlushElementTree();
+ styleUpdateTimerId = undefined;
}, 2000);
};Also applies to: 39-43
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/repl/src/examples/styling-inline-styles/main-thread.js` at line 6,
The renderPage function schedules a delayed update but never cancels previous
timers, allowing stale timeouts to mutate nodes; fix this by keeping the timeout
id in a shared variable (e.g., renderTimeoutId on module/global scope), call
clearTimeout(renderTimeoutId) at the start of renderPage to cancel any pending
timer, then set renderTimeoutId = setTimeout(...) for the new delayed update so
only the latest timer can run.
| const parsed = JSON.parse(raw) as CodeState; | ||
| if (typeof parsed.mainThread === 'string') { | ||
| return { | ||
| mainThread: parsed.mainThread, | ||
| background: parsed.background ?? '', | ||
| css: parsed.css ?? '', | ||
| }; |
There was a problem hiding this comment.
Validate background and css as strings before returning parsed state.
background and css can currently pass through non-string values from corrupted/locally edited storage, which can break consumers that expect source text strings.
Proposed fix
export function loadFromLocalStorage(): CodeState | null {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return null;
- const parsed = JSON.parse(raw) as CodeState;
+ const parsed = JSON.parse(raw) as Partial<CodeState>;
if (typeof parsed.mainThread === 'string') {
return {
mainThread: parsed.mainThread,
- background: parsed.background ?? '',
- css: parsed.css ?? '',
+ background: typeof parsed.background === 'string' ? parsed.background : '',
+ css: typeof parsed.css === 'string' ? parsed.css : '',
};
}
} catch {📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const parsed = JSON.parse(raw) as CodeState; | |
| if (typeof parsed.mainThread === 'string') { | |
| return { | |
| mainThread: parsed.mainThread, | |
| background: parsed.background ?? '', | |
| css: parsed.css ?? '', | |
| }; | |
| export function loadFromLocalStorage(): CodeState | null { | |
| try { | |
| const raw = localStorage.getItem(STORAGE_KEY); | |
| if (!raw) return null; | |
| const parsed = JSON.parse(raw) as Partial<CodeState>; | |
| if (typeof parsed.mainThread === 'string') { | |
| return { | |
| mainThread: parsed.mainThread, | |
| background: typeof parsed.background === 'string' ? parsed.background : '', | |
| css: typeof parsed.css === 'string' ? parsed.css : '', | |
| }; | |
| } | |
| } catch { | |
| return null; | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/repl/src/local-storage.ts` around lines 21 - 27, The parsed
CodeState may contain non-string values for background and css from corrupted
storage; update the return path in the block after const parsed =
JSON.parse(raw) as CodeState to explicitly validate that parsed.background and
parsed.css are strings (e.g., typeof parsed.background === 'string' ?
parsed.background : '') and use an empty-string fallback when they are not,
leaving the existing mainThread string check as-is; target the parsed variable
and the object literal that returns mainThread/background/css.
| const parsed = JSON.parse(json) as CodeState; | ||
| if (typeof parsed.mainThread === 'string') { | ||
| return { | ||
| mainThread: parsed.mainThread, | ||
| background: parsed.background ?? '', | ||
| css: parsed.css ?? '', | ||
| }; |
There was a problem hiding this comment.
Validate decoded optional fields as strings.
Like local-storage parsing, this can pass through non-string background/css values when present.
Proposed fix
- const parsed = JSON.parse(json) as CodeState;
+ const parsed = JSON.parse(json) as Partial<CodeState>;
if (typeof parsed.mainThread === 'string') {
return {
mainThread: parsed.mainThread,
- background: parsed.background ?? '',
- css: parsed.css ?? '',
+ background: typeof parsed.background === 'string' ? parsed.background : '',
+ css: typeof parsed.css === 'string' ? parsed.css : '',
};
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const parsed = JSON.parse(json) as CodeState; | |
| if (typeof parsed.mainThread === 'string') { | |
| return { | |
| mainThread: parsed.mainThread, | |
| background: parsed.background ?? '', | |
| css: parsed.css ?? '', | |
| }; | |
| const parsed = JSON.parse(json) as Partial<CodeState>; | |
| if (typeof parsed.mainThread === 'string') { | |
| return { | |
| mainThread: parsed.mainThread, | |
| background: typeof parsed.background === 'string' ? parsed.background : '', | |
| css: typeof parsed.css === 'string' ? parsed.css : '', | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/repl/src/url-state.ts` around lines 32 - 38, The parsing currently
trusts parsed.background and parsed.css may be non-string; update the return in
the branch that checks typeof parsed.mainThread === 'string' to explicitly
validate background and css are strings (e.g., use typeof parsed.background ===
'string' ? parsed.background : '' and likewise for parsed.css) so non-string
values from the decoded JSON fall back to '' while keeping parsed.mainThread as
returned.
| const params = new URLSearchParams(window.location.search); | ||
|
|
There was a problem hiding this comment.
Persist shared state in location.hash instead of query params.
This currently stores code in search params, which deviates from the hash-based sharing objective and increases leakage risk (server logs/referrer propagation).
Proposed fix
export function getInitialState(): InitialState {
- const params = new URLSearchParams(window.location.search);
+ const hashParams = new URLSearchParams(
+ window.location.hash.startsWith('#')
+ ? window.location.hash.slice(1)
+ : window.location.hash,
+ );
- const encodedCode = params.get('c');
+ const encodedCode = hashParams.get('c');
@@
- const sampleParam = params.get('s');
+ const sampleParam = hashParams.get('s');
@@
export function saveToUrl(code: CodeState): void {
const encoded = encodeCode(code);
const url = new URL(window.location.href);
- url.searchParams.delete('s');
- url.searchParams.set('c', encoded);
+ const hashParams = new URLSearchParams(
+ url.hash.startsWith('#') ? url.hash.slice(1) : '',
+ );
+ hashParams.delete('s');
+ hashParams.set('c', encoded);
+ url.hash = hashParams.toString();
+ url.searchParams.delete('c');
+ url.searchParams.delete('s');
window.history.replaceState({}, '', url);
}
@@
export function saveSampleToUrl(sampleIndex: number): void {
const url = new URL(window.location.href);
- url.searchParams.delete('c');
- url.searchParams.set('s', String(sampleIndex));
+ const hashParams = new URLSearchParams(
+ url.hash.startsWith('#') ? url.hash.slice(1) : '',
+ );
+ hashParams.delete('c');
+ hashParams.set('s', String(sampleIndex));
+ url.hash = hashParams.toString();
+ url.searchParams.delete('c');
+ url.searchParams.delete('s');
window.history.replaceState({}, '', url);
}Also applies to: 73-89
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/repl/src/url-state.ts` around lines 51 - 52, The code currently
builds URLSearchParams from window.location.search (const params = new
URLSearchParams(window.location.search)), which stores shared state in the query
string; change all URL read/write logic to use location.hash instead (e.g.,
const params = new URLSearchParams(window.location.hash.slice(1))) and update
the corresponding save path to write to window.location.hash =
params.toString(); apply this change consistently for the read and write helpers
referenced in this file (the existing URLSearchParams usage and any functions
that serialize/deserialize state between lines ~51 and ~89, e.g., the code that
constructs/reads params and the routine that updates the URL) while preserving
current encoding/decoding behavior.
Add a browser-based REPL for learning and experimenting with Lynx Element PAPIs. Integrate @lynx-js/types into the Monaco editor via a build-time type collection script, providing autocomplete and hover docs with platform annotations (@android, @iOS, @SInCE, etc.) alongside the existing @lynx-js/type-element-api types.
- Introduce applyCollapsedState() as the single shared path for both manual toggle and collapseByContent, ensuring identical timing and redistribution logic in all cases - Add distributeEqual() that measures the panel group's real pixel size via elementRef and resizes all expanded panels to equal shares after any collapse/expand operation, resetting any prior drag-adjusted sizes - Replace fragile pixel-threshold collapsed detection (size.inPixels <= 30) with panel.isCollapsed() to avoid false positives mid-drag - Store panel refs in a stable useRef container to prevent stale-closure issues in memoized callbacks - Add id prop to each ResizablePanel for stable library-internal tracking - Reset all panels to equal expanded state when layout orientation changes - Split EditorWindow into EditorWindowHeader + EditorWindowBody; header now receives onToggle callback instead of panelRef so toggle logic stays in EditorPane where redistribution context is available - Cols-mode collapsed header renders as full-height vertical strip with rotated filename; removed redundant borderRight
setCode() triggers Monaco's onChange which queues debouncedRebuild; without cancellation that timer fires 500ms later and calls setSampleIndex(null), immediately marking the fresh sample as custom code. Clearing the timer after rebuild() prevents this.
When switching examples, instead of splitting expanded panels equally, compute each panel's natural height from its line count and shrink panels that need less space than their equal share, redistributing the surplus to panels with more content. Also removes the 100ms setTimeout from the initial mount effect in App.tsx (React runs child effects before parent, so Monaco is already ready) and adds an opacity fade-in via layoutReady state to avoid the flash of un-sized panels on first load.
Examples: - Add interactivity examples split by thread: Event Handling (bg/mt) and Element Refs (bg/mt), replacing old SelectorQuery/getElementById - Add "Theme via globalProps" example, replacing Console API example Timing: - Measure render time via onLoad callback instead of post-setState - Display sub-millisecond values in µs; shorten labels to css/asm/render/total - Clear timing display on each rebuild start Header (mobile): - Replace Editor/Preview tab labels with icons (Code2/Eye) + hidden text on small screens for a compact header - Responsive padding on Share button (px-2 sm:px-3) CSS / styling: - Move custom rules into @layer base for correct Tailwind v4 cascade - Add resize-handle hover/active styles with 5px hit-area pseudo-element and accent-color highlight on drag - Remove !important from .repl-header background rule
Example files use Lynx platform globals (__CreateView, lynx, etc.) and intentionally bypass standard lint rules. Ignore the entire directory in both eslint.config.js and biome.jsonc instead of adding per-file eslint-disable / biome-ignore comments.
- Add a reload button (RotateCw icon) in the preview pane header that clears the console and triggers a rebuild - Clear console when switching examples - Move clearConsole out of the template-switch handler and into onReload - Rename 'renderPage & updatePage' sample to 'Page Lifecycle' - Hide the lifecycle-events sample
Build the repl with rsbuild in the website workflow and copy the output into website/doc_build/repl so it is served at <pages-url>/repl/ on every push to main. Use the configure-pages base_path output to set the asset prefix correctly. Also exclude packages/repl/rsbuild.config.ts from ESLint since it is outside the repl's TypeScript project scope, fix the node: import protocol, and remove unused webPlatformRoot variable.
…rds and simplify dispose handler Inject callDestroyLifetimeFun into lynxCoreInject.tt in the REPL background thread preamble, mirroring how ReactLynx registers its lifecycle teardown. For raw Element PAPI cards the cleanup neutralizes stale event handlers (publishEvent / publicComponentEvent) on dispose. With all card types now properly registering callDestroyLifetimeFun, remove the defensive nativeGlobal guard from registerDisposeHandler in both web-worker-runtime and web-core-wasm — the unconditional call is safe again. Also remove unused src/defaults/ files and src/defaults.ts from the REPL, and add @Huxpro as CODEOWNER for packages/repl.
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Actionable comments posted: 9
♻️ Duplicate comments (6)
packages/repl/src/url-state.ts (2)
33-38:⚠️ Potential issue | 🟠 MajorValidate decoded optional fields as strings.
Line 36 and Line 37 currently accept any non-null values, so malformed payloads can pass non-string
background/cssintoCodeState.💡 Proposed fix
- const parsed = JSON.parse(json) as CodeState; + const parsed = JSON.parse(json) as Partial<CodeState>; if (typeof parsed.mainThread === 'string') { return { mainThread: parsed.mainThread, - background: parsed.background ?? '', - css: parsed.css ?? '', + background: typeof parsed.background === 'string' ? parsed.background : '', + css: typeof parsed.css === 'string' ? parsed.css : '', }; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/src/url-state.ts` around lines 33 - 38, The returned CodeState currently trusts parsed.background and parsed.css without type-checking; update the return to validate each optional field is a string (e.g., background: typeof parsed.background === 'string' ? parsed.background : '' and css: typeof parsed.css === 'string' ? parsed.css : '') so malformed/non-string values are replaced with ''. Keep the existing check for parsed.mainThread being a string and use these guards when constructing the CodeState object.
51-52:⚠️ Potential issue | 🟠 MajorPersist shared REPL state in
location.hash, not query params.Line 51 and Line 73-Line 89 currently read/write
searchparams, but this REPL’s sharing contract is hash-based. Keeping code state in query params also increases server/referrer leakage.💡 Proposed fix
export function getInitialState(): InitialState { - const params = new URLSearchParams(window.location.search); + const params = new URLSearchParams( + window.location.hash.startsWith('#') + ? window.location.hash.slice(1) + : window.location.hash, + ); @@ export function saveToUrl(code: CodeState): void { const encoded = encodeCode(code); const url = new URL(window.location.href); - url.searchParams.delete('s'); - url.searchParams.set('c', encoded); + const params = new URLSearchParams( + url.hash.startsWith('#') ? url.hash.slice(1) : '', + ); + params.delete('s'); + params.set('c', encoded); + url.hash = params.toString(); + url.searchParams.delete('c'); + url.searchParams.delete('s'); window.history.replaceState({}, '', url); } @@ export function saveSampleToUrl(sampleIndex: number): void { const url = new URL(window.location.href); - url.searchParams.delete('c'); - url.searchParams.set('s', String(sampleIndex)); + const params = new URLSearchParams( + url.hash.startsWith('#') ? url.hash.slice(1) : '', + ); + params.delete('c'); + params.set('s', String(sampleIndex)); + url.hash = params.toString(); + url.searchParams.delete('c'); + url.searchParams.delete('s'); window.history.replaceState({}, '', url); }Also applies to: 73-89
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/src/url-state.ts` around lines 51 - 52, The code currently constructs URLSearchParams from window.location.search (const params) and reads/writes shared REPL state using query parameters; change this to use the hash fragment instead: initialize params with new URLSearchParams(window.location.hash.slice(1)) (strip the leading '#'), and when updating state (the code that writes back params in the block around lines 73–89) update window.location.hash = '#' + params.toString() or use history.replaceState to set the hash (not search). Replace any references to window.location.search with window.location.hash and ensure encoding/decoding uses the same URLSearchParams API so the REPL sharing contract stays hash-based and avoids query param leakage.packages/repl/src/examples/styling-inline-styles/main-thread.js (1)
39-43:⚠️ Potential issue | 🟠 MajorCancel stale delayed updates before scheduling a new one.
Line 39 schedules a fresh timeout on every
renderPagecall; quick re-runs can let older callbacks mutate stale nodes.💡 Proposed fix
+let styleUpdateTimerId; + globalThis.renderPage = function renderPage() { + if (styleUpdateTimerId != null) { + clearTimeout(styleUpdateTimerId); + styleUpdateTimerId = undefined; + } + const page = __CreatePage("page", 0); const container = __CreateView(0); @@ - setTimeout(() => { + styleUpdateTimerId = setTimeout(() => { __SetInlineStyles(box1, "width:200px; height:60px; background-color:`#ef4444`; border-radius:8px; align-items:center; justify-content:center;"); __SetInlineStyles(box2, "width:200px; height:60px; background-color:`#10b981`; border-radius:8px; margin-top:12px; align-items:center; justify-content:center;"); __FlushElementTree(); + styleUpdateTimerId = undefined; }, 2000); };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/src/examples/styling-inline-styles/main-thread.js` around lines 39 - 43, renderPage schedules a setTimeout that can leave stale callbacks mutating nodes; to fix, track the timeout id in a module-level variable (e.g., inlineStylesTimer) and before scheduling the new setTimeout clearTimeout(inlineStylesTimer) if present, then assign the new timer id to inlineStylesTimer; keep the existing callbacks (__SetInlineStyles, __FlushElementTree) but ensure the old pending timeout is cancelled so stale callbacks don't run.packages/repl/src/bundler/css-processor.ts (1)
81-93:⚠️ Potential issue | 🟠 MajorPreserve the full
var()fallback expression.Only the first fallback token is captured now, so multi-token defaults are truncated.
♻️ Proposed fix
- const varDefaultValueNode = varFunctionValues[2]; + const varDefaultValueTokens = varFunctionValues.slice(2); @@ - if (varDefaultValueNode) { - const currentDefaultValueText = csstree.generate(varDefaultValueNode); + if (varDefaultValueTokens.length > 0) { + const currentDefaultValueText = varDefaultValueTokens + .map((token) => csstree.generate(token)) + .join('') + .trim(); defaultValueMap[varName] = currentDefaultValueText; item.data = { ...n, type: 'Raw', value: currentDefaultValueText, };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/src/bundler/css-processor.ts` around lines 81 - 93, The code currently takes only varFunctionValues[2] (varDefaultValueNode) which yields the first token of a multi-token fallback, so update the logic in css-processor.ts to capture the entire fallback expression for the var() function: instead of using only varDefaultValueNode, gather all elements from varFunctionValues starting at index 2 (or reconstruct the node representing the value list), then call csstree.generate on that combined subtree to produce the full fallback text, assign that string to defaultValueMap[varName] and to item.data.value (preserving n for other fields) so multi-token defaults are not truncated.packages/repl/src/editor.ts (1)
56-69:⚠️ Potential issue | 🟠 MajorManage Monaco extra-lib lifecycle to prevent global accumulation.
addExtraLib()registrations are global onjavascriptDefaults; calling them percreateEditor()without disposing accumulates type libs across editor instances.♻️ Proposed fix
import * as monaco from 'monaco-editor'; import lynxElementApi from '@lynx-js/type-element-api/types/element-api.d.ts?raw'; import lynxTypesMap from './generated/lynx-types-map.json'; +let extraLibDisposables: monaco.IDisposable[] = []; + export function createEditor( @@ - monaco.languages.typescript.javascriptDefaults.addExtraLib( + extraLibDisposables.forEach((d) => d.dispose()); + extraLibDisposables = []; + + extraLibDisposables.push(monaco.languages.typescript.javascriptDefaults.addExtraLib( lynxElementApi, 'file:///lynx-globals.d.ts', - ); + )); @@ - monaco.languages.typescript.javascriptDefaults.addExtraLib( - content, - `file:///${filePath}`, - ); + extraLibDisposables.push( + monaco.languages.typescript.javascriptDefaults.addExtraLib( + content, + `file:///${filePath}`, + ), + ); } @@ dispose() { @@ cssModel.dispose(); + extraLibDisposables.forEach((d) => d.dispose()); + extraLibDisposables = []; }, }; }#!/bin/bash # Verify extra-lib registration/disposal lifecycle in editor module rg -n "addExtraLib|setExtraLibs|extraLibDispos|dispose\\(" packages/repl/src/editor.tsAlso applies to: 127-134
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/src/editor.ts` around lines 56 - 69, Registrations via monaco.languages.typescript.javascriptDefaults.addExtraLib are global and currently added every time createEditor runs, so collect the IDisposable returned by addExtraLib for the initial lynxElementApi and for each entry in lynxTypesMap and dispose them before re-registering (or use javascriptDefaults.setExtraLibs once with all libs). Concretely: in the code that calls addExtraLib for lynxElementApi and inside the for loop over lynxTypesMap, capture each returned disposable and store them (e.g., module-level array or a property on the editor instance), then call dispose() on each saved disposable before creating new ones in createEditor; alternatively replace the addExtraLib calls with a single javascriptDefaults.setExtraLibs(...) call passing all virtual files to avoid accumulation. Ensure references to addExtraLib, lynxElementApi, lynxTypesMap, createEditor and javascriptDefaults are used to locate and update the code.packages/repl/src/App.tsx (1)
54-63:⚠️ Potential issue | 🟠 MajorGuard URL sample index before dereferencing.
Line 55 reads
samples[urlState.sampleIndex]directly. A malformed hash can makesampleundefined and crash initialization.Proposed fix
if (urlState?.type === 'sample') { - const sample = samples[urlState.sampleIndex]; - return { - code: { - mainThread: sample.mainThread, - background: sample.background, - css: sample.css, - }, - sampleIndex: urlState.sampleIndex, - }; + const sampleIndex = urlState.sampleIndex; + if ( + Number.isInteger(sampleIndex) + && sampleIndex >= 0 + && sampleIndex < samples.length + ) { + const sample = samples[sampleIndex]; + return { + code: { + mainThread: sample.mainThread, + background: sample.background, + css: sample.css, + }, + sampleIndex, + }; + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/src/App.tsx` around lines 54 - 63, The code dereferences samples[urlState.sampleIndex] without validating the index; update the initialization in App.tsx to guard urlState.sampleIndex before use by checking it's a finite integer within 0 <= sampleIndex < samples.length (and not NaN), and only then set sample = samples[sampleIndex]; otherwise fall back to a safe default sample (e.g., samples[0] or the existing default code object) and set sampleIndex accordingly; replace the direct access in the block that returns code.mainThread/background/css so it uses the validated sample and index.
🧹 Nitpick comments (6)
packages/repl/src/examples/global-fetch/main-thread.js (1)
42-52: Consider adding a fallback for the error message.The error branch concatenates
d.errordirectly, which could display"Fetch failed: undefined"if the background thread omits theerrorproperty. A minor defensive tweak would improve UX.🛠️ Suggested improvement
} else { - __SetAttribute(statusRaw, 'text', 'Fetch failed: ' + d.error); + __SetAttribute(statusRaw, 'text', `Fetch failed: ${d.error ?? 'Unknown error'}`); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/src/examples/global-fetch/main-thread.js` around lines 42 - 52, The event listener callback for 'fetchResult' should defensively handle a missing error property: inside the lynx.getJSContext().addEventListener('fetchResult', (event) => { ... }) callback, compute a safe error string (e.g. use d.error if present otherwise a fallback like "unknown error" or the stringified value) and use that safe value when calling __SetAttribute(statusRaw, 'text', ...), leaving the success branch unchanged and still calling __FlushElementTree(); this prevents showing "Fetch failed: undefined" when d.error is omitted.CODEOWNERS (1)
1-8: Consider organizing entries and adding blank lines for readability.The new
packages/repl/**entry is placed at the top, which breaks the flow of the existing package groupings. For better maintainability, consider:
- Organizing entries alphabetically or by logical package grouping (e.g., place repl between react and rspeedy, or after web-platform).
- Adding blank lines between unrelated package groups to improve readability.
Example:
-packages/repl/** `@huxpro` -packages/web-platform/** `@pupiltong` `@Sherry-hue` -packages/webpack/** `@colinaaa` `@upupming` `@luhc228` -packages/rspeedy/** `@colinaaa` `@upupming` `@luhc228` -packages/rspeedy/plugin-react/** `@upupming` packages/react/** `@hzy` `@HuJean` `@Yradex` packages/react/transform/** `@gaoachao` + +packages/repl/** `@huxpro` + +packages/rspeedy/** `@colinaaa` `@upupming` `@luhc228` +packages/rspeedy/plugin-react/** `@upupming` + +packages/webpack/** `@colinaaa` `@upupming` `@luhc228` + +packages/web-platform/** `@pupiltong` `@Sherry-hue` + benchmark/react/** `@hzy` `@HuJean`The ownership assignment to
@huxprois appropriate given the PR context.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@CODEOWNERS` around lines 1 - 8, Reorder and group CODEOWNERS entries for readability: move the packages/repl/** `@huxpro` line so it fits the existing grouping (e.g., place it near packages/react/** or after packages/web-platform/**), arrange entries alphabetically or by logical package groups (web-platform, repl, react, rspeedy, react/transform, benchmark/react), and add blank lines between unrelated groups (e.g., between web-platform/repl/react and rspeedy/react/transform) to improve maintainability while keeping the same ownership handles (e.g., `@huxpro`, `@pupiltong`, `@Sherry-hue`, `@colinaaa`, `@upupming`, `@luhc228`, `@hzy`, `@HuJean`, `@Yradex`, `@gaoachao`).packages/repl/src/examples/global-props-theme/background.js (1)
8-15: MakethemeChangedlistener registration idempotent.Since this is an example file in the REPL package context, it may be re-evaluated. If re-evaluation occurs, the current code will register duplicate listeners and trigger repeated
dispatchEventcalls for each theme toggle. Extract the handler function and clean up the previous listener before adding a new one.♻️ Suggested pattern
const emitter = lynx.getJSModule('GlobalEventEmitter'); +const onThemeChanged = function(theme) { + lynx.getCoreContext().dispatchEvent({ + type: 'themeChanged', + data: { theme }, + }); +}; -emitter.addListener('themeChanged', function(theme) { - lynx.getCoreContext().dispatchEvent({ - type: 'themeChanged', - data: { theme }, - }); -}); +emitter.removeListener?.('themeChanged', onThemeChanged); +emitter.addListener('themeChanged', onThemeChanged);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/src/examples/global-props-theme/background.js` around lines 8 - 15, The themeChanged listener is registered every time this module is re-evaluated, causing duplicate dispatches; make registration idempotent by extracting the handler into a named function (e.g., onThemeChanged) and before calling emitter.addListener('themeChanged', ...) remove any previous registration via emitter.removeListener('themeChanged', onThemeChanged) (or call removeListener unconditionally then addListener), referencing the existing emitter (lynx.getJSModule('GlobalEventEmitter')) and the dispatch call to lynx.getCoreContext().dispatchEvent to ensure only a single listener instance is active.packages/repl/src/examples/interactivity-event-handling/main-thread.js (1)
63-66:removeEventListeneris available; consider ifrenderPageshould support being called multiple times.The
addEventListeneron line 63 is only executed whenrenderPage()is invoked. In the current REPL, each template change creates a fresh context, so duplicate listeners won't naturally accumulate. However, ifrenderPage()is called multiple times within the same context, new listeners will register without removing the previous ones. If that's a supported pattern, storing and removing the previous handler is good defensive practice. Otherwise, this is not critical for the current REPL flow.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/src/examples/interactivity-event-handling/main-thread.js` around lines 63 - 66, The addEventListener call on lynx.getJSContext() inside renderPage can register duplicate handlers if renderPage is called multiple times; change renderPage to store the handler function (e.g., const eventHandler = (e) => {...}) and before adding it call removeEventListener with that stored handler (or remove a previously-stored handler) so you unregister any prior listener; update references to lynx.getJSContext().addEventListener and use the corresponding removeEventListener to clean up the previous eventHandler.packages/repl/src/examples/interactivity-event-main-thread/main-thread.js (1)
52-56: Extract shared box-style generation to remove duplicated literals.The same long style string is repeated in two places. A tiny helper keeps updates safer and easier to read.
♻️ Proposed refactor
+ const getBoxStyle = (color) => + `width:200px; height:200px; background-color:${color}; border-radius:16px; align-items:center; justify-content:center; margin-bottom:16px;`; + const colorBox = __CreateView(0); __AppendElement(container, colorBox); - __SetInlineStyles( - colorBox, - 'width:200px; height:200px; background-color:' + colors[0] - + '; border-radius:16px; align-items:center; justify-content:center; margin-bottom:16px;', - ); + __SetInlineStyles(colorBox, getBoxStyle(colors[0])); @@ - __SetInlineStyles( - colorBox, - 'width:200px; height:200px; background-color:' + colors[colorIndex] - + '; border-radius:16px; align-items:center; justify-content:center; margin-bottom:16px;', - ); + __SetInlineStyles(colorBox, getBoxStyle(colors[colorIndex]));Also applies to: 84-88
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/src/examples/interactivity-event-main-thread/main-thread.js` around lines 52 - 56, The long inline style string used when calling __SetInlineStyles for colorBox is duplicated; extract it into a small helper (e.g., getBoxStyle or buildBoxStyle) that returns the concatenated style string and use that helper in both places where __SetInlineStyles is called (the current colorBox call and the second occurrence around the other color box usage). Replace the literal concatenation with a single call to the helper and pass the color as an argument so the style composition (width/height/border-radius/align-items/justify-content/margin-bottom + background-color) is defined in one place and reused.packages/repl/src/examples/interactivity-event-bg/background.js (1)
33-47: Extract shared counter update dispatch to avoid drift.
onDecrementandonIncrementduplicate the same dispatch payload. A tiny helper keeps both paths consistent.♻️ Proposed refactor
let count = 0; +function emitCounterUpdate() { + lynx.getCoreContext().dispatchEvent({ + type: 'counterUpdate', + data: { count }, + }); +} + globalThis.onDecrement = function(_event) { count--; - lynx.getCoreContext().dispatchEvent({ - type: 'counterUpdate', - data: { count }, - }); + emitCounterUpdate(); }; globalThis.onIncrement = function(_event) { count++; - lynx.getCoreContext().dispatchEvent({ - type: 'counterUpdate', - data: { count }, - }); + emitCounterUpdate(); };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/src/examples/interactivity-event-bg/background.js` around lines 33 - 47, Both onDecrement and onIncrement duplicate the same lynx.getCoreContext().dispatchEvent payload; create a small helper (e.g., updateCounterDispatch or dispatchCounterUpdate) that accepts the new count and performs lynx.getCoreContext().dispatchEvent({ type: 'counterUpdate', data: { count } }), then call that helper from both globalThis.onDecrement and globalThis.onIncrement after mutating count to keep dispatch logic centralized and prevent drift.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/repl/README.md`:
- Around line 19-40: The two fenced code blocks in README.md (the pipeline table
and the ascii diagrams under "This REPL enters at step 6") are missing language
identifiers; update each opening fence to include a language like text (i.e.,
change ``` to ```text for both the table block and the ascii diagram block) so
markdown linting passes while leaving the block contents unchanged.
In `@packages/repl/src/App.tsx`:
- Line 154: Update the timerRef declaration to provide an explicit initial value
for React 19 types: change the useRef call for timerRef so its type is
ReturnType<typeof setTimeout> | undefined and pass undefined as the initial
value (modify the timerRef declaration in App.tsx where timerRef is defined).
In `@packages/repl/src/components/ConsolePanel.tsx`:
- Around line 45-47: The filter tab buttons in ConsolePanel.tsx (the <button>
used with key={tab} and onClick={() => setFilter(tab)}) lack an explicit type
and may submit a parent form; update the button element to include type="button"
so clicking a tab calls setFilter(tab) without triggering form submission.
- Line 1: Remove the broad file-wide ESLint suppression comment ("/*
eslint-disable headers/header-format, sort-imports, import/order,
n/file-extension-in-import, `@typescript-eslint/no-unnecessary-type-assertion`
*/") at the top of ConsolePanel.tsx and either fix the underlying lint
violations or apply targeted, minimal disables: replace the blanket disable with
specific inline disables (e.g., // eslint-disable-next-line <rule-name>) only on
the lines causing issues (imports, header formatting, or specific type
assertions) so that rules like headers/header-format, sort-imports,
import/order, n/file-extension-in-import, and
`@typescript-eslint/no-unnecessary-type-assertion` remain enforced elsewhere in
the file; ensure you reference and adjust the import statements and the exact
lines with unnecessary type assertions or header-format problems when scoping
the disables.
In `@packages/repl/src/components/ui/button.tsx`:
- Around line 36-40: The Button base component renders a plain <button> without
a type, causing default submit behavior inside forms; update the component
(Button in packages/repl/src/components/ui/button.tsx) to ensure a safe default
by passing an explicit type attribute (e.g., type="button" unless a type prop is
provided). Modify the props handling for the Button component (the element that
uses cn(buttonVariants({ variant, size, className })), ref, {...props}) to
supply props.type || 'button' (or set a default in the component's prop
destructuring) so existing usage and forwarding of other props/ref remain
unchanged.
In `@packages/repl/src/examples/interactivity-event-bg/main-thread.js`:
- Around line 93-96: The code currently calls
lynx.getJSContext().addEventListener('counterUpdate', ...) on every renderPage()
causing duplicate handlers; replace the inline anonymous listener with a managed
named handler (e.g., function counterUpdateHandler(event) {
__SetAttribute(counterRaw, 'text', String(event.data.count));
__FlushElementTree(); }) and ensure you remove any previous subscription before
adding it (call lynx.getJSContext().removeEventListener('counterUpdate',
counterUpdateHandler) or store the handler reference on a module/global variable
and only add if not already registered) so that counterUpdate subscriptions are
idempotent across reruns.
In `@packages/repl/src/examples/interactivity-event-main-thread/main-thread.js`:
- Line 90: The displayed tap text always uses the plural "times"; update the
code that sets the attribute for tapInfoRaw (the __SetAttribute call that uses
tapCount) to choose singular when tapCount === 1. Replace the current
unconditional string with a conditional/ternary that builds "Tapped 1 time" for
tapCount === 1 and "Tapped N times" otherwise, using the existing tapCount
variable and keeping the attribute name 'text' and target object tapInfoRaw
unchanged.
- Around line 73-76: The handlers object used by globalThis.runWorklet is
vulnerable to inherited keys; replace the plain object with a null-prototype map
(e.g., handlers = Object.create(null) or a Map) and harden the dispatcher in
runWorklet by checking that handlers[handlerId] (or handlers.get(handlerId)) is
present and typeof === 'function' before calling it; reference the handlers
variable and the globalThis.runWorklet function to locate and update the
registration/storage and the invocation guard.
In `@packages/repl/src/examples/lifecycle-events/main-thread.js`:
- Around line 45-47: The lifecycle event is dispatched with PascalCase in
__OnLifecycleEvent and its log text in addLine; update the event type string
from "FirstScreen" to "firstScreen" in the __OnLifecycleEvent call and change
the addLine message to "Dispatched: { type: 'firstScreen' }" so it matches the
canonical camelCase used elsewhere (e.g., lifecycle-render example) and respects
case-sensitive matching.
---
Duplicate comments:
In `@packages/repl/src/App.tsx`:
- Around line 54-63: The code dereferences samples[urlState.sampleIndex] without
validating the index; update the initialization in App.tsx to guard
urlState.sampleIndex before use by checking it's a finite integer within 0 <=
sampleIndex < samples.length (and not NaN), and only then set sample =
samples[sampleIndex]; otherwise fall back to a safe default sample (e.g.,
samples[0] or the existing default code object) and set sampleIndex accordingly;
replace the direct access in the block that returns
code.mainThread/background/css so it uses the validated sample and index.
In `@packages/repl/src/bundler/css-processor.ts`:
- Around line 81-93: The code currently takes only varFunctionValues[2]
(varDefaultValueNode) which yields the first token of a multi-token fallback, so
update the logic in css-processor.ts to capture the entire fallback expression
for the var() function: instead of using only varDefaultValueNode, gather all
elements from varFunctionValues starting at index 2 (or reconstruct the node
representing the value list), then call csstree.generate on that combined
subtree to produce the full fallback text, assign that string to
defaultValueMap[varName] and to item.data.value (preserving n for other fields)
so multi-token defaults are not truncated.
In `@packages/repl/src/editor.ts`:
- Around line 56-69: Registrations via
monaco.languages.typescript.javascriptDefaults.addExtraLib are global and
currently added every time createEditor runs, so collect the IDisposable
returned by addExtraLib for the initial lynxElementApi and for each entry in
lynxTypesMap and dispose them before re-registering (or use
javascriptDefaults.setExtraLibs once with all libs). Concretely: in the code
that calls addExtraLib for lynxElementApi and inside the for loop over
lynxTypesMap, capture each returned disposable and store them (e.g.,
module-level array or a property on the editor instance), then call dispose() on
each saved disposable before creating new ones in createEditor; alternatively
replace the addExtraLib calls with a single javascriptDefaults.setExtraLibs(...)
call passing all virtual files to avoid accumulation. Ensure references to
addExtraLib, lynxElementApi, lynxTypesMap, createEditor and javascriptDefaults
are used to locate and update the code.
In `@packages/repl/src/examples/styling-inline-styles/main-thread.js`:
- Around line 39-43: renderPage schedules a setTimeout that can leave stale
callbacks mutating nodes; to fix, track the timeout id in a module-level
variable (e.g., inlineStylesTimer) and before scheduling the new setTimeout
clearTimeout(inlineStylesTimer) if present, then assign the new timer id to
inlineStylesTimer; keep the existing callbacks (__SetInlineStyles,
__FlushElementTree) but ensure the old pending timeout is cancelled so stale
callbacks don't run.
In `@packages/repl/src/url-state.ts`:
- Around line 33-38: The returned CodeState currently trusts parsed.background
and parsed.css without type-checking; update the return to validate each
optional field is a string (e.g., background: typeof parsed.background ===
'string' ? parsed.background : '' and css: typeof parsed.css === 'string' ?
parsed.css : '') so malformed/non-string values are replaced with ''. Keep the
existing check for parsed.mainThread being a string and use these guards when
constructing the CodeState object.
- Around line 51-52: The code currently constructs URLSearchParams from
window.location.search (const params) and reads/writes shared REPL state using
query parameters; change this to use the hash fragment instead: initialize
params with new URLSearchParams(window.location.hash.slice(1)) (strip the
leading '#'), and when updating state (the code that writes back params in the
block around lines 73–89) update window.location.hash = '#' + params.toString()
or use history.replaceState to set the hash (not search). Replace any references
to window.location.search with window.location.hash and ensure encoding/decoding
uses the same URLSearchParams API so the REPL sharing contract stays hash-based
and avoids query param leakage.
---
Nitpick comments:
In `@CODEOWNERS`:
- Around line 1-8: Reorder and group CODEOWNERS entries for readability: move
the packages/repl/** `@huxpro` line so it fits the existing grouping (e.g., place
it near packages/react/** or after packages/web-platform/**), arrange entries
alphabetically or by logical package groups (web-platform, repl, react, rspeedy,
react/transform, benchmark/react), and add blank lines between unrelated groups
(e.g., between web-platform/repl/react and rspeedy/react/transform) to improve
maintainability while keeping the same ownership handles (e.g., `@huxpro`,
`@pupiltong`, `@Sherry-hue`, `@colinaaa`, `@upupming`, `@luhc228`, `@hzy`, `@HuJean`, `@Yradex`,
`@gaoachao`).
In `@packages/repl/src/examples/global-fetch/main-thread.js`:
- Around line 42-52: The event listener callback for 'fetchResult' should
defensively handle a missing error property: inside the
lynx.getJSContext().addEventListener('fetchResult', (event) => { ... })
callback, compute a safe error string (e.g. use d.error if present otherwise a
fallback like "unknown error" or the stringified value) and use that safe value
when calling __SetAttribute(statusRaw, 'text', ...), leaving the success branch
unchanged and still calling __FlushElementTree(); this prevents showing "Fetch
failed: undefined" when d.error is omitted.
In `@packages/repl/src/examples/global-props-theme/background.js`:
- Around line 8-15: The themeChanged listener is registered every time this
module is re-evaluated, causing duplicate dispatches; make registration
idempotent by extracting the handler into a named function (e.g.,
onThemeChanged) and before calling emitter.addListener('themeChanged', ...)
remove any previous registration via emitter.removeListener('themeChanged',
onThemeChanged) (or call removeListener unconditionally then addListener),
referencing the existing emitter (lynx.getJSModule('GlobalEventEmitter')) and
the dispatch call to lynx.getCoreContext().dispatchEvent to ensure only a single
listener instance is active.
In `@packages/repl/src/examples/interactivity-event-bg/background.js`:
- Around line 33-47: Both onDecrement and onIncrement duplicate the same
lynx.getCoreContext().dispatchEvent payload; create a small helper (e.g.,
updateCounterDispatch or dispatchCounterUpdate) that accepts the new count and
performs lynx.getCoreContext().dispatchEvent({ type: 'counterUpdate', data: {
count } }), then call that helper from both globalThis.onDecrement and
globalThis.onIncrement after mutating count to keep dispatch logic centralized
and prevent drift.
In `@packages/repl/src/examples/interactivity-event-handling/main-thread.js`:
- Around line 63-66: The addEventListener call on lynx.getJSContext() inside
renderPage can register duplicate handlers if renderPage is called multiple
times; change renderPage to store the handler function (e.g., const eventHandler
= (e) => {...}) and before adding it call removeEventListener with that stored
handler (or remove a previously-stored handler) so you unregister any prior
listener; update references to lynx.getJSContext().addEventListener and use the
corresponding removeEventListener to clean up the previous eventHandler.
In `@packages/repl/src/examples/interactivity-event-main-thread/main-thread.js`:
- Around line 52-56: The long inline style string used when calling
__SetInlineStyles for colorBox is duplicated; extract it into a small helper
(e.g., getBoxStyle or buildBoxStyle) that returns the concatenated style string
and use that helper in both places where __SetInlineStyles is called (the
current colorBox call and the second occurrence around the other color box
usage). Replace the literal concatenation with a single call to the helper and
pass the color as an argument so the style composition
(width/height/border-radius/align-items/justify-content/margin-bottom +
background-color) is defined in one place and reused.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (75)
.github/repl.instructions.md.github/workflows/workflow-website.ymlCODEOWNERSbiome.jsonceslint.config.jspackages/repl/.gitignorepackages/repl/README.mdpackages/repl/index.htmlpackages/repl/package.jsonpackages/repl/postcss.config.mjspackages/repl/rsbuild.config.tspackages/repl/scripts/collect-lynx-types.mjspackages/repl/src/App.tsxpackages/repl/src/bundler/css-processor.tspackages/repl/src/bundler/template-builder.tspackages/repl/src/components/ConsolePanel.tsxpackages/repl/src/components/EditorPane.tsxpackages/repl/src/components/EditorWindow.tsxpackages/repl/src/components/Header.tsxpackages/repl/src/components/LynxPreview.tsxpackages/repl/src/components/PreviewPane.tsxpackages/repl/src/components/ui/button.tsxpackages/repl/src/components/ui/resizable.tsxpackages/repl/src/components/ui/select.tsxpackages/repl/src/components/ui/separator.tsxpackages/repl/src/console/console-wrapper.tspackages/repl/src/console/types.tspackages/repl/src/console/useConsole.tspackages/repl/src/editor.tspackages/repl/src/env.d.tspackages/repl/src/examples/attributes-dataset/main-thread.jspackages/repl/src/examples/attributes-set-and-get/main-thread.jspackages/repl/src/examples/demo-background-counter/background.jspackages/repl/src/examples/demo-background-counter/main-thread.jspackages/repl/src/examples/demo-data-driven-list/background.jspackages/repl/src/examples/demo-data-driven-list/main-thread.jspackages/repl/src/examples/global-console/background.jspackages/repl/src/examples/global-console/main-thread.jspackages/repl/src/examples/global-fetch/background.jspackages/repl/src/examples/global-fetch/main-thread.jspackages/repl/src/examples/global-props-theme/background.jspackages/repl/src/examples/global-props-theme/main-thread.jspackages/repl/src/examples/global-props/main-thread.jspackages/repl/src/examples/global-system-info/main-thread.jspackages/repl/src/examples/interactivity-event-bg/background.jspackages/repl/src/examples/interactivity-event-bg/main-thread.jspackages/repl/src/examples/interactivity-event-handling/background.jspackages/repl/src/examples/interactivity-event-handling/main-thread.jspackages/repl/src/examples/interactivity-event-main-thread/main-thread.jspackages/repl/src/examples/interactivity-get-element-by-id/main-thread.jspackages/repl/src/examples/interactivity-refs-bg/background.jspackages/repl/src/examples/interactivity-refs-bg/main-thread.jspackages/repl/src/examples/interactivity-refs-main-thread/main-thread.jspackages/repl/src/examples/interactivity-selector-query/main-thread.jspackages/repl/src/examples/lifecycle-events/main-thread.jspackages/repl/src/examples/lifecycle-raf/main-thread.jspackages/repl/src/examples/lifecycle-render/background.jspackages/repl/src/examples/lifecycle-render/main-thread.jspackages/repl/src/examples/list-virtualization/main-thread.jspackages/repl/src/examples/styling-css-classes-and-scoping/index.csspackages/repl/src/examples/styling-css-classes-and-scoping/main-thread.jspackages/repl/src/examples/styling-inline-styles/main-thread.jspackages/repl/src/examples/threading-cross-thread/background.jspackages/repl/src/examples/threading-cross-thread/main-thread.jspackages/repl/src/examples/ui-tree-creating-elements/main-thread.jspackages/repl/src/examples/ui-tree-element-types/main-thread.jspackages/repl/src/examples/ui-tree-tree-mutations/main-thread.jspackages/repl/src/examples/ui-tree-tree-traversal/main-thread.jspackages/repl/src/globals.csspackages/repl/src/index.tsxpackages/repl/src/local-storage.tspackages/repl/src/samples.tspackages/repl/src/url-state.tspackages/repl/tsconfig.jsonpnpm-workspace.yaml
🚧 Files skipped from review as they are similar to previous changes (43)
- packages/repl/index.html
- packages/repl/src/examples/attributes-set-and-get/main-thread.js
- packages/repl/src/index.tsx
- packages/repl/tsconfig.json
- packages/repl/src/components/ui/separator.tsx
- packages/repl/src/components/ui/resizable.tsx
- packages/repl/package.json
- packages/repl/src/examples/interactivity-event-handling/background.js
- packages/repl/src/components/Header.tsx
- packages/repl/src/examples/list-virtualization/main-thread.js
- biome.jsonc
- packages/repl/src/local-storage.ts
- packages/repl/src/examples/ui-tree-tree-mutations/main-thread.js
- packages/repl/src/examples/lifecycle-render/background.js
- packages/repl/src/examples/attributes-dataset/main-thread.js
- packages/repl/src/examples/demo-data-driven-list/main-thread.js
- packages/repl/src/examples/global-props/main-thread.js
- packages/repl/src/console/types.ts
- packages/repl/src/examples/interactivity-selector-query/main-thread.js
- packages/repl/.gitignore
- packages/repl/postcss.config.mjs
- packages/repl/src/components/PreviewPane.tsx
- packages/repl/src/examples/global-system-info/main-thread.js
- packages/repl/src/examples/global-console/main-thread.js
- packages/repl/src/examples/demo-background-counter/main-thread.js
- .github/repl.instructions.md
- packages/repl/src/examples/interactivity-refs-bg/main-thread.js
- packages/repl/src/console/console-wrapper.ts
- packages/repl/src/examples/global-props-theme/main-thread.js
- packages/repl/src/examples/interactivity-refs-main-thread/main-thread.js
- packages/repl/src/examples/threading-cross-thread/main-thread.js
- packages/repl/src/examples/lifecycle-raf/main-thread.js
- packages/repl/src/console/useConsole.ts
- packages/repl/src/examples/styling-css-classes-and-scoping/main-thread.js
- packages/repl/src/examples/ui-tree-element-types/main-thread.js
- packages/repl/src/examples/ui-tree-creating-elements/main-thread.js
- .github/workflows/workflow-website.yml
- packages/repl/src/examples/demo-data-driven-list/background.js
- packages/repl/src/examples/global-fetch/background.js
- packages/repl/src/examples/styling-css-classes-and-scoping/index.css
- packages/repl/src/components/EditorWindow.tsx
- packages/repl/src/examples/interactivity-get-element-by-id/main-thread.js
- packages/repl/src/examples/demo-background-counter/background.js
| ``` | ||
| Step What happens Tool / Plugin | ||
| ──── ────────────────────────────────── ───────────────────────────── | ||
| 1. Source Transform (JSX/TS → JS) rspack loader (SWC) | ||
| 2. Module Resolution (import/require) rspack resolver | ||
| 3. CSS Processing (CSS → LynxStyleNode) @lynx-js/css-serializer | ||
| 4. Bundling (multi-file → chunks) rspack | ||
| 5. Asset Tagging (lynx:main-thread) MarkMainThreadPlugin | ||
| 6. Template Assembly (assets → data) LynxTemplatePlugin | ||
| 7. Encoding (data → binary/JSON) @lynx-js/tasm / WebEncodePlugin | ||
| 8. Emit (write to disk) rspack compilation | ||
| ``` | ||
|
|
||
| This REPL **enters at step 6** and takes a shortcut: | ||
|
|
||
| ``` | ||
| rspeedy dev (full pipeline): | ||
| [1] → [2] → [3] → [4] → [5] → [6] → [7] → [8] → lynx-view (via URL fetch) | ||
|
|
||
| This REPL (pure browser): | ||
| [3'] → [6'] ─────────→ lynx-view (via callback) | ||
| ``` |
There was a problem hiding this comment.
Add language identifiers to fenced code blocks.
Both code fences should include a language (e.g., text) to satisfy markdown linting.
📝 Proposed fix
-```
+```text
Step What happens Tool / Plugin
@@
-```
+```
-```
+```text
rspeedy dev (full pipeline):
@@
-```
+```🧰 Tools
🪛 markdownlint-cli2 (0.21.0)
[warning] 19-19: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
[warning] 34-34: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/repl/README.md` around lines 19 - 40, The two fenced code blocks in
README.md (the pipeline table and the ascii diagrams under "This REPL enters at
step 6") are missing language identifiers; update each opening fence to include
a language like text (i.e., change ``` to ```text for both the table block and
the ascii diagram block) so markdown linting passes while leaving the block
contents unchanged.
| }, []); | ||
|
|
||
| // Debounced rebuild + persist | ||
| const timerRef = useRef<ReturnType<typeof setTimeout>>(); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== React / `@types/react` versions found in package manifests ==="
fd package.json -HI -E node_modules | xargs rg -n '"react"|"@types/react"' | head -20
echo
echo "=== Checking React version more specifically ==="
cat package.json | grep -A2 -B2 '"react"' || echo "Check root package.json"Repository: lynx-family/lynx-stack
Length of output: 1611
🏁 Script executed:
#!/bin/bash
# Check the actual file and its context
echo "=== Content around line 154 in packages/repl/src/App.tsx ==="
head -160 packages/repl/src/App.tsx | tail -15
echo
echo "=== Checking tsconfig.json settings ==="
cat tsconfig.json | head -30
echo
echo "=== No-arg useRef invocations in the file ==="
rg -nP 'useRef<[^>]+>\(\s*\)' packages/repl/src/App.tsx 2>/dev/null || echo "No matches found"Repository: lynx-family/lynx-stack
Length of output: 3607
🏁 Script executed:
#!/bin/bash
# Check what useRef usage patterns exist across the codebase
echo "=== useRef patterns in the repl package ==="
rg 'useRef' packages/repl/src/ -A2 -B1 | head -50Repository: lynx-family/lynx-stack
Length of output: 3492
🌐 Web query:
React 19 useRef no initial value type error breaking change
💡 Result:
In React 19’s TypeScript types (react@19 / @types/react@19), useRef no longer has a zero-argument overload, so code like this now fails:
const r = useRef<MyType>(); // ❌ React 18 ok, React 19 types errorThis is an intentional type-level breaking change tracked in DefinitelyTyped as “Require initial value for useRef” (codemod name: useref-required-initial). [1]
Fix
Pass an explicit initial value that matches what you mean:
1) “Starts empty” value container
const r = useRef<MyType | undefined>(undefined); // ✅React 19 types require an argument; undefined is the explicit “no value yet”. [1][2]
2) DOM/instance refs (typical)
const inputRef = useRef<HTMLInputElement | null>(null); // ✅null remains the conventional initializer for element refs. [2]
Why it shows up as a “breaking change”
React’s docs show the signature as useRef(initialValue) (i.e., it expects an initial value), and the React 19 TS typings now enforce that expectation. [2]
Sources: React useRef docs [2]; DefinitelyTyped React 19 breaking-type-change list (includes “Require initial value for useRef”) [1].
🏁 Script executed:
#!/bin/bash
# Check if there are any type suppressions or ignores around this line
echo "=== Context around line 154 with potential suppressions ==="
sed -n '150,160p' packages/repl/src/App.tsxRepository: lynx-family/lynx-stack
Length of output: 464
🏁 Script executed:
#!/bin/bash
# Check if the project has any type checking bypasses or if this would fail type checking
echo "=== Check for biome/eslint type comment suppressions ==="
rg '@ts-' packages/repl/src/App.tsxRepository: lynx-family/lynx-stack
Length of output: 122
Pass an explicit initial value to useRef for React 19 type compatibility.
React 19's TypeScript types require an explicit initial value for useRef — the zero-argument overload has been removed as a breaking change. Line 154's useRef<ReturnType<typeof setTimeout>>() will fail type checking. Add undefined as the initial value and update the type annotation:
- const timerRef = useRef<ReturnType<typeof setTimeout>>();
+ const timerRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/repl/src/App.tsx` at line 154, Update the timerRef declaration to
provide an explicit initial value for React 19 types: change the useRef call for
timerRef so its type is ReturnType<typeof setTimeout> | undefined and pass
undefined as the initial value (modify the timerRef declaration in App.tsx where
timerRef is defined).
| @@ -0,0 +1,165 @@ | |||
| /* eslint-disable headers/header-format, sort-imports, import/order, n/file-extension-in-import, @typescript-eslint/no-unnecessary-type-assertion */ | |||
There was a problem hiding this comment.
Avoid broad file-wide ESLint suppression.
This disables multiple rules for the entire component. Please scope disables to the minimal line/rule (or fix underlying violations) to keep lint guarantees intact.
As per coding guidelines "Follow ESLint rules as configured in eslint.config.js, including React and TypeScript-specific rules".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/repl/src/components/ConsolePanel.tsx` at line 1, Remove the broad
file-wide ESLint suppression comment ("/* eslint-disable headers/header-format,
sort-imports, import/order, n/file-extension-in-import,
`@typescript-eslint/no-unnecessary-type-assertion` */") at the top of
ConsolePanel.tsx and either fix the underlying lint violations or apply
targeted, minimal disables: replace the blanket disable with specific inline
disables (e.g., // eslint-disable-next-line <rule-name>) only on the lines
causing issues (imports, header formatting, or specific type assertions) so that
rules like headers/header-format, sort-imports, import/order,
n/file-extension-in-import, and `@typescript-eslint/no-unnecessary-type-assertion`
remain enforced elsewhere in the file; ensure you reference and adjust the
import statements and the exact lines with unnecessary type assertions or
header-format problems when scoping the disables.
| <button | ||
| key={tab} | ||
| onClick={() => setFilter(tab)} |
There was a problem hiding this comment.
Set explicit button type for filter tabs.
Without type="button", these can submit a parent form unexpectedly.
🔧 Proposed fix
<button
key={tab}
+ type='button'
onClick={() => setFilter(tab)}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <button | |
| key={tab} | |
| onClick={() => setFilter(tab)} | |
| <button | |
| key={tab} | |
| type='button' | |
| onClick={() => setFilter(tab)} |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/repl/src/components/ConsolePanel.tsx` around lines 45 - 47, The
filter tab buttons in ConsolePanel.tsx (the <button> used with key={tab} and
onClick={() => setFilter(tab)}) lack an explicit type and may submit a parent
form; update the button element to include type="button" so clicking a tab calls
setFilter(tab) without triggering form submission.
| <button | ||
| className={cn(buttonVariants({ variant, size, className }))} | ||
| ref={ref} | ||
| {...props} | ||
| /> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat packages/repl/src/components/ui/button.tsxRepository: lynx-family/lynx-stack
Length of output: 1663
🏁 Script executed:
rg -nP --type=tsx -C3 '<form|<Button\b' packages/repl/srcRepository: lynx-family/lynx-stack
Length of output: 93
🏁 Script executed:
cat eslint.config.jsRepository: lynx-family/lynx-stack
Length of output: 13459
🏁 Script executed:
cat tsconfig.jsonRepository: lynx-family/lynx-stack
Length of output: 12731
Set a safe default type for the base button component.
The button element renders without an explicit type attribute. HTML buttons default to type="submit" inside forms, which causes unintended form submissions when this component is used without explicitly passing a type prop.
💡 Proposed fix
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
- ({ className, variant, size, ...props }, ref) => {
+ ({ className, variant, size, type = 'button', ...props }, ref) => {
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
+ type={type}
ref={ref}
{...props}
/>
);
},
);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/repl/src/components/ui/button.tsx` around lines 36 - 40, The Button
base component renders a plain <button> without a type, causing default submit
behavior inside forms; update the component (Button in
packages/repl/src/components/ui/button.tsx) to ensure a safe default by passing
an explicit type attribute (e.g., type="button" unless a type prop is provided).
Modify the props handling for the Button component (the element that uses
cn(buttonVariants({ variant, size, className })), ref, {...props}) to supply
props.type || 'button' (or set a default in the component's prop destructuring)
so existing usage and forwarding of other props/ref remain unchanged.
| lynx.getJSContext().addEventListener('counterUpdate', (event) => { | ||
| __SetAttribute(counterRaw, 'text', String(event.data.count)); | ||
| __FlushElementTree(); | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Check how JSContext event APIs are used across repo
echo "=== Searching for addEventListener/removeEventListener usage ==="
rg -nP --type=js --type=ts 'getJSContext\(\)\.(addEventListener|removeEventListener)\('
echo ""
echo "=== Searching for renderPage patterns in examples ==="
rg -nP --type=js 'renderPage\s*=\s*function\s+renderPage\b' packages/repl/src/examplesRepository: lynx-family/lynx-stack
Length of output: 4887
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check the specific file in question
echo "=== Contents of main-thread.js around lines 93-96 ==="
if [ -f "packages/repl/src/examples/interactivity-event-bg/main-thread.js" ]; then
sed -n '85,110p' packages/repl/src/examples/interactivity-event-bg/main-thread.js
else
echo "File not found"
fiRepository: lynx-family/lynx-stack
Length of output: 585
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Search for JSContext definition/implementation
echo "=== Searching for JSContext class/interface definition ==="
rg -nP 'class JSContext|interface JSContext|export.*JSContext' --type=ts --type=js | head -20
echo ""
echo "=== Searching for addEventListener implementation on JSContext ==="
rg -nP -B 2 -A 10 'addEventListener.*function|addEventListener\s*\(' --type=ts --type=js | grep -A 10 JSContext | head -30Repository: lynx-family/lynx-stack
Length of output: 3344
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Search for REPL execution logic and renderPage invocations
echo "=== Searching for renderPage invocations in REPL/runner ==="
rg -nP 'renderPage\(' --type=js --type=ts packages/repl | head -20
echo ""
echo "=== Checking for reload/rerun mechanism in REPL ==="
rg -nP '(reload|rerun|reset|restart)' --type=ts --type=js packages/repl/src | grep -i -E '(reload|rerun|reset|restart|execute)' | head -15Repository: lynx-family/lynx-stack
Length of output: 2757
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Look at other examples that have addEventListener inside renderPage to see patterns
echo "=== Check other example files with addEventListener inside renderPage ==="
for file in \
"packages/repl/src/examples/global-props-theme/main-thread.js" \
"packages/repl/src/examples/interactivity-event-handling/main-thread.js" \
"packages/repl/src/examples/lifecycle-render/main-thread.js"; do
if [ -f "$file" ]; then
echo "=== $file ==="
rg -nP -B 2 -A 5 'addEventListener' "$file" | head -15
echo ""
fi
doneRepository: lynx-family/lynx-stack
Length of output: 1571
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Search for REPL execution/evaluation logic
echo "=== Searching for code execution in REPL components ==="
rg -nP '(eval|execute|run).*renderPage|renderPage.*function' --type=ts --type=jsx packages/repl/src/components | head -15
echo ""
echo "=== Looking for editor/execution flow in REPL ==="
fd -t f '\.(ts|tsx)$' packages/repl/src/components | xargs rg -l 'execute\|eval\|run' | head -5Repository: lynx-family/lynx-stack
Length of output: 166
🏁 Script executed:
#!/bin/bash
set -euoripefail
# Check main REPL component structure
echo "=== Check LynxPreview component for execution flow ==="
if [ -f "packages/repl/src/components/LynxPreview.tsx" ]; then
wc -l packages/repl/src/components/LynxPreview.tsx
echo ""
# Look for where code gets executed
rg -nP -B 2 -A 5 'eval|Function|new Function|executeCode|runCode' packages/repl/src/components/LynxPreview.tsx | head -30
fiRepository: lynx-family/lynx-stack
Length of output: 740
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check main REPL component structure
echo "=== Check LynxPreview component for execution flow ==="
if [ -f "packages/repl/src/components/LynxPreview.tsx" ]; then
wc -l packages/repl/src/components/LynxPreview.tsx
echo ""
# Look for where code gets executed
rg -nP -B 2 -A 5 'eval|Function|new Function|executeCode|runCode' packages/repl/src/components/LynxPreview.tsx | head -30
fiRepository: lynx-family/lynx-stack
Length of output: 170
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Search for actual script execution in REPL
echo "=== Looking for script execution/eval in REPL runner ==="
fd -t f '\(ts\|tsx\)$' packages/repl/src | xargs rg -l 'eval\|Function\|execute' | head -10Repository: lynx-family/lynx-stack
Length of output: 124
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Look at REPL package.json and structure
echo "=== REPL package structure ==="
find packages/repl/src -type f -name '*.ts' -o -name '*.tsx' | head -20
echo ""
echo "=== Search for where user code gets executed/loaded ==="
rg -nP 'new Function|eval\(|globalThis\.renderPage' --type=ts --type=js packages/repl/src | grep -v node_modules | head -20Repository: lynx-family/lynx-stack
Length of output: 3250
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Read LynxPreview.tsx to understand execution flow
echo "=== LynxPreview.tsx (main execution component) ==="
cat -n packages/repl/src/components/LynxPreview.tsxRepository: lynx-family/lynx-stack
Length of output: 6031
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Look for where renderPage gets called/invoked
echo "=== Search for renderPage() call in REPL code ==="
rg -nP 'renderPage\(' --type=ts --type=js packages/repl/src | grep -v 'function renderPage\|globalThis\.renderPage\s*='Repository: lynx-family/lynx-stack
Length of output: 115
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Look at template loading and boot mechanism
echo "=== Search for how template code gets executed ==="
rg -nP 'customTemplateLoader|evalTemplate|boot|execute.*script' --type=ts packages/repl/src | head -20Repository: lynx-family/lynx-stack
Length of output: 594
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check the web-core/lynx-view to understand renderPage invocation
echo "=== Search for renderPage being called in core ==="
rg -nP 'renderPage' --type=ts --type=js packages/web-platform | head -20Repository: lynx-family/lynx-stack
Length of output: 2251
Prevent duplicate counterUpdate subscriptions across reruns.
Line 93 adds a new listener on every renderPage() call. In REPL rerun/reload flows (triggered by template changes), this can accumulate handlers and apply duplicate UI updates per event.
🛠️ Proposed fix (replace one-off inline listener with managed handler)
+let counterUpdateHandler;
+
globalThis.renderPage = function renderPage() {
@@
- lynx.getJSContext().addEventListener('counterUpdate', (event) => {
- __SetAttribute(counterRaw, 'text', String(event.data.count));
- __FlushElementTree();
- });
+ const jsContext = lynx.getJSContext();
+ if (
+ counterUpdateHandler &&
+ typeof jsContext.removeEventListener === 'function'
+ ) {
+ jsContext.removeEventListener('counterUpdate', counterUpdateHandler);
+ }
+
+ counterUpdateHandler = (event) => {
+ __SetAttribute(counterRaw, 'text', String(event.data.count));
+ __FlushElementTree();
+ };
+ jsContext.addEventListener('counterUpdate', counterUpdateHandler);
};🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/repl/src/examples/interactivity-event-bg/main-thread.js` around
lines 93 - 96, The code currently calls
lynx.getJSContext().addEventListener('counterUpdate', ...) on every renderPage()
causing duplicate handlers; replace the inline anonymous listener with a managed
named handler (e.g., function counterUpdateHandler(event) {
__SetAttribute(counterRaw, 'text', String(event.data.count));
__FlushElementTree(); }) and ensure you remove any previous subscription before
adding it (call lynx.getJSContext().removeEventListener('counterUpdate',
counterUpdateHandler) or store the handler reference on a module/global variable
and only add if not already registered) so that counterUpdate subscriptions are
idempotent across reruns.
| const handlers = {}; | ||
| globalThis.runWorklet = function(handlerId, args) { | ||
| if (handlers[handlerId]) handlers[handlerId](...args); | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Read the full file to understand context
echo "=== Full file content ==="
cat -n "packages/repl/src/examples/interactivity-event-main-thread/main-thread.js"Repository: lynx-family/lynx-stack
Length of output: 4227
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Search for all runWorklet invocations and context
echo "=== runWorklet definition and call sites ==="
rg -nP -C3 'runWorklet' --type js --type ts
# Check how handlers are registered in this file
echo -e "\n=== Handler registration patterns ==="
rg -nP -C2 'handlers\[' --type js --type tsRepository: lynx-family/lynx-stack
Length of output: 50378
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check for worklet event registrations
echo "=== Worklet event registration ==="
rg -nP -C2 "type:\s*['\"]worklet['\"]" --type js --type ts
# Look for handler ID generation patterns
echo -e "\n=== Handler or event ID patterns ==="
rg -nP -C2 "handlerId|handler\s*:" --type js --type ts | head -100Repository: lynx-family/lynx-stack
Length of output: 16483
Harden runWorklet dispatch against inherited-key lookups.
Using a plain {} with dynamic keys can hit inherited properties (for example __proto__) and attempt to call a non-function. Use a null-prototype map and a function guard.
🔧 Proposed fix
- const handlers = {};
+ const handlers = Object.create(null);
globalThis.runWorklet = function(handlerId, args) {
- if (handlers[handlerId]) handlers[handlerId](...args);
+ const handler = handlers[handlerId];
+ if (typeof handler === 'function') handler(...args);
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const handlers = {}; | |
| globalThis.runWorklet = function(handlerId, args) { | |
| if (handlers[handlerId]) handlers[handlerId](...args); | |
| }; | |
| const handlers = Object.create(null); | |
| globalThis.runWorklet = function(handlerId, args) { | |
| const handler = handlers[handlerId]; | |
| if (typeof handler === 'function') handler(...args); | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/repl/src/examples/interactivity-event-main-thread/main-thread.js`
around lines 73 - 76, The handlers object used by globalThis.runWorklet is
vulnerable to inherited keys; replace the plain object with a null-prototype map
(e.g., handlers = Object.create(null) or a Map) and harden the dispatcher in
runWorklet by checking that handlers[handlerId] (or handlers.get(handlerId)) is
present and typeof === 'function' before calling it; reference the handlers
variable and the globalThis.runWorklet function to locate and update the
registration/storage and the invocation guard.
| + '; border-radius:16px; align-items:center; justify-content:center; margin-bottom:16px;', | ||
| ); | ||
| __SetAttribute(colorLabelRaw, 'text', colors[colorIndex]); | ||
| __SetAttribute(tapInfoRaw, 'text', 'Tapped ' + tapCount + ' times'); |
There was a problem hiding this comment.
Handle singular/plural tap text.
For tapCount === 1, the current message becomes “Tapped 1 times”.
✏️ Proposed fix
- __SetAttribute(tapInfoRaw, 'text', 'Tapped ' + tapCount + ' times');
+ __SetAttribute(
+ tapInfoRaw,
+ 'text',
+ `Tapped ${tapCount} time${tapCount === 1 ? '' : 's'}`,
+ );📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| __SetAttribute(tapInfoRaw, 'text', 'Tapped ' + tapCount + ' times'); | |
| __SetAttribute( | |
| tapInfoRaw, | |
| 'text', | |
| `Tapped ${tapCount} time${tapCount === 1 ? '' : 's'}`, | |
| ); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/repl/src/examples/interactivity-event-main-thread/main-thread.js` at
line 90, The displayed tap text always uses the plural "times"; update the code
that sets the attribute for tapInfoRaw (the __SetAttribute call that uses
tapCount) to choose singular when tapCount === 1. Replace the current
unconditional string with a conditional/ternary that builds "Tapped 1 time" for
tapCount === 1 and "Tapped N times" otherwise, using the existing tapCount
variable and keeping the attribute name 'text' and target object tapInfoRaw
unchanged.
| __OnLifecycleEvent({ type: "FirstScreen", timestamp: Date.now() }); | ||
|
|
||
| addLine("Dispatched: { type: 'FirstScreen' }"); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect lifecycle event payloads emitted in REPL examples
rg -nP --type=js "__OnLifecycleEvent\s*\(\s*\{[^}]*type:\s*['\"][^'\"]+['\"]" packages/repl/src/examples
# Compare casing usage directly
rg -nP --type=js "firstScreen|FirstScreen" packages/repl/src/examples -C2Repository: lynx-family/lynx-stack
Length of output: 2231
Normalize lifecycle event type casing to firstScreen
Line 45 dispatches "FirstScreen" (PascalCase) while the related lifecycle example at line 52 of packages/repl/src/examples/lifecycle-render/main-thread.js uses 'firstScreen' (camelCase). The comment at line 37 also references firstScreen as the canonical form, confirming case-sensitive event matching expects lowercase. Both lines 45 and 47 must be normalized to firstScreen.
🔧 Proposed fix
- __OnLifecycleEvent({ type: "FirstScreen", timestamp: Date.now() });
+ __OnLifecycleEvent({ type: "firstScreen", timestamp: Date.now() });
- addLine("Dispatched: { type: 'FirstScreen' }");
+ addLine("Dispatched: { type: 'firstScreen' }");🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/repl/src/examples/lifecycle-events/main-thread.js` around lines 45 -
47, The lifecycle event is dispatched with PascalCase in __OnLifecycleEvent and
its log text in addLine; update the event type string from "FirstScreen" to
"firstScreen" in the __OnLifecycleEvent call and change the addLine message to
"Dispatched: { type: 'firstScreen' }" so it matches the canonical camelCase used
elsewhere (e.g., lifecycle-render example) and respects case-sensitive matching.
- Run dprint fmt to fix formatting in 29 files (quote style, trailing commas, line lengths) - Add pnpm peerDependencyRules to allow tailwindcss v4 alongside v3 (rsbuild-plugin-tailwindcss@0.2.4 expects v3 peer but repl uses v4) - Run pnpm dedupe to clean up lockfile after adding peerDependencyRules - Pin @lynx-js/types to exact "3.7.0" in packages/repl (consistent with rest of workspace) - Add -i tailwindcss -i @types/react to sherif to allow legitimate version splits (repl uses React 19 + Tailwind v4; existing packages use React 18 + Tailwind v3) - Add repl/src/components and repl/src/editor.ts to eslint ignores (Vite path aliases and ?raw imports not supported by root tsconfig resolver) - Fix console.log → console.info in collect-lynx-types.mjs (biome lint)
- Inline collect-lynx-types.mjs into build/dev scripts since enablePrePostScripts is false and prebuild hooks don't run - Fix biome noConfusingVoidType lint error in css-processor.ts - Fix eslint no-unused-expressions error in css-processor.ts
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (6)
packages/repl/README.md (1)
19-30:⚠️ Potential issue | 🟡 MinorAdd language identifiers to fenced code blocks.
Both fences should specify a language (use
text) to satisfy MD040.🛠️ Proposed fix
-``` +```text Step What happens Tool / Plugin ──── ────────────────────────────────── ───────────────────────────── 1. Source Transform (JSX/TS → JS) rspack loader (SWC) 2. Module Resolution (import/require) rspack resolver 3. CSS Processing (CSS → LynxStyleNode) `@lynx-js/css-serializer` 4. Bundling (multi-file → chunks) rspack 5. Asset Tagging (lynx:main-thread) MarkMainThreadPlugin 6. Template Assembly (assets → data) LynxTemplatePlugin 7. Encoding (data → binary/JSON) `@lynx-js/tasm` / WebEncodePlugin 8. Emit (write to disk) rspack compilation-
+text
rspeedy dev (full pipeline):
[1] → [2] → [3] → [4] → [5] → [6] → [7] → [8] → lynx-view (via URL fetch)This REPL (pure browser):
[3'] → [6'] ─────────→ lynx-view (via callback)Also applies to: 34-40
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/README.md` around lines 19 - 30, The README has fenced code blocks missing language identifiers (MD040); update both backtick fences around the ASCII table and the rspeedy/dev snippet to use ```text instead of ``` so the table and the "rspeedy dev (full pipeline):" snippet are marked as text; ensure you change the opening fences for the table block and the subsequent pipeline block (the blocks containing the "Step What happens" table and the "rspeedy dev (full pipeline):" lines) to ```text so the linter passes.packages/repl/src/examples/lifecycle-raf/main-thread.js (1)
36-65:⚠️ Potential issue | 🟠 MajorPrevent stacked RAF loops across re-renders by adding explicit cancellation.
Line 60 and Line 64 schedule frames, but the id is never retained/cancelled. If
renderPageruns again in the same context, old loops continue running.Suggested fix
globalThis.renderPage = function renderPage() { + // Stop previous animation loop if this example is re-rendered. + globalThis.__lifecycleRafCleanup?.(); + const page = __CreatePage('page', 0); @@ - function animate() { + let rafId = -1; + let stopped = false; + + function animate() { + if (stopped) return; frame++; @@ - lynx.requestAnimationFrame(animate); + rafId = lynx.requestAnimationFrame(animate); } @@ - lynx.requestAnimationFrame(animate); + rafId = lynx.requestAnimationFrame(animate); + + globalThis.__lifecycleRafCleanup = function __lifecycleRafCleanup() { + stopped = true; + if (rafId !== -1) lynx.cancelAnimationFrame(rafId); + rafId = -1; + };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/src/examples/lifecycle-raf/main-thread.js` around lines 36 - 65, The RAF loop can be doubled on re-render because request IDs are never stored or cancelled; add a persistent variable (e.g., frameId) alongside animate and use lynx.requestAnimationFrame to assign the returned id to it, then call lynx.cancelAnimationFrame(frameId) before scheduling a new frame (and on cleanup/unmount) so old loops are cancelled; update calls around the animate function and the initial lynx.requestAnimationFrame(animate) to use and manage this frameId.packages/repl/src/examples/lifecycle-events/main-thread.js (1)
48-50:⚠️ Potential issue | 🟠 MajorUse canonical lifecycle type
firstScreen(case-sensitive).Line 48 dispatches
'FirstScreen', but Line 40 documentsfirstScreenas the canonical event type. This mismatch can break listeners expecting lowercase.🔧 Proposed fix
- __OnLifecycleEvent({ type: 'FirstScreen', timestamp: Date.now() }); + __OnLifecycleEvent({ type: 'firstScreen', timestamp: Date.now() }); - addLine('Dispatched: { type: \'FirstScreen\' }'); + addLine('Dispatched: { type: \'firstScreen\' }');#!/bin/bash set -euo pipefail # Verify lifecycle event type casing across REPL examples rg -nP --type=js "__OnLifecycleEvent\s*\(\s*\{[^}]*type:\s*['\"][^'\"]+['\"]" packages/repl/src/examples -C2 rg -nP --type=js "firstScreen|FirstScreen" packages/repl/src/examples -C2🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/src/examples/lifecycle-events/main-thread.js` around lines 48 - 50, Change the lifecycle event dispatch to use the canonical, case-sensitive type "firstScreen" so listeners match; in the snippet calling __OnLifecycleEvent({ type: 'FirstScreen', timestamp: Date.now() }) replace the type value with 'firstScreen' and update the accompanying addLine message (addLine('Dispatched: { type: \'firstScreen\' }')) to keep the example consistent with the documented canonical type.packages/repl/src/examples/styling-inline-styles/main-thread.js (1)
42-52:⚠️ Potential issue | 🟠 MajorCancel previous delayed style updates before scheduling a new one.
Line 42 schedules a new timer on every
renderPagecall. If the example is rerun quickly, older timers can still fire and mutate outdated nodes.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/src/examples/styling-inline-styles/main-thread.js` around lines 42 - 52, renderPage currently schedules a new setTimeout on every call which can leave old timers mutating outdated nodes; introduce a module-scoped timer variable (e.g., pendingStyleTimer) and call clearTimeout(pendingStyleTimer) before creating a new setTimeout, then assign the returned id to pendingStyleTimer so that __SetInlineStyles and __FlushElementTree are only run from the latest scheduled timer and previous timers are canceled.packages/repl/src/editor.ts (1)
64-77:⚠️ Potential issue | 🟠 MajorDispose Monaco extra libs; they outlive editor/model disposal.
Lines 64-77 register libs into Monaco’s global TypeScript defaults, but Line 135-142 only disposes editors/models. Recreating the editor can accumulate duplicate libs and stale typings unless those extra-lib disposables are explicitly cleaned up.
In monaco-editor, does disposing an editor/model automatically remove extra libs added via monaco.languages.typescript.javascriptDefaults.addExtraLib()? If not, what is the recommended cleanup pattern?Also applies to: 135-142
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/src/editor.ts` around lines 64 - 77, The extra libs added with monaco.languages.typescript.javascriptDefaults.addExtraLib (e.g., the call that registers lynxElementApi and the loop over lynxTypesMap) are global and are not removed when an editor or model is disposed, so capture and store the IDisposable returned by each addExtraLib call (for lynxElementApi and for each entry in lynxTypesMap) and call dispose() on those disposables when tearing down the editor/model (the same cleanup routine that runs for the model/editor disposal around the code at lines ~135-142); ensure you track these disposables (e.g., in a list or map) so recreating the editor will first dispose previous extra-lib disposables to avoid duplicate/stale typings.packages/repl/src/bundler/css-processor.ts (1)
81-93:⚠️ Potential issue | 🟠 MajorPreserve complete
var()fallback expressions.Line 81 only captures one fallback token (
varFunctionValues[2]). Multi-token defaults (e.g.var(--x, 1px solid red)) are truncated, and Lines 87-93 persist the truncated value.Suggested fix
- const varDefaultValueNode = varFunctionValues[2]; + const varDefaultValueTokens = varFunctionValues.slice(2); if (!varName || (firstOperator && firstOperator !== ',')) { throw new Error(`illegal css value ${csstree.generate(n)}`); } - if (varDefaultValueNode) { - const currentDefaultValueText = csstree.generate(varDefaultValueNode); + if (varDefaultValueTokens.length > 0) { + const currentDefaultValueText = varDefaultValueTokens + .map((token) => csstree.generate(token)) + .join('') + .trim(); defaultValueMap[varName] = currentDefaultValueText; item.data = { ...n, type: 'Raw', value: currentDefaultValueText, };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/src/bundler/css-processor.ts` around lines 81 - 93, The code only reads a single token varDefaultValueNode = varFunctionValues[2], which truncates multi-token fallbacks; instead collect all tokens from index 2 onward (e.g. slice varFunctionValues.slice(2)) and generate the complete fallback text with csstree.generate on the combined node/sequence, then set defaultValueMap[varName] and item.data.value to that full generated string so multi-token defaults like "1px solid red" are preserved.
🧹 Nitpick comments (3)
packages/repl/src/components/ui/separator.tsx (1)
14-24: Prevent consumer props from overriding computed accessibility semanticsBecause
{...props}is spread last, a caller can overriderole/aria-orientationand bypass thedecorative+orientationcontract. Prefer spreadingpropsfirst, then setting computed attrs.Suggested diff
<div ref={ref} + {...props} role={decorative ? 'none' : 'separator'} aria-orientation={decorative ? undefined : orientation} className={cn( 'shrink-0 bg-border', orientation === 'horizontal' ? 'h-px w-full' : 'min-h-full w-px', className, )} - {...props} />🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/src/components/ui/separator.tsx` around lines 14 - 24, The spread of consumer props comes last allowing callers to override computed accessibility attributes; change the JSX in the Separator component so {...props} is applied before the computed attributes (role, aria-orientation, className) that rely on decorative and orientation. Locate the element using ref, decorative, orientation, props and cn, and reorder the attributes so role={decorative ? 'none' : 'separator'} and aria-orientation={decorative ? undefined : orientation} (and the className computed with cn) are set after spreading props to ensure callers cannot override those semantics.packages/repl/src/components/ui/select.tsx (1)
29-52: Reduce repeatedposition === 'popper'checks for readability.Optional cleanup: compute once and reuse in both className branches.
♻️ Suggested refactor
const SelectContent = React.forwardRef< React.ComponentRef<typeof SelectPrimitive.Content>, React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content> ->(({ className, children, position = 'popper', ...props }, ref) => ( +>(({ className, children, position = 'popper', ...props }, ref) => { + const isPopper = position === 'popper'; + return ( <SelectPrimitive.Portal> <SelectPrimitive.Content ref={ref} className={cn( @@ - position === 'popper' + isPopper && 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1', className, )} position={position} {...props} @@ className={cn( 'p-1', - position === 'popper' + isPopper && 'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]', )} > {children} </SelectPrimitive.Viewport> </SelectPrimitive.Content> </SelectPrimitive.Portal> -)); + ); +});🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/src/components/ui/select.tsx` around lines 29 - 52, The code repeats the conditional check position === 'popper' when building className for SelectPrimitive.Content and SelectPrimitive.Viewport; to fix, compute a single boolean like isPopper = position === 'popper' at the top of the SelectContent function (before the return) and replace both occurrences with that variable so SelectContent, SelectPrimitive.Content and SelectPrimitive.Viewport use isPopper for their conditional className fragments, preserving the existing class strings and the position prop.packages/repl/src/examples/threading-cross-thread/background.js (1)
13-21: Optional: deduplicate dispatch payload construction.The periodic path works, but both timers build the same event envelope. A tiny helper will keep this example easier to extend.
♻️ Proposed cleanup
+const coreContext = lynx.getCoreContext(); +function dispatchGreeting(message) { + coreContext.dispatchEvent({ + type: 'greeting', + data: { message }, + }); +} + setTimeout(() => { - lynx.getCoreContext().dispatchEvent({ - type: 'greeting', - data: { message: 'Hello from Background Thread!' }, - }); + dispatchGreeting('Hello from Background Thread!'); }, 500); @@ setInterval(() => { count++; - lynx.getCoreContext().dispatchEvent({ - type: 'greeting', - data: { message: 'Update #' + count + ' from background' }, - }); + dispatchGreeting('Update #' + count + ' from background'); }, 2000);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/src/examples/threading-cross-thread/background.js` around lines 13 - 21, The code repeatedly constructs the same event envelope inside the periodic timer; extract that logic into a small helper (e.g., buildGreetingEvent or makeGreetingPayload) and use it when calling lynx.getCoreContext().dispatchEvent inside setInterval (and any other timer) so the envelope construction is deduplicated and easier to extend; update references to count inside the helper signature or have the helper accept the message/count to compose the data object.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@eslint.config.js`:
- Around line 74-80: The ignore block in eslint.config.js currently excludes
core REPL source files so remove or narrow the entries
'packages/repl/src/components/**' and 'packages/repl/src/editor.ts' from the
ignore list so ESLint/TypeScript rules apply to the main implementation; instead
only ignore non-source/config or example assets (e.g., raw example files or
build-only config), ensuring that actual source patterns like
packages/repl/src/**/*.{ts,tsx,js,jsx} remain linted.
In `@packages/repl/src/bundler/css-processor.ts`:
- Around line 145-150: The code forcibly wraps URL values as url('...') which
breaks when the URL contains single quotes; in the node.type === 'Url' branch
(where item.data is set and value is currently `url('${node.value}')`) update
the logic to choose safe quoting: if node.value contains a single quote but not
a double quote, wrap with double quotes (url("...")); if it contains a double
quote but not a single quote, wrap with single quotes; if it contains both,
escape the single quotes (or otherwise escape conflicting characters) and then
wrap (e.g., url('escaped...')); assign that safe string to item.data.value while
keeping type: 'Raw' so generated CSS is valid.
- Around line 168-179: The current code takes parentSelectorList and blindly
prepends the entire list into each nested selector (in the block handling
parent.prelude and subParseChild.prelude), which flattens comma-separated
parents incorrectly; change this to perform a Cartesian expansion: iterate over
each selector in parentSelectorList (from parent.prelude.children) and for each
child selector in (subParseChild.prelude.children) create/prepend a copy of that
single parent selector (not the whole list) to the child's selector.children
(using selector.children.prependList on a single-parent copy) and ensure you
also preserve whitespace via prependData as needed so that .a, .b { .c {} }
becomes .a .c, .b .c rather than merging lists.
In `@packages/repl/src/examples/interactivity-get-element-by-id/main-thread.js`:
- Around line 54-63: The scheduled lookup in the anonymous setTimeout can run
after a new render and produce stale updates; add a cancellable timer so prior
pending lookups are cleared before scheduling a new one. Introduce a
module-scope or enclosing-scope variable (e.g., pendingLookupTimer) and call
clearTimeout(pendingLookupTimer) before assigning pendingLookupTimer =
setTimeout(...), then use that timer id to cancel when appropriate (e.g., at the
start of renderPage or before scheduling). Ensure the callback still references
boxes, resultRaw, __QuerySelector, __SetAttribute and __FlushElementTree as
before but will not run if cleared.
In `@packages/repl/src/examples/ui-tree-tree-mutations/main-thread.js`:
- Around line 36-53: This module schedules three setTimeouts that mutate the
element tree (calls to __InsertElementBefore, __RemoveElement, __ReplaceElement)
and they must be cancelled when the entrypoint is re-run; add a per-invocation
timer registry (e.g., an array of timer IDs) inside renderPage (or the
entrypoint that calls these timeouts), push each setTimeout id into that
registry when created, and at the start of renderPage clear any existing timers
by iterating the registry and calling clearTimeout on each id before resetting
the registry so stale timers cannot fire and mutate stale nodes.
---
Duplicate comments:
In `@packages/repl/README.md`:
- Around line 19-30: The README has fenced code blocks missing language
identifiers (MD040); update both backtick fences around the ASCII table and the
rspeedy/dev snippet to use ```text instead of ``` so the table and the "rspeedy
dev (full pipeline):" snippet are marked as text; ensure you change the opening
fences for the table block and the subsequent pipeline block (the blocks
containing the "Step What happens" table and the "rspeedy dev (full pipeline):"
lines) to ```text so the linter passes.
In `@packages/repl/src/bundler/css-processor.ts`:
- Around line 81-93: The code only reads a single token varDefaultValueNode =
varFunctionValues[2], which truncates multi-token fallbacks; instead collect all
tokens from index 2 onward (e.g. slice varFunctionValues.slice(2)) and generate
the complete fallback text with csstree.generate on the combined node/sequence,
then set defaultValueMap[varName] and item.data.value to that full generated
string so multi-token defaults like "1px solid red" are preserved.
In `@packages/repl/src/editor.ts`:
- Around line 64-77: The extra libs added with
monaco.languages.typescript.javascriptDefaults.addExtraLib (e.g., the call that
registers lynxElementApi and the loop over lynxTypesMap) are global and are not
removed when an editor or model is disposed, so capture and store the
IDisposable returned by each addExtraLib call (for lynxElementApi and for each
entry in lynxTypesMap) and call dispose() on those disposables when tearing down
the editor/model (the same cleanup routine that runs for the model/editor
disposal around the code at lines ~135-142); ensure you track these disposables
(e.g., in a list or map) so recreating the editor will first dispose previous
extra-lib disposables to avoid duplicate/stale typings.
In `@packages/repl/src/examples/lifecycle-events/main-thread.js`:
- Around line 48-50: Change the lifecycle event dispatch to use the canonical,
case-sensitive type "firstScreen" so listeners match; in the snippet calling
__OnLifecycleEvent({ type: 'FirstScreen', timestamp: Date.now() }) replace the
type value with 'firstScreen' and update the accompanying addLine message
(addLine('Dispatched: { type: \'firstScreen\' }')) to keep the example
consistent with the documented canonical type.
In `@packages/repl/src/examples/lifecycle-raf/main-thread.js`:
- Around line 36-65: The RAF loop can be doubled on re-render because request
IDs are never stored or cancelled; add a persistent variable (e.g., frameId)
alongside animate and use lynx.requestAnimationFrame to assign the returned id
to it, then call lynx.cancelAnimationFrame(frameId) before scheduling a new
frame (and on cleanup/unmount) so old loops are cancelled; update calls around
the animate function and the initial lynx.requestAnimationFrame(animate) to use
and manage this frameId.
In `@packages/repl/src/examples/styling-inline-styles/main-thread.js`:
- Around line 42-52: renderPage currently schedules a new setTimeout on every
call which can leave old timers mutating outdated nodes; introduce a
module-scoped timer variable (e.g., pendingStyleTimer) and call
clearTimeout(pendingStyleTimer) before creating a new setTimeout, then assign
the returned id to pendingStyleTimer so that __SetInlineStyles and
__FlushElementTree are only run from the latest scheduled timer and previous
timers are canceled.
---
Nitpick comments:
In `@packages/repl/src/components/ui/select.tsx`:
- Around line 29-52: The code repeats the conditional check position ===
'popper' when building className for SelectPrimitive.Content and
SelectPrimitive.Viewport; to fix, compute a single boolean like isPopper =
position === 'popper' at the top of the SelectContent function (before the
return) and replace both occurrences with that variable so SelectContent,
SelectPrimitive.Content and SelectPrimitive.Viewport use isPopper for their
conditional className fragments, preserving the existing class strings and the
position prop.
In `@packages/repl/src/components/ui/separator.tsx`:
- Around line 14-24: The spread of consumer props comes last allowing callers to
override computed accessibility attributes; change the JSX in the Separator
component so {...props} is applied before the computed attributes (role,
aria-orientation, className) that rely on decorative and orientation. Locate the
element using ref, decorative, orientation, props and cn, and reorder the
attributes so role={decorative ? 'none' : 'separator'} and
aria-orientation={decorative ? undefined : orientation} (and the className
computed with cn) are set after spreading props to ensure callers cannot
override those semantics.
In `@packages/repl/src/examples/threading-cross-thread/background.js`:
- Around line 13-21: The code repeatedly constructs the same event envelope
inside the periodic timer; extract that logic into a small helper (e.g.,
buildGreetingEvent or makeGreetingPayload) and use it when calling
lynx.getCoreContext().dispatchEvent inside setInterval (and any other timer) so
the envelope construction is deduplicated and easier to extend; update
references to count inside the helper signature or have the helper accept the
message/count to compose the data object.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (35)
.github/workflows/nodejs-dependencies.ymleslint.config.jspackage.jsonpackages/repl/README.mdpackages/repl/package.jsonpackages/repl/scripts/collect-lynx-types.mjspackages/repl/src/bundler/css-processor.tspackages/repl/src/components/ui/button.tsxpackages/repl/src/components/ui/select.tsxpackages/repl/src/components/ui/separator.tsxpackages/repl/src/editor.tspackages/repl/src/examples/attributes-dataset/main-thread.jspackages/repl/src/examples/attributes-set-and-get/main-thread.jspackages/repl/src/examples/demo-background-counter/background.jspackages/repl/src/examples/demo-background-counter/main-thread.jspackages/repl/src/examples/demo-data-driven-list/background.jspackages/repl/src/examples/demo-data-driven-list/main-thread.jspackages/repl/src/examples/global-console/background.jspackages/repl/src/examples/global-console/main-thread.jspackages/repl/src/examples/global-system-info/main-thread.jspackages/repl/src/examples/interactivity-event-handling/main-thread.jspackages/repl/src/examples/interactivity-get-element-by-id/main-thread.jspackages/repl/src/examples/interactivity-selector-query/main-thread.jspackages/repl/src/examples/lifecycle-events/main-thread.jspackages/repl/src/examples/lifecycle-raf/main-thread.jspackages/repl/src/examples/list-virtualization/main-thread.jspackages/repl/src/examples/styling-css-classes-and-scoping/main-thread.jspackages/repl/src/examples/styling-inline-styles/main-thread.jspackages/repl/src/examples/threading-cross-thread/background.jspackages/repl/src/examples/threading-cross-thread/main-thread.jspackages/repl/src/examples/ui-tree-creating-elements/main-thread.jspackages/repl/src/examples/ui-tree-tree-mutations/main-thread.jspackages/repl/src/examples/ui-tree-tree-traversal/main-thread.jspackages/repl/tsconfig.jsonpnpm-workspace.yaml
🚧 Files skipped from review as they are similar to previous changes (18)
- packages/repl/src/examples/threading-cross-thread/main-thread.js
- packages/repl/src/examples/demo-background-counter/main-thread.js
- packages/repl/src/examples/list-virtualization/main-thread.js
- packages/repl/src/examples/ui-tree-creating-elements/main-thread.js
- packages/repl/src/examples/interactivity-selector-query/main-thread.js
- packages/repl/src/components/ui/button.tsx
- packages/repl/src/examples/ui-tree-tree-traversal/main-thread.js
- packages/repl/scripts/collect-lynx-types.mjs
- packages/repl/tsconfig.json
- packages/repl/package.json
- packages/repl/src/examples/demo-data-driven-list/background.js
- packages/repl/src/examples/demo-data-driven-list/main-thread.js
- packages/repl/src/examples/global-system-info/main-thread.js
- packages/repl/src/examples/global-console/background.js
- packages/repl/src/examples/demo-background-counter/background.js
- packages/repl/src/examples/attributes-set-and-get/main-thread.js
- pnpm-workspace.yaml
- packages/repl/src/examples/global-console/main-thread.js
| // REPL examples use Lynx platform globals and are not subject to lint rules | ||
| 'packages/repl/src/examples/**', | ||
| // REPL components use Vite path aliases and ?raw imports not handled by root tsconfig | ||
| 'packages/repl/src/components/**', | ||
| 'packages/repl/src/editor.ts', | ||
| // REPL build config is not part of the TS project | ||
| 'packages/repl/rsbuild.config.ts', |
There was a problem hiding this comment.
Do not disable lint for core REPL source files.
Line 77 and Line 78 exclude packages/repl/src/components/** and packages/repl/src/editor.ts, which removes lint/type-rule coverage from the main implementation surface. Keep ignores limited to non-source assets/config-only files.
♻️ Suggested narrowing
// REPL examples use Lynx platform globals and are not subject to lint rules
'packages/repl/src/examples/**',
- // REPL components use Vite path aliases and ?raw imports not handled by root tsconfig
- 'packages/repl/src/components/**',
- 'packages/repl/src/editor.ts',
// REPL build config is not part of the TS project
'packages/repl/rsbuild.config.ts',As per coding guidelines: **/*.{ts,tsx,js,jsx}: Follow ESLint rules as configured in eslint.config.js, including React and TypeScript-specific rules.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@eslint.config.js` around lines 74 - 80, The ignore block in eslint.config.js
currently excludes core REPL source files so remove or narrow the entries
'packages/repl/src/components/**' and 'packages/repl/src/editor.ts' from the
ignore list so ESLint/TypeScript rules apply to the main implementation; instead
only ignore non-source/config or example assets (e.g., raw example files or
build-only config), ensuring that actual source patterns like
packages/repl/src/**/*.{ts,tsx,js,jsx} remain linted.
| if (node.type === 'Url') { | ||
| item.data = { | ||
| ...node, | ||
| type: 'Raw', | ||
| value: `url('${node.value}')`, | ||
| }; |
There was a problem hiding this comment.
Do not force single-quoted url() serialization.
Line 149 rewrites every URL as url('...'). If the URL itself contains ' (common in data URIs/SVG payloads), the generated CSS becomes invalid.
Suggested fix
if (node.type === 'Url') {
item.data = {
...node,
type: 'Raw',
- value: `url('${node.value}')`,
+ value: csstree.generate(node),
};
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (node.type === 'Url') { | |
| item.data = { | |
| ...node, | |
| type: 'Raw', | |
| value: `url('${node.value}')`, | |
| }; | |
| if (node.type === 'Url') { | |
| item.data = { | |
| ...node, | |
| type: 'Raw', | |
| value: csstree.generate(node), | |
| }; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/repl/src/bundler/css-processor.ts` around lines 145 - 150, The code
forcibly wraps URL values as url('...') which breaks when the URL contains
single quotes; in the node.type === 'Url' branch (where item.data is set and
value is currently `url('${node.value}')`) update the logic to choose safe
quoting: if node.value contains a single quote but not a double quote, wrap with
double quotes (url("...")); if it contains a double quote but not a single
quote, wrap with single quotes; if it contains both, escape the single quotes
(or otherwise escape conflicting characters) and then wrap (e.g.,
url('escaped...')); assign that safe string to item.data.value while keeping
type: 'Raw' so generated CSS is valid.
| const parentSelectorList = parent.prelude | ||
| .children as csstree.List<csstree.Selector>; | ||
| (subParseChild.prelude.children as csstree.List< | ||
| csstree.Selector | ||
| >).forEach((selector) => { | ||
| selector.children.prependData({ | ||
| ...selector, | ||
| type: 'WhiteSpace', | ||
| value: ' ', | ||
| }); | ||
| selector.children.prependList(parentSelectorList.copy()); | ||
| }); |
There was a problem hiding this comment.
Fix nested selector flattening for comma-separated parent selectors.
Lines 168-179 prepend the full parent selector list into each child selector node. This does not produce a proper Cartesian expansion; selectors like .a, .b { .c {} } are flattened incorrectly.
Suggested fix
- const parentSelectorList = parent.prelude
- .children as csstree.List<csstree.Selector>;
- (subParseChild.prelude.children as csstree.List<
- csstree.Selector
- >).forEach((selector) => {
- selector.children.prependData({
- ...selector,
- type: 'WhiteSpace',
- value: ' ',
- });
- selector.children.prependList(parentSelectorList.copy());
- });
+ const parentSelectors = (parent.prelude.children as csstree.List<csstree.Selector>)
+ .toArray()
+ .map((s) => csstree.generate(s));
+ const childSelectors = (subParseChild.prelude.children as csstree.List<csstree.Selector>)
+ .toArray()
+ .map((s) => csstree.generate(s));
+
+ const mergedSelectorText = parentSelectors
+ .flatMap((p) => childSelectors.map((c) => `${p} ${c}`))
+ .join(', ');
+
+ const mergedAst = csstree.parse(
+ `${mergedSelectorText}{--mocked-declaration:1;}`,
+ ) as csstree.StyleSheet;
+ subParseChild.prelude = (mergedAst.children.first as csstree.Rule).prelude;
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/repl/src/bundler/css-processor.ts` around lines 168 - 179, The
current code takes parentSelectorList and blindly prepends the entire list into
each nested selector (in the block handling parent.prelude and
subParseChild.prelude), which flattens comma-separated parents incorrectly;
change this to perform a Cartesian expansion: iterate over each selector in
parentSelectorList (from parent.prelude.children) and for each child selector in
(subParseChild.prelude.children) create/prepend a copy of that single parent
selector (not the whole list) to the child's selector.children (using
selector.children.prependList on a single-parent copy) and ensure you also
preserve whitespace via prependData as needed so that .a, .b { .c {} } becomes
.a .c, .b .c rather than merging lists.
| setTimeout(() => { | ||
| const results = []; | ||
| for (const { id } of boxes) { | ||
| // __QuerySelector with "#id" — equivalent to getElementById | ||
| const el = __QuerySelector(page, '#' + id); | ||
| results.push(id + ': ' + (el ? 'found' : 'not found')); | ||
| } | ||
| __SetAttribute(resultRaw, 'text', results.join('\n')); | ||
| __FlushElementTree(); | ||
| }, 100); |
There was a problem hiding this comment.
Cancel pending lookup timer to avoid stale updates on rapid re-renders.
Line 54 schedules async work but doesn’t cancel prior timers. If renderPage() is triggered again within 100ms, an older callback can still run and update stale nodes.
Proposed fix
+let pendingLookupTimer;
+
globalThis.renderPage = function renderPage() {
+ if (pendingLookupTimer) {
+ clearTimeout(pendingLookupTimer);
+ pendingLookupTimer = undefined;
+ }
+
const page = __CreatePage('page', 0);
const container = __CreateView(0);
__AppendElement(page, container);
@@
- setTimeout(() => {
+ pendingLookupTimer = setTimeout(() => {
const results = [];
for (const { id } of boxes) {
// __QuerySelector with "#id" — equivalent to getElementById
const el = __QuerySelector(page, '#' + id);
results.push(id + ': ' + (el ? 'found' : 'not found'));
}
__SetAttribute(resultRaw, 'text', results.join('\n'));
__FlushElementTree();
+ pendingLookupTimer = undefined;
}, 100);
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| setTimeout(() => { | |
| const results = []; | |
| for (const { id } of boxes) { | |
| // __QuerySelector with "#id" — equivalent to getElementById | |
| const el = __QuerySelector(page, '#' + id); | |
| results.push(id + ': ' + (el ? 'found' : 'not found')); | |
| } | |
| __SetAttribute(resultRaw, 'text', results.join('\n')); | |
| __FlushElementTree(); | |
| }, 100); | |
| let pendingLookupTimer; | |
| globalThis.renderPage = function renderPage() { | |
| if (pendingLookupTimer) { | |
| clearTimeout(pendingLookupTimer); | |
| pendingLookupTimer = undefined; | |
| } | |
| const page = __CreatePage('page', 0); | |
| const container = __CreateView(0); | |
| __AppendElement(page, container); | |
| // ... other code ... | |
| pendingLookupTimer = setTimeout(() => { | |
| const results = []; | |
| for (const { id } of boxes) { | |
| // __QuerySelector with "#id" — equivalent to getElementById | |
| const el = __QuerySelector(page, '#' + id); | |
| results.push(id + ': ' + (el ? 'found' : 'not found')); | |
| } | |
| __SetAttribute(resultRaw, 'text', results.join('\n')); | |
| __FlushElementTree(); | |
| pendingLookupTimer = undefined; | |
| }, 100); | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/repl/src/examples/interactivity-get-element-by-id/main-thread.js`
around lines 54 - 63, The scheduled lookup in the anonymous setTimeout can run
after a new render and produce stale updates; add a cancellable timer so prior
pending lookups are cleared before scheduling a new one. Introduce a
module-scope or enclosing-scope variable (e.g., pendingLookupTimer) and call
clearTimeout(pendingLookupTimer) before assigning pendingLookupTimer =
setTimeout(...), then use that timer id to cancel when appropriate (e.g., at the
start of renderPage or before scheduling). Ensure the callback still references
boxes, resultRaw, __QuerySelector, __SetAttribute and __FlushElementTree as
before but will not run if cleared.
| setTimeout(() => { | ||
| const inserted = createItem('Inserted before B', '#10b981'); | ||
| __InsertElementBefore(container, inserted, itemB); | ||
| __FlushElementTree(); | ||
| }, 1500); | ||
|
|
||
| // __RemoveElement — remove a child | ||
| setTimeout(() => { | ||
| __RemoveElement(container, itemA); | ||
| __FlushElementTree(); | ||
| }, 3000); | ||
|
|
||
| // __ReplaceElement — replace one element with another | ||
| setTimeout(() => { | ||
| const replacement = createItem('C - Replaced', '#f59e0b'); | ||
| __ReplaceElement(replacement, itemC); | ||
| __FlushElementTree(); | ||
| }, 4500); |
There was a problem hiding this comment.
Cancel pending timers before re-running renderPage.
If this entrypoint is invoked multiple times in the REPL, old timers can still fire and mutate stale nodes, causing nondeterministic behavior/errors. Guard timer lifecycle per invocation.
💡 Suggested fix
+globalThis.__uiTreeMutationsTimers?.forEach(clearTimeout);
+
globalThis.renderPage = function renderPage() {
+ const timerIds = [];
+ globalThis.__uiTreeMutationsTimers = timerIds;
+ const schedule = (fn, delay) => {
+ const id = setTimeout(fn, delay);
+ timerIds.push(id);
+ return id;
+ };
+
const page = __CreatePage('page', 0);
const container = __CreateView(0);
__AppendElement(page, container);
__SetInlineStyles(container, 'padding:40px; align-items:center;');
@@
- setTimeout(() => {
+ schedule(() => {
const inserted = createItem('Inserted before B', '#10b981');
__InsertElementBefore(container, inserted, itemB);
__FlushElementTree();
}, 1500);
@@
- setTimeout(() => {
+ schedule(() => {
__RemoveElement(container, itemA);
__FlushElementTree();
}, 3000);
@@
- setTimeout(() => {
+ schedule(() => {
const replacement = createItem('C - Replaced', '#f59e0b');
__ReplaceElement(replacement, itemC);
__FlushElementTree();
}, 4500);
};🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/repl/src/examples/ui-tree-tree-mutations/main-thread.js` around
lines 36 - 53, This module schedules three setTimeouts that mutate the element
tree (calls to __InsertElementBefore, __RemoveElement, __ReplaceElement) and
they must be cancelled when the entrypoint is re-run; add a per-invocation timer
registry (e.g., an array of timer IDs) inside renderPage (or the entrypoint that
calls these timeouts), push each setTimeout id into that registry when created,
and at the start of renderPage clear any existing timers by iterating the
registry and calling clearTimeout on each id before resetting the registry so
stale timers cannot fire and mutate stale nodes.
The root .gitignore has a `lib` pattern which excludes packages/repl/src/lib/utils.ts from git tracking, causing CI build failures. Move to src/utils/cn.ts instead.
Merging this PR will degrade performance by 5.45%
Performance Changes
Comparing Footnotes
|
Web Explorer#7939 Bundle Size — 383.53KiB (0%).7a36d50(current) vs 6cbe5d8 main#7929(baseline) Bundle metrics
Bundle size by type
|
| Current #7939 |
Baseline #7929 |
|
|---|---|---|
252.58KiB |
252.58KiB |
|
95.85KiB |
95.85KiB |
|
35.1KiB |
35.1KiB |
Bundle analysis report Branch Huxpro:Huxpro/repl Project dashboard
Generated by RelativeCI Documentation Report issue
- Add Apache 2.0 license headers to all source files - Fix import ordering and sort-imports issues - Change type aliases to interfaces where required - Add eslint-disable for browser APIs (BroadcastChannel, localStorage) falsely flagged as unsupported Node.js features - Fix missing .js file extension in import
There was a problem hiding this comment.
♻️ Duplicate comments (2)
packages/repl/src/url-state.ts (2)
36-42:⚠️ Potential issue | 🟠 MajorValidate optional decoded fields at runtime (Line 36–42).
parsed.backgroundandparsed.cssare accepted with?? '', so non-string values can leak through as invalidCodeStateat runtime.Proposed fix
- const parsed = JSON.parse(json) as CodeState; + const parsed = JSON.parse(json) as Partial<CodeState>; if (typeof parsed.mainThread === 'string') { return { mainThread: parsed.mainThread, - background: parsed.background ?? '', - css: parsed.css ?? '', + background: typeof parsed.background === 'string' ? parsed.background : '', + css: typeof parsed.css === 'string' ? parsed.css : '', }; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/src/url-state.ts` around lines 36 - 42, parsed is created from JSON.parse as a CodeState but background and css are currently accepted via nullish coalescing and may be non-strings; update the runtime validation so that when returning the object (the branch where typeof parsed.mainThread === 'string') you only accept parsed.background and parsed.css when they are strings (e.g., background: typeof parsed.background === 'string' ? parsed.background : '' and similar for css) so the returned object always conforms to CodeState; keep the existing mainThread string check and ensure the return uses these validated/coerced values.
55-56:⚠️ Potential issue | 🟠 MajorUse
location.hashconsistently for shared state (Line 55–56, 77–93).State is currently read/written via query params, but this REPL feature is intended to persist via URL hash (
#...) for share links.Proposed fix
export function getInitialState(): InitialState { - const params = new URLSearchParams(window.location.search); + const params = new URLSearchParams( + window.location.hash.startsWith('#') + ? window.location.hash.slice(1) + : window.location.hash, + ); @@ export function saveToUrl(code: CodeState): void { const encoded = encodeCode(code); const url = new URL(window.location.href); - url.searchParams.delete('s'); - url.searchParams.set('c', encoded); + const hashParams = new URLSearchParams( + url.hash.startsWith('#') ? url.hash.slice(1) : '', + ); + hashParams.delete('s'); + hashParams.set('c', encoded); + url.hash = hashParams.toString(); + url.searchParams.delete('c'); + url.searchParams.delete('s'); window.history.replaceState({}, '', url); } @@ export function saveSampleToUrl(sampleIndex: number): void { const url = new URL(window.location.href); - url.searchParams.delete('c'); - url.searchParams.set('s', String(sampleIndex)); + const hashParams = new URLSearchParams( + url.hash.startsWith('#') ? url.hash.slice(1) : '', + ); + hashParams.delete('c'); + hashParams.set('s', String(sampleIndex)); + url.hash = hashParams.toString(); + url.searchParams.delete('c'); + url.searchParams.delete('s'); window.history.replaceState({}, '', url); }Also applies to: 77-93
🧹 Nitpick comments (1)
packages/repl/src/console/types.ts (1)
8-21: Consider removing duplicated shape betweenConsoleEntryandConsoleMessage.No functional issue, but
ConsoleEntrycan extendConsoleMessageto keep these types in sync more easily.♻️ Proposed refactor
-export interface ConsoleEntry { - id: number; - level: ConsoleLevel; - source: ConsoleSource; - args: string[]; - timestamp: number; -} - export interface ConsoleMessage { level: ConsoleLevel; source: ConsoleSource; args: string[]; timestamp: number; } + +export interface ConsoleEntry extends ConsoleMessage { + id: number; +}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/repl/src/console/types.ts` around lines 8 - 21, ConsoleEntry duplicates the shape of ConsoleMessage; refactor by having ConsoleEntry extend ConsoleMessage and only add the unique id field so the two types stay in sync. Update the declaration so ConsoleEntry extends ConsoleMessage and remove duplicated properties (level, source, args, timestamp) from ConsoleEntry, leaving only id as the additional property.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@packages/repl/src/url-state.ts`:
- Around line 36-42: parsed is created from JSON.parse as a CodeState but
background and css are currently accepted via nullish coalescing and may be
non-strings; update the runtime validation so that when returning the object
(the branch where typeof parsed.mainThread === 'string') you only accept
parsed.background and parsed.css when they are strings (e.g., background: typeof
parsed.background === 'string' ? parsed.background : '' and similar for css) so
the returned object always conforms to CodeState; keep the existing mainThread
string check and ensure the return uses these validated/coerced values.
---
Nitpick comments:
In `@packages/repl/src/console/types.ts`:
- Around line 8-21: ConsoleEntry duplicates the shape of ConsoleMessage;
refactor by having ConsoleEntry extend ConsoleMessage and only add the unique id
field so the two types stay in sync. Update the declaration so ConsoleEntry
extends ConsoleMessage and remove duplicated properties (level, source, args,
timestamp) from ConsoleEntry, leaving only id as the additional property.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
packages/repl/postcss.config.mjspackages/repl/src/console/types.tspackages/repl/src/console/useConsole.tspackages/repl/src/env.d.tspackages/repl/src/index.tsxpackages/repl/src/local-storage.tspackages/repl/src/url-state.tspackages/repl/src/utils/cn.ts
🚧 Files skipped from review as they are similar to previous changes (4)
- packages/repl/src/index.tsx
- packages/repl/src/console/useConsole.ts
- packages/repl/src/env.d.ts
- packages/repl/src/local-storage.ts
Summary
This PR introduces a new
@lynx-js/replpackage: an interactive in-browser playground for Lynx, built with Rsbuild + React + Monaco Editor. It lets developers edit Lynx code (main-thread and background scripts) and see a live preview side-by-side.@lynx-js/typestype hints and autocomplete@lynx-js/web-platformrunning in an iframeCI deployment
The repl is now built and deployed to GitHub Pages on every push to
main, served at<pages-url>/repl/. This is done by extending the existingworkflow-website.yml:ASSET_PREFIXset to the correct Pages sub-pathpackages/repl/distintowebsite/doc_build/replTest plan
<pages-url>/repl/after mergingSummary by CodeRabbit
New Features
Documentation
Chores