Skip to content

bugfix dep#56

Closed
HoikanChan wants to merge 5 commits intomasterfrom
bugfix-dep
Closed

bugfix dep#56
HoikanChan wants to merge 5 commits intomasterfrom
bugfix-dep

Conversation

@HoikanChan
Copy link
Contributor

@HoikanChan HoikanChan commented Aug 14, 2025

  • fix(next): array item didn't update
  • fix(next): jsx slice can't render in for
  • fix(next): 修复map场景使用optionalCallExpr
  • chore: fix dependency to caret range

Summary by CodeRabbit

  • New Features

    • Support optional chaining in map-to-for transformations for safer rendering when data may be undefined.
    • Allow passing a component factory (function) to render and JSX slices, enabling lazy component creation.
  • Bug Fixes

    • More resilient list rendering with improved handling of empty/non-array data and optional keys.
    • Smoother suspense fallback handling.
  • Tests

    • Expanded e2e coverage for for-loops, JSX reuse, optional chaining, and nested structures.
  • Chores

    • Updated workspace dependency ranges for improved version management.

@changeset-bot
Copy link

changeset-bot bot commented Aug 14, 2025

⚠️ No Changeset found

Latest commit: a4dbed5

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link

coderabbitai bot commented Aug 14, 2025

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

This PR updates multiple compiler/runtime components to support thunk-wrapped component nodes and optional-chained map transformations. It adjusts render/for/expression runtime handling, updates generators and Babel plugins, and broadens tests. Several package.json files switch workspace dependencies from workspace:* to workspace:^.

Changes

Cohort / File(s) Summary
Workspace version spec updates
next-packages/**/package.json (compiler/babel-inula-next-core, compiler/error-handler, compiler/jsx-parser, compiler/reactivity-parser, compiler/unplugin-inula-next, compiler/view-generator, inula-bridge, runtime)
Change workspace dependency specifiers from "workspace:*" to "workspace:^" across multiple packages.
JSX slice thunking
next-packages/compiler/babel-inula-next-core/src/sugarPlugins/jsxSlicePlugin.ts, .../test/sugarPlugins/jsxSlice.test.ts, .../test/e2e/subComponent.test.tsx
Replace direct createCompNode calls with zero-arg thunks; align tests and snapshots to expect thunks and adjusted ExpNode deps/priority.
Map → For with optional chaining
.../src/sugarPlugins/mapping2ForPlugin.ts, .../test/e2e/mappingToFor.test.ts, .../test/sugarPlugins/mappingToFor.test.tsx, .../test/generator/view.test.tsx
Add handling for OptionalCallExpression/OptionalMemberExpression; update visitors; extend tests to cover optional-chained map and snapshot changes.
ForGenerator key mapping update
next-packages/compiler/view-generator/src/NodeGenerators/ForGenerator.ts
keyMappingFn now accepts nullable key; generate optional-chained map (array.value?.map?.(...)) or null literal when key is null.
Suspense fallback input change
next-packages/compiler/view-generator/src/NodeGenerators/SuspenseGenerator.ts
Pass fallback expression directly to fallback() instead of wrapping in an arrow function.
Runtime: expression/for/render
next-packages/runtime/src/Nodes/MutableNodes/expression.ts, .../MutableNodes/for.ts, .../render.ts
ExpNode executes any function-valued node; ForNode initializes data/keys with guards and refines update logic; render accepts CompNode or thunk and invokes if function.
Runtime tests
next-packages/runtime/test/e2e/for.test.tsx, .../test/e2e/jsx-rendering.test.tsx
Add extensive for-loop and JSX reuse tests, including optional chaining, nested loops, and mutation scenarios.

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant Runtime as runtime.render
  participant DOM as Container

  Caller->>Runtime: render(compNode | () => compNode, container)
  alt compNode is function
    Runtime->>Runtime: compNode = compNode()
  end
  Runtime->>DOM: validate/clear/insertNode(compNode)
  Runtime-->>Caller: runDidMount()
Loading
sequenceDiagram
  participant Src as Source Code
  participant Babel as mapping2ForPlugin
  participant Gen as ForGenerator
  participant RT as ForNode (runtime)

  Src->>Babel: array?.map?(...) in JSX
  Babel->>Gen: transform to <for each=... map=...>
  Gen->>Gen: keyMappingFn -> value?.map?.(...)
  Gen-->>Src: emits createForNode(...)
  Src->>RT: execute at runtime
  RT-->>Src: iterates with optional-chained mapping
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • bugfix next 814 #54: Mirrors the same files and control-flow changes (jsxSlice, mapping2For, ForGenerator, runtime render/for/expression), indicating a parallel or upstream/downstream change.
  • feat/next #20: Earlier work on render/CompNode handling and map-to-for transformation; this PR extends those areas with thunks and optional calls.

Poem

A rabbit taps keys with a delicate thunk,
Slices wrapped softly, no longer kerplunk.
Maps tiptoe optional, for-loops take flight,
Render now checks if a function’s in sight.
Arrays hop and update, keys follow through—
Carrots compiled, the garden runs true. 🥕✨

✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch bugfix-dep

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🔭 Outside diff range comments (5)
next-packages/runtime/src/Nodes/MutableNodes/for.ts (5)

401-404: Bug: updateItemFunc invoked with wrong arguments.

You're passing (dirtyBits, item) but the signature is (item: Value, idx: number). This prevents per-item updates from receiving the correct item and index.

-        newUpdateArr[prevIdx]?.(this.owner.dirtyBits, newData[idx]);
+        newUpdateArr[prevIdx]?.(newData[idx], idx);

256-261: Stale cached nodes risk: mark nodes as dirty when deleting.

removeNodesInLifeCycle deletes from nodesMap but doesn’t flip nodesDirty. Any consumer of this.nodes may see stale cachedNodes.

Outside this hunk, update removeNodesInLifeCycle to set nodesDirty:

removeNodesInLifeCycle(nodes: InulaBaseNode[], key: Value) {
  this.runWillUnmount(key);
  super.removeNodes(nodes);
  this.runDidUnmount(key);
  this.nodesMap.delete(key);
  this.nodesDirty = true; // ensure nodes getter recomputes
}

49-57: Guard against missing node groups in nodes getter.

If keys/data fall out of sync, spreading undefined throws. Default to [] to be safe.

Outside this hunk, update nodes getter:

get nodes() {
  if (!this.nodesDirty) return this.cachedNodes;
  const nodes: InulaBaseNode[] = [];
  for (let idx = 0; idx < this.data.length; idx++) {
    const key = this.keys?.[idx] ?? idx;
    const group = this.nodesMap.get(key) ?? [];
    nodes.push(...group);
  }
  this.cachedNodes = nodes;
  this.nodesDirty = false;
  return nodes;
}

92-99: Guard iteration in updateItem when group is missing.

Iterating over undefined will throw.

Outside this hunk, update updateItem:

updateItem(idx: number, data: Value[]) {
  this.updateItemFuncArr[idx]?.(data[idx], idx);
  const key = this.keys?.[idx] ?? idx;
  const group = this.nodesMap.get(key) ?? [];
  for (const node of group) {
    update(node);
  }
}

267-312: Handle non-array data in updateWithOutKey to support optional chaining transforms.

Coerce non-arrays to [] to match arr?.map(...) semantics.

Outside this hunk, update updateWithOutKey:

updateWithOutKey() {
  const rawData = this.dataFunc();
  const newData = Array.isArray(rawData) ? rawData : [];
  const preLength = this.data.length;
  const currLength = newData.length;
  // ...rest of function unchanged
}
🧹 Nitpick comments (10)
next-packages/runtime/src/Nodes/MutableNodes/expression.ts (1)

66-68: Unconditionally invoking function-valued children: consider guarding against async thunks and unexpected return types

Calling thunks here is aligned with the new render contract. However, if a function returns a Promise (async thunk) or another unexpected value, it will flow into the node list and likely break later during mount. Suggest guarding against Promises or surfacing a clear error in dev.

Apply this diff to add a defensive check:

-        if (typeof node === 'function') {
-          return node();
-        }
+        if (typeof node === 'function') {
+          const out = node();
+          // Guard against accidental async thunks that return a Promise
+          if (out && typeof (out as any).then === 'function') {
+            throw new Error(
+              'Async thunks in children are not supported. Return a CompNode or an array of CompNodes, not a Promise.'
+            );
+          }
+          return out;
+        }

If you anticipate nested thunks or deeper arrays, we can also refactor to recursively unwrap and normalize nodes to avoid partial flattening. Happy to draft that if desired.

next-packages/runtime/test/e2e/jsx-rendering.test.tsx (1)

43-44: Rename test for clarity and grammar

Minor naming nit: “multi times” → “multiple times” improves readability.

-it('should render JSX variable multi times', ({ container }) => {
+it('should render the same JSX variable multiple times', ({ container }) => {
next-packages/compiler/babel-inula-next-core/src/sugarPlugins/jsxSlicePlugin.ts (1)

90-95: Name the wrapper as a “thunk” for clarity and reuse it consistently

The new wrapper correctly defers createCompNode(sliceId). A small readability nit: naming it sliceThunk communicates intent better and reads cleaner in both branches.

Apply this diff:

-  const jsxSliceAlternative = 
+  const sliceThunk = 
     t.arrowFunctionExpression(
       [],
       t.callExpression(t.identifier(importMap.createCompNode), [sliceId])
     );
 
 if (path.parentPath.isArrowFunctionExpression()) {
   // special case: returned by arrow function
   const block = t.blockStatement([
     sliceComp,
-    t.returnStatement(jsxSliceAlternative),
+    t.returnStatement(sliceThunk),
   ]);
   path.replaceWith(block);
 } else {
   // insert into the previous statement
   const stmt = path.getStatementParent();
   if (!stmt) {
     throw new Error('Cannot find the statement parent');
   }
   stmt.insertBefore(sliceComp);
-  path.replaceWith(jsxSliceAlternative);
+  path.replaceWith(sliceThunk);
 }

Also applies to: 100-101, 110-111

next-packages/runtime/test/e2e/for.test.tsx (2)

62-72: Prefer const for non-reassigned variables

extra isn’t reassigned.

Apply this diff:

-      let extra = 'extra';
+      const extra = 'extra';

359-403: Fix inline snapshot escaping to satisfy no-useless-escape

The inline snapshot uses unnecessary \" escapes. Use a template literal to avoid escaping double quotes.

Apply this diff:

-    expect(container.innerHTML).toMatchInlineSnapshot(
-      '"<div style=\"margin-left: 0px;\"><div>Comment: Root comment</div><div style=\"margin-left: 20px;\"><div>Comment: First reply</div></div><div style=\"margin-left: 20px;\"><div>Comment: Second reply</div></div></div>"'
-    );
+    expect(container.innerHTML).toMatchInlineSnapshot(
+      `"<div style="margin-left: 0px;"><div>Comment: Root comment</div><div style="margin-left: 20px;"><div>Comment: First reply</div></div><div style="margin-left: 20px;"><div>Comment: Second reply</div></div></div>"`
+    );
next-packages/compiler/babel-inula-next-core/test/sugarPlugins/jsxSlice.test.ts (1)

85-97: Array elements as thunks are fine; consider a sparse-array case.

Minor: add a test for sparse arrays or arrays containing non-JSX items to ensure the plugin doesn't over-transform mixed content.

Would you like me to add a test variant like: const arr = [

, null, 0, ] and assert only the JSX entries are wrapped?

next-packages/runtime/src/Nodes/MutableNodes/for.ts (2)

75-75: Nit: spacing around assignment.

-    const data= dataFunc();
+    const data = dataFunc();

107-112: Minor: avoid optional field for boolean sentinel.

notInitialized? = true is odd; make it a plain boolean to reflect intent.

Outside this hunk:

-  notInitialized? = true;
+  notInitialized = true;
next-packages/compiler/babel-inula-next-core/test/sugarPlugins/mappingToFor.test.tsx (1)

109-133: Optional-chaining map → for transform looks correct; add a safety test.

The transform drops optional chaining on the data source (). This is fine if ForNode treats non-array values as [] during updates. Given the runtime change in ForNode’s constructor only, please add a test ensuring comment.replies toggling between undefined and an array doesn’t crash and renders nothing for undefined.

I can add a follow-up plugin test for comment.replies?.map?.(…) and an e2e runtime test that mutates replies from undefined → [] → [x] and back, to validate stability. Want me to draft those?

next-packages/compiler/babel-inula-next-core/src/sugarPlugins/mapping2ForPlugin.ts (1)

71-81: Callee extraction is solid; consider computed '["map"]' support and narrowing types

Handling both MemberExpression and OptionalMemberExpression here is correct. Two small enhancements to consider:

  • Support computed property access where users write arr?.['map']?.(cb). Today, the plugin silently skips this valid case because property isn’t an Identifier.
  • Narrow NodePath types to MemberExpression | OptionalMemberExpression after the type guard to improve maintainability.

Example adjustment for property checks (pseudo):

const isMapProp =
  map.isIdentifier({ name: 'map' }) ||
  (callee.node.computed === true && map.isStringLiteral({ value: 'map' }));

if (!isMapProp) return;

This would allow both arr?.map?.(cb) and arr?.['map']?.(cb).

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these settings in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5732365 and c994f58.

📒 Files selected for processing (22)
  • next-packages/compiler/babel-inula-next-core/package.json (1 hunks)
  • next-packages/compiler/babel-inula-next-core/src/sugarPlugins/jsxSlicePlugin.ts (2 hunks)
  • next-packages/compiler/babel-inula-next-core/src/sugarPlugins/mapping2ForPlugin.ts (4 hunks)
  • next-packages/compiler/babel-inula-next-core/test/e2e/mappingToFor.test.ts (1 hunks)
  • next-packages/compiler/babel-inula-next-core/test/e2e/subComponent.test.tsx (1 hunks)
  • next-packages/compiler/babel-inula-next-core/test/generator/view.test.tsx (1 hunks)
  • next-packages/compiler/babel-inula-next-core/test/sugarPlugins/jsxSlice.test.ts (2 hunks)
  • next-packages/compiler/babel-inula-next-core/test/sugarPlugins/mappingToFor.test.tsx (1 hunks)
  • next-packages/compiler/error-handler/package.json (1 hunks)
  • next-packages/compiler/jsx-parser/package.json (1 hunks)
  • next-packages/compiler/reactivity-parser/package.json (1 hunks)
  • next-packages/compiler/unplugin-inula-next/package.json (1 hunks)
  • next-packages/compiler/view-generator/package.json (1 hunks)
  • next-packages/compiler/view-generator/src/NodeGenerators/ForGenerator.ts (2 hunks)
  • next-packages/compiler/view-generator/src/NodeGenerators/SuspenseGenerator.ts (1 hunks)
  • next-packages/inula-bridge/package.json (1 hunks)
  • next-packages/runtime/package.json (1 hunks)
  • next-packages/runtime/src/Nodes/MutableNodes/expression.ts (1 hunks)
  • next-packages/runtime/src/Nodes/MutableNodes/for.ts (4 hunks)
  • next-packages/runtime/src/render.ts (1 hunks)
  • next-packages/runtime/test/e2e/for.test.tsx (2 hunks)
  • next-packages/runtime/test/e2e/jsx-rendering.test.tsx (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (4)
next-packages/runtime/test/e2e/jsx-rendering.test.tsx (1)
next-packages/runtime/src/render.ts (1)
  • render (11-21)
next-packages/compiler/babel-inula-next-core/test/sugarPlugins/mappingToFor.test.tsx (1)
packages/inula-dev-tools/babel.config.js (1)
  • api (16-40)
next-packages/compiler/babel-inula-next-core/test/e2e/mappingToFor.test.ts (1)
next-packages/compiler/unplugin-inula-next/src/index.ts (1)
  • transform (38-59)
next-packages/compiler/babel-inula-next-core/test/sugarPlugins/jsxSlice.test.ts (1)
packages/inula-dev-tools/babel.config.js (1)
  • api (16-40)
🪛 GitHub Actions: ci
next-packages/compiler/error-handler/package.json

[error] 1-1: Step: pnpm install. ERR_PNPM_OUTDATED_LOCKFILE: Cannot install with 'frozen-lockfile' because pnpm-lock.yaml is not up to date with /next-packages/compiler/error-handler/package.json.

🪛 ast-grep (0.38.6)
next-packages/runtime/src/render.ts

[warning] 14-14: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
Context: container.innerHTML = ''
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://owasp.org/www-community/xss-filter-evasion-cheatsheet
- https://cwe.mitre.org/data/definitions/79.html

(dom-content-modification)

🪛 ESLint
next-packages/runtime/test/e2e/for.test.tsx

[error] 63-63: 'extra' is never reassigned. Use 'const' instead.

(prefer-const)


[error] 401-401: Unnecessary escape character: ".

(no-useless-escape)


[error] 401-401: Unnecessary escape character: ".

(no-useless-escape)


[error] 401-401: Unnecessary escape character: ".

(no-useless-escape)


[error] 401-401: Unnecessary escape character: ".

(no-useless-escape)


[error] 401-401: Unnecessary escape character: ".

(no-useless-escape)


[error] 401-401: Unnecessary escape character: ".

(no-useless-escape)

🔇 Additional comments (27)
next-packages/inula-bridge/package.json (1)

33-41: Caret workspace ranges adoption: LGTM

Switching to "workspace:^" for internal packages is a good move for predictable semver alignment across the monorepo.

next-packages/runtime/package.json (1)

37-38: LGTM on caret workspace range update

The devDependency switch to "workspace:^" aligns with the repo-wide dependency strategy.

next-packages/compiler/jsx-parser/package.json (1)

26-27: LGTM on devDependency range update

Consistent with the monorepo’s move to caret workspace ranges.

next-packages/compiler/reactivity-parser/package.json (1)

28-30: Switching to workspace:^ is appropriate for published ranges

Good change. Using "workspace:^" communicates a compatible semver range to external consumers upon publish, while still linking locally in the workspace. For pre-1.0 versions, caret ranges remain conservative (patch-level for 0.0.x), which aligns with safety expectations.

next-packages/runtime/test/e2e/jsx-rendering.test.tsx (1)

43-51: Solid coverage for reusing the same JSX variable

This test is valuable. It verifies that reusing a JSX variable multiple times yields distinct rendered instances rather than moving a single node—preventing subtle DOM ownership bugs.

next-packages/compiler/unplugin-inula-next/package.json (1)

88-90: No "workspace:*" occurrences found — caret workspace ranges look consistent

I ran the provided audit across the repository's package.json files; it returned no matches for "workspace:*". No further changes required.

next-packages/compiler/babel-inula-next-core/test/e2e/subComponent.test.tsx (1)

70-72: Snapshot updates align with thunk-based subcomponent creation

Switching input to a factory (() => $$createCompNode(...)) and emitting $$createExpNode(() => input, () => [], 0) is consistent with the thunk/wrapper approach. Removing deps ([]) and lowering the priority to 0 is appropriate since input is stable and the inner JSX_input tracks its own reactive state.

next-packages/runtime/src/render.ts (1)

16-18: Thunk support in render looks correct

Type-guarding the argument and executing function-valued nodes aligns with the new JSX slice/thunk strategy.

next-packages/compiler/babel-inula-next-core/src/sugarPlugins/jsxSlicePlugin.ts (1)

96-101: Replacing an expression-bodied arrow with a block body is safe here

Transforming the JSX-returning arrow body to a block with a local declaration plus a return is valid and avoids scope leakage. Good call.

next-packages/compiler/view-generator/src/NodeGenerators/ForGenerator.ts (1)

70-75: createForNode accepts null keys; ForNode falls back to index

Verified: createForNode's keysFunc is typed as null | (() => Value[]), and ForNode defines keys?: Value[] and uses this.keys?.[idx] ?? idx in the nodes getter — returning t.nullLiteral() when no key is provided is correct and downstream logic will fall back to the item index.

  • next-packages/runtime/src/Nodes/MutableNodes/for.ts — createForNode signature (lines ~477–484) and ForNode class (lines ~28–52) showing keys?: Value[] and fallback usage.
  • next-packages/compiler/view-generator/src/NodeGenerators/ForGenerator.ts — generator returns t.nullLiteral() for absent keys and passes the key mapper into createForNode (calls around lines ~35–43 and the checked snippet at ~70–75).
next-packages/compiler/babel-inula-next-core/test/generator/view.test.tsx (1)

168-174: Snapshot update matches new optional-chained key mapping

The expected code now emits a block-bodied arrow and guards mapping with optional chaining; this aligns with the generator change.

next-packages/runtime/test/e2e/for.test.tsx (2)

269-308: Solid coverage: dependency updates across list and external state

Good end-to-end assertions for pushing items and independent counter changes within the same render tree.


310-318: Nice breadth of for-loop scenarios

Empty arrays, removals, clear operations, and optional member expressions are all exercised. This materially reduces regression risk.

Also applies to: 320-339, 341-357, 405-436

next-packages/compiler/babel-inula-next-core/test/sugarPlugins/jsxSlice.test.ts (9)

34-36: Thunks for JSX slice assignment look correct.

Wrapping the slice in a thunk aligns with the new runtime that accepts function-valued nodes.


41-66: Multi-level JSX slice transformation LGTM.

Snapshot reflects correct thunk-wrapping of the component node.


69-81: Ternary branches correctly return thunks.

This preserves lazy evaluation and prevents premature node creation.


101-112: Attribute value thunking relies on runtime invocation — please verify.

Ensure the runtime attribute/expr handlers will invoke function-valued entries (e.g., ExpNode/props path). Without that, icon would receive a function instead of a node.

I can add an integration test asserting that <div icon={}/> renders the Icon in attributes after transformation to icon={() => $$createCompNode(JSX_Icon)}. Want me to draft it?


116-131: Fragment handling with thunks looks consistent.

Approach is consistent for both direct assignment and conditional branches.


135-164: Correct: transform inside nested arrow but not the arrow-return of .

This matches the documented behavior; inner JSX returned from fn is wrapped, outer

  • {fn(color)}
  • remains intact.


    168-186: Arrow-return JSX is wrapped correctly.

    Render prop output as a thunk is expected with the updated runtime.


    204-206: Return-path JSX thunking is correct.

    Snapshot matches the new pattern.


    212-224: No transform for arrow in is correct.

    Maintains expected semantics for the iterator callback.

    next-packages/compiler/view-generator/package.json (1)

    26-32: Workspace deps switched to caret range — good.

    This aligns with publishing-friendly semver ranges across the monorepo.

    Double-check your package manager (pnpm/yarn) version supports "workspace:^" and that your release pipeline respects it during publish. Need a quick repo-wide scan to ensure consistency?

    next-packages/compiler/babel-inula-next-core/package.json (1)

    38-43: Consistent switch to "workspace:^" across compiler deps.

    Looks good and consistent with the PR objective.

    next-packages/compiler/babel-inula-next-core/test/sugarPlugins/mappingToFor.test.tsx (1)

    106-107: Snapshot literal formatting change is fine.

    No functional impact.

    next-packages/compiler/babel-inula-next-core/src/sugarPlugins/mapping2ForPlugin.ts (2)

    39-44: OptionalCallExpression support added — good coverage

    Registering a visitor for OptionalCallExpression and delegating to the same handler as CallExpression is the right approach to support both arr.map(...) and arr?.map?.(...).


    115-120: Good: nested OptionalCallExpression traversal is covered

    Adding OptionalCallExpression to the inner traversal ensures nested optional map calls inside the generated for are processed too. This keeps nested transformations consistent.

    Comment on lines +51 to +56
    * Convert map call (regular or optional) in JSXExpressionContainer to for visitor
    *
    * @param path Map call expression path
    * @param path Map call expression path (CallExpression or OptionalCallExpression)
    * @param inner is inside for tag
    */

    function callExpressionVisitor(path: NodePath<t.CallExpression>, inner: boolean): void {
    function mapCallVisitor(path: NodePath<t.CallExpression | t.OptionalCallExpression>, inner: boolean): void {
    Copy link

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    ⚠️ Potential issue

    Potentially missed cases due to ChainExpression wrappers around optional chains

    Optional chaining in Babel can introduce a ChainExpression wrapper around the entire chain. In such cases:

    • For top-level usage in JSX, the OptionalCallExpression’s parent may be ChainExpression, not JSXExpressionContainer.
    • For nested usage inside map callbacks, the immediate parent may be ChainExpression, not ArrowFunctionExpression.

    Because the current checks require the immediate parent to be JSXExpressionContainer (outer) or ArrowFunctionExpression (inner), these cases may be skipped and not transformed.

    Proposed fix: Loosen the parent checks to tolerate an intermediate ChainExpression, and replace the correct target when doing path.replaceWith(). Here is a robust version of mapCallVisitor illustrating the adjustments (focus: parent checks and replacement target):

    function mapCallVisitor(path: NodePath<t.CallExpression | t.OptionalCallExpression>, inner: boolean): void {
      // Helper: climb through a ChainExpression parent if present
      const unwrapChainParent = <T extends Node>(p: NodePath<T>) =>
        p.parentPath && p.parentPath.isChainExpression() ? p.parentPath : p;
    
      const parent = path.parentPath;
      const parentOrChain = parent && parent.isChainExpression() ? parent.parentPath : parent;
    
      // Match arrow function map call inside for tag
      if (inner) {
        const isInsideArrow =
          parent?.isArrowFunctionExpression() ||
          (parent?.isChainExpression() && parent.parentPath?.isArrowFunctionExpression());
        if (!isInsideArrow) return;
      }
    
      // Match map call in JSX expression container
      if (!inner) {
        const isInJsxExpr =
          parent?.isJSXExpressionContainer() ||
          (parent?.isChainExpression() && parent.parentPath?.isJSXExpressionContainer());
        if (!isInJsxExpr) return;
      }
    
      // Don't convert map call inside any JSXOpeningElement (e.g. attributes)
      if (path.parentPath?.parentPath?.parentPath?.isJSXOpeningElement()) return;
    
      const callee = path.get('callee');
      // … existing callee handling …
    
      // Generate for tag ...
      const forElement = /* as in current implementation */;
    
      // Replace correct target: if callee node is wrapped in a ChainExpression parent, replace that
      const replaceTarget = parent?.isChainExpression() ? parent : path;
      if (replaceTarget.parentPath?.isArrowFunctionExpression()) {
        replaceTarget.replaceWith(forElement);
      } else {
        replaceTarget.parentPath?.replaceWith(forElement);
      }
    
      // Traverse inner map calls inside the new for
      if (!inner) {
        (replaceTarget.parentPath ?? path.parentPath)?.traverse({
          CallExpression(innerPath: NodePath<t.CallExpression>) {
            mapCallVisitor(innerPath, true);
          },
          OptionalCallExpression(innerPath: NodePath<t.OptionalCallExpression>) {
            mapCallVisitor(innerPath, true);
          },
        });
      }
    }

    This keeps your current logic but ensures wrapped chains aren’t accidentally skipped.

    🤖 Prompt for AI Agents
    In
    next-packages/compiler/babel-inula-next-core/src/sugarPlugins/mapping2ForPlugin.ts
    around lines 51-56, optional chaining can wrap call expressions in a
    ChainExpression so current checks that require the immediate parent to be
    JSXExpressionContainer (outer) or ArrowFunctionExpression (inner) will miss
    those cases; update the visitor to tolerate an intermediate ChainExpression by
    treating parentOrChain = parentPath.isChainExpression() ? parentPath.parentPath
    : parentPath (or a helper to unwrap one ChainExpression), use that unwrapped
    parent for the JSX/ArrowFunction checks, and when replacing ensure you replace
    the ChainExpression node (or its parent) rather than the raw call path (i.e.,
    set replaceTarget = parentPath.isChainExpression() ? parentPath : path and then
    replace either replaceTarget or replaceTarget.parentPath as appropriate), and
    finally traverse inside the newly inserted for element to call mapCallVisitor
    for nested CallExpression/OptionalCallExpression occurrences.
    

    Comment on lines +73 to +109
    it('should transform to optional key mapping ', () => {
    const code = `
    function MyComp({ comment }) {
    return (
    <div >
    {comment.replies?.map(reply => (
    <CommentComponent
    key={reply.id}
    />
    ))}
    </div>
    )
    }
    `;
    const transformedCode = transform(code);
    expect(transformedCode).toMatchInlineSnapshot(`
    "import { compBuilder as $$compBuilder, createForNode as $$createForNode, createCompNode as $$createCompNode, createHTMLNode as $$createHTMLNode } from "@openinula/next";
    function MyComp({
    comment
    }) {
    const $$self = $$compBuilder();
    $$self.addProp("comment", $$value => comment = $$value, 1);
    return $$self.prepare().init($$createHTMLNode("div", null, $$createForNode(() => comment.replies, () => {
    return comment.replies?.map?.(reply => reply.id);
    }, ($$n, updateItemFuncArr, reply, $$key, $$i) => {
    updateItemFuncArr[$$i] = (newItem, newIdx) => {
    reply = newItem;
    };
    return [$$createCompNode(CommentComponent, {
    "key": reply.id
    }, $$node => {
    $$node.updateProp("key", () => reply.id, [reply?.id], 1);
    })];
    }, 1)));
    }"
    `);
    });
    Copy link

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    💡 Verification agent

    🧩 Analysis chain

    Great coverage for arr?.map with key mapping; add a couple of edge-case tests

    This snapshot thoroughly validates the optional map transformation and key handling. To avoid regressions, consider adding:

    • arr.map?.(cb) form (optional call on the method), e.g., comment.replies.map?.(reply => ).
    • Nested optional mapping inside the callback, e.g., list.map(item => item.children?.map(child => )).
    • Computed property access: comment.replies?.['map']?.(reply => ...).

    These ensure both OptionalCallExpression variants and computed properties are covered.

    You can check if these cases are already covered anywhere with:


    🏁 Script executed:

    #!/bin/bash
    # 1) Look for existing tests using `map?.(`, nested optional mapping, or computed '["map"]'
    rg -n --no-heading "map\?\.\(|\?\.\s*map\(|\?\.\s*\[\s*['\"]map['\"]\s*\]\s*\?\.\s*\(" -A 2 test
    
    # 2) Confirm no handling of `ChainExpression` in the plugin (helps validate the earlier concern)
    rg -n --no-heading "ChainExpression" next-packages/compiler/babel-inula-next-core/src/sugarPlugins/mapping2ForPlugin.ts

    Length of output: 267


    🏁 Script executed:

    #!/bin/bash
    set -euo pipefail
    
    echo "== Search for literal 'map?.(' occurrences =="
    rg -n -F 'map?.(' || true
    
    echo
    echo "== Search for literal '?.map(' occurrences =="
    rg -n -F '?.map(' || true
    
    echo
    echo "== Search for literal '\"map\"' and \"'map'\" (to find computed property uses) =="
    rg -n -F '"map"' || true
    rg -n -F "'map'" || true
    
    echo
    echo "== Locate mapping2ForPlugin.ts =="
    mapping_files=$(fd -HI --type f mapping2ForPlugin.ts 2>/dev/null || true)
    if [ -z "$mapping_files" ]; then
      mapping_files=$(find . -type f -name mapping2ForPlugin.ts 2>/dev/null || true)
    fi
    if [ -z "$mapping_files" ]; then
      echo "No mapping2ForPlugin.ts found."
    else
      echo "Found files:"
      printf '%s\n' "$mapping_files"
    fi
    
    echo
    echo "== Show mapping2ForPlugin.ts (first 400 lines) and search for 'ChainExpression' =="
    if [ -n "$mapping_files" ]; then
      while IFS= read -r f; do
        echo "---- $f ----"
        sed -n '1,400p' "$f" || true
        echo
        echo "Search for 'ChainExpression' in $f:"
        rg -n "ChainExpression" "$f" || true
        echo
      done <<< "$mapping_files"
    fi
    
    echo
    echo "== Search for 'ChainExpression' anywhere in repo =="
    rg -n "ChainExpression" || true

    Length of output: 8138


    Good coverage for arr?.map?. — computed-property + nested-callback cases need attention

    Short summary: arr?.map?.(...) is already covered in tests, but the mapping plugin only accepts Identifier properties named "map", so computed-access forms (e.g. obj?.['map']?.(...)) will be ignored. Please add tests for the missing edge cases and decide whether to extend the plugin.

    Files/locations found

    • Tests already covering optional-call forms:
      • next-packages/compiler/babel-inula-next-core/test/e2e/mappingToFor.test.ts — line ~96: return comment.replies?.map?.(reply => reply.id);
      • next-packages/compiler/babel-inula-next-core/test/generator/view.test.tsx — line ~169: return list?.map?.((item, index) => index);
    • Plugin code that blocks computed-property handling:
      • next-packages/compiler/babel-inula-next-core/src/sugarPlugins/mapping2ForPlugin.ts — contains
        • if (!map.isIdentifier()) { return; }
        • if (map.node.name !== 'map') { return; }
          These checks cause computed property access (e.g. comment.replies?.['map']?.(...)) to be skipped.

    Suggested actions

    • Add a test for nested optional mapping inside the callback (e.g. list.map(item => item.children?.map(child => <X key={child.id}/>))).
    • Add a test for computed-property access (comment.replies?.['map']?.(reply => <CommentComponent key={reply.id} />)) to assert current behaviour.
    • If computed-access should be supported, update mapping2ForPlugin.ts to accept computed string-literal/property 'map' (and handle any ChainExpression variants), then add corresponding tests.
    🤖 Prompt for AI Agents
    In next-packages/compiler/babel-inula-next-core/test/e2e/mappingToFor.test.ts
    around lines 73 to 109, add tests for two missing edge cases: (1) a nested
    optional mapping inside the map callback (e.g. list.map(item =>
    item.children?.map(child => <X key={child.id}/>))) to ensure inner optional-map
    transforms, and (2) a computed-property access form (e.g.
    comment.replies?.['map']?.(reply => <CommentComponent key={reply.id} />)) to
    capture current behavior. Then decide whether to extend the plugin: if
    supporting computed access, update
    next-packages/compiler/babel-inula-next-core/src/sugarPlugins/mapping2ForPlugin.ts
    to accept computed property access and ChainExpression variants by allowing
    MemberExpression with computed string-literal 'map' (and ChainExpression
    wrapping) instead of only Identifier named "map", handle extraction of the
    property name safely, and add the new tests asserting the transformed output;
    otherwise add the computed-access test asserting it remains untransformed.
    

    Comment on lines +21 to 22
    "@openinula/babel-api": "workspace:^"
    },
    Copy link

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    💡 Verification agent

    🧩 Analysis chain

    CI is failing due to an outdated pnpm-lock.yaml; update lockfile after changing workspace ranges

    The change to "workspace:^" is fine, but the CI failure indicates the lockfile is out of sync. Please regenerate and commit pnpm-lock.yaml.

    Recommended steps:

    • Run: pnpm install -w to refresh the workspace lockfile.
    • Commit pnpm-lock.yaml.
    • If CI must remain frozen, ensure the updated lockfile is part of this PR. Alternatively (less ideal), adjust CI to not use --frozen-lockfile for this run.

    Optionally verify no lingering "workspace:*" specifiers:


    🏁 Script executed:

    #!/bin/bash
    # Find any remaining workspace:* specifiers
    rg -n $'\"workspace:\\*\"' -S --glob '!**/pnpm-lock.yaml'

    Length of output: 55


    🏁 Script executed:

    #!/bin/bash
    set -euo pipefail
    
    echo "PWD: $(pwd)"
    echo "Git branch: $(git rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
    echo "Git short HEAD: $(git rev-parse --short HEAD 2>/dev/null || true)"
    echo
    
    echo "1) Searching for any workspace:* or workspace:^ specifiers (excluding pnpm-lock.yaml)..."
    rg -n --hidden -S '"workspace:[^"]*"' --glob '!**/pnpm-lock.yaml' || true
    echo
    
    echo "2) Searching for workspace specifiers inside pnpm-lock.yaml files (if present)..."
    rg -n --hidden -S '"workspace:[^"]*"' || true
    echo
    
    echo "3) Show the package.json under review (next-packages/compiler/error-handler/package.json):"
    if [ -f next-packages/compiler/error-handler/package.json ]; then
      sed -n '1,240p' next-packages/compiler/error-handler/package.json || true
    else
      echo "File not found: next-packages/compiler/error-handler/package.json"
    fi
    echo
    
    echo "4) List pnpm-lock.yaml files in the repo (if any):"
    if command -v fd >/dev/null 2>&1; then
      fd -HI --hidden --type f pnpm-lock.yaml || true
    else
      rg --files --hidden -g 'pnpm-lock.yaml' || true
    fi
    echo
    
    echo "5) Show files changed in the last commit (HEAD):"
    git show --name-only --pretty=format:'' HEAD || true
    echo
    
    echo "6) Git status (porcelain):"
    git status --porcelain || true
    echo

    Length of output: 6584


    Regenerate and commit pnpm-lock.yaml — lockfile out of sync

    Multiple package.json files were changed to use "workspace:^" but pnpm-lock.yaml was not updated in this PR, so CI (with --frozen-lockfile) will fail.

    Files requiring the lockfile refresh:

    • next-packages/compiler/babel-inula-next-core/package.json
    • next-packages/compiler/error-handler/package.json
    • next-packages/compiler/jsx-parser/package.json
    • next-packages/compiler/reactivity-parser/package.json
    • next-packages/compiler/unplugin-inula-next/package.json
    • next-packages/compiler/view-generator/package.json
    • next-packages/inula-bridge/package.json
    • next-packages/runtime/package.json

    Relevant snippet (example):
    "@openinula/babel-api": "workspace:^"

    Recommended fix:

    • Run: pnpm install -w
    • Commit the updated pnpm-lock.yaml to this branch/PR
    • Ensure CI uses the updated lockfile (avoid temporarily disabling --frozen-lockfile unless absolutely necessary)

    Note: I verified there are no remaining "workspace:*" specifiers.

    🤖 Prompt for AI Agents
    In next-packages/compiler/error-handler/package.json around lines 21 to 22, the
    package.json was changed to use "workspace:^" but pnpm-lock.yaml was not
    regenerated, causing CI --frozen-lockfile to fail; run pnpm install -w at the
    monorepo root to refresh pnpm-lock.yaml (this will pick up the workspace:^
    changes across the listed packages), add and commit the updated pnpm-lock.yaml
    to this branch, and push so CI uses the updated lockfile (do not disable
    --frozen-lockfile).
    

    Comment on lines +26 to 29
    const fallbacked = t.callExpression(
    t.memberExpression(suspenseNode, t.identifier('fallback')), [
    fallbackNode,
    ]);
    Copy link

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    💡 Verification agent

    🧩 Analysis chain

    Confirm fallback API: passing raw expression may change laziness semantics

    Previously the generator emitted .fallback(() => ${fallback}), which defers evaluation. Changing to pass fallbackNode directly risks eager evaluation if fallback is a JSX/comp node instead of a factory. Please confirm createSuspenseNode().fallback now expects a node rather than a thunk. If not, wrap fallbackNode in an arrow to preserve laziness:

    -const fallbacked = t.callExpression(
    -  t.memberExpression(suspenseNode, t.identifier('fallback')), [
    -  fallbackNode,
    -]);
    +const fallbacked = t.callExpression(
    +  t.memberExpression(suspenseNode, t.identifier('fallback')),
    +  [t.arrowFunctionExpression([], fallbackNode)]
    +);

    Also, update the JSDoc/example (Lines 10-11) if the new, non-thunk shape is intentional to avoid confusing consumers.

    Run this to verify the expected parameter shape for fallback at its definition:


    🏁 Script executed:

    #!/bin/bash
    # Locate createSuspenseNode and fallback signature/usages to confirm expected arg type.
    rg -n -A 8 -B 4 'createSuspenseNode\('
    rg -n -A 2 -B 2 '\.fallback\s*\('
    rg -n -A 10 -B 5 'function\s+createSuspenseNode|const\s+createSuspenseNode|export\s+const\s+createSuspenseNode|export\s+function\s+createSuspenseNode'

    Length of output: 4537


    🏁 Script executed:

    #!/bin/bash
    set -euo pipefail
    echo "=== show Suspense.ts ==="
    sed -n '1,240p' next-packages/runtime/src/Nodes/MutableNodes/Suspense.ts || true
    
    echo; echo "=== search for 'fallback(' across repo ==="
    rg -n --hidden --no-ignore 'fallback\s*\(' || true
    
    echo; echo "=== show surrounding area where createSuspenseNode is exported ==="
    rg -n -A8 -B8 'export function createSuspenseNode' next-packages/runtime/src/Nodes/MutableNodes/Suspense.ts || true

    Length of output: 279282


    Pass a thunk to Suspense.fallback — runtime expects () => InulaBaseNode

    Confirmed: next-packages/runtime/src/Nodes/MutableNodes/Suspense.ts defines
    fallback(fallback: () => InulaBaseNode) { this.fallbackNode = fallback(); return this; }
    so the runtime expects a thunk and calls it. Passing the raw node from the generator will cause eager evaluation and changes semantics.

    Files to fix:

    • next-packages/compiler/view-generator/src/NodeGenerators/SuspenseGenerator.ts
      • Replace the current call that passes fallbackNode directly with a thunk-wrapping arrow function.

    Suggested change (update generator):

    -const fallbacked = t.callExpression(
    -  t.memberExpression(suspenseNode, t.identifier('fallback')), [
    -  fallbackNode,
    -]);
    +const fallbacked = t.callExpression(
    +  t.memberExpression(suspenseNode, t.identifier('fallback')),
    +  [t.arrowFunctionExpression([], fallbackNode)]
    +);
    • next-packages/runtime/src/Nodes/MutableNodes/Suspense.ts
      • Reference: fallback(fallback: () => InulaBaseNode) { this.fallbackNode = fallback(); return this; } — keeps the expected thunk API.

    Docs: the JSDoc/example already shows .fallback(() => ${fallback}); keep it as-is. Only update docs if you intentionally change the runtime API to accept non-thunks.

    📝 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.

    Suggested change
    const fallbacked = t.callExpression(
    t.memberExpression(suspenseNode, t.identifier('fallback')), [
    fallbackNode,
    ]);
    const fallbacked = t.callExpression(
    t.memberExpression(suspenseNode, t.identifier('fallback')),
    [t.arrowFunctionExpression([], fallbackNode)]
    );
    🤖 Prompt for AI Agents
    In next-packages/compiler/view-generator/src/NodeGenerators/SuspenseGenerator.ts
    around lines 26 to 29, the generator currently passes the raw fallbackNode into
    Suspense.fallback but the runtime expects a thunk (() => InulaBaseNode); change
    the call so it passes an arrow-function thunk that returns the fallback node
    (e.g. create an arrow function with no params whose body returns the existing
    fallbackNode AST) and use that thunk as the argument to the
    t.callExpression/t.memberExpression instead of the raw fallbackNode so the
    runtime can lazily evaluate the fallback.
    

    Comment on lines +75 to 80
    const data= dataFunc();
    if (Array.isArray(data)) {
    this.data = [...data];
    if (keysFunc) this.keys = [...keysFunc()];
    }
    this.update();
    Copy link

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    ⚠️ Potential issue

    Initialize guards only in ctor; update paths still assume arrays.

    Constructor now guards non-array returns, but updateWithOutKey/updateWithKey still assume array-like returns. With the new compiler behavior transforming arr?.map(...) to , dataFunc may return undefined/null at runtime, which will throw on .length. Treat non-arrays as [] in updates.

    Apply this diff within updateWithKey and I can follow up with a patch for updateWithOutKey:

    -  updateWithKey() {
    -    const newData = this.dataFunc();
    -    const newKeys = this.keysFunc!();
    +  updateWithKey() {
    +    const rawData = this.dataFunc();
    +    const newData = Array.isArray(rawData) ? rawData : [];
    +    const rawKeys = this.keysFunc!();
    +    const newKeys = Array.isArray(rawKeys) ? rawKeys : [];

    Additionally, updateWithOutKey should coerce non-arrays to [] as shown below (outside this hunk).

    Committable suggestion skipped: line range outside the PR's diff.

    🤖 Prompt for AI Agents
    In next-packages/runtime/src/Nodes/MutableNodes/for.ts around lines 75 to 80,
    updateWithKey and updateWithOutKey still assume dataFunc() returns an array and
    call array properties/methods directly; change both update paths to coerce
    non-array returns to an empty array (e.g. let data = dataFunc(); if
    (!Array.isArray(data)) data = [];), and similarly coerce keysFunc() before using
    it (or default to []). Ensure all uses of .length, indexing, and iteration
    operate on the coerced local array so runtime undefined/null from
    optional-chaining inputs won’t throw.
    

    Comment on lines +133 to 151
    if (this.dataReactBits & this.owner.dirtyBits!) {
    this.updateArray();
    return;
    }
    this.updateItems();
    // Only update items if there are dirty bits not covered by our reactive data bits
    // This optimization prevents unnecessary item updates when all changes are already
    // handled by the current reactive scope
    //
    // Example 1: this.dataReactBits -> 1110, changed (dirtyBits) -> 1010
    // ~this.dataReactBits & changed -> ~1110 & 1010 -> 0001 & 1010 -> 0000
    // No update needed because dataReactBits contains all the changed bits
    //
    // Example 2: this.dataReactBits -> 1110, changed (dirtyBits) -> 1101
    // ~this.dataReactBits & changed -> ~1110 & 1101 -> 0001 & 1101 -> 0001
    // Update needed because dataReactBits doesn't contain all the changed bits
    // (bit 0 is dirty but not covered by our reactive scope)
    if (~this.dataReactBits & this.owner.dirtyBits!) {
    this.updateItems();
    }
    }
    Copy link

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    🛠️ Refactor suggestion

    Make dirty-bit checks explicit to avoid accidental truthiness traps.

    Using raw bitwise results as booleans works but is easy to misread and fragile if Bits changes type. Compare against 0 for clarity.

    -    if (this.dataReactBits & this.owner.dirtyBits!) {
    +    if (((this.dataReactBits) & (this.owner.dirtyBits!)) !== 0) {
           this.updateArray();
         }
    -    if (~this.dataReactBits & this.owner.dirtyBits!) {
    +    if (((~this.dataReactBits) & (this.owner.dirtyBits!)) !== 0) {
           this.updateItems();
         }
    📝 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.

    Suggested change
    if (this.dataReactBits & this.owner.dirtyBits!) {
    this.updateArray();
    return;
    }
    this.updateItems();
    // Only update items if there are dirty bits not covered by our reactive data bits
    // This optimization prevents unnecessary item updates when all changes are already
    // handled by the current reactive scope
    //
    // Example 1: this.dataReactBits -> 1110, changed (dirtyBits) -> 1010
    // ~this.dataReactBits & changed -> ~1110 & 1010 -> 0001 & 1010 -> 0000
    // No update needed because dataReactBits contains all the changed bits
    //
    // Example 2: this.dataReactBits -> 1110, changed (dirtyBits) -> 1101
    // ~this.dataReactBits & changed -> ~1110 & 1101 -> 0001 & 1101 -> 0001
    // Update needed because dataReactBits doesn't contain all the changed bits
    // (bit 0 is dirty but not covered by our reactive scope)
    if (~this.dataReactBits & this.owner.dirtyBits!) {
    this.updateItems();
    }
    }
    if (((this.dataReactBits) & (this.owner.dirtyBits!)) !== 0) {
    this.updateArray();
    }
    // Only update items if there are dirty bits not covered by our reactive data bits
    // This optimization prevents unnecessary item updates when all changes are already
    // handled by the current reactive scope
    //
    // Example 1: this.dataReactBits -> 1110, changed (dirtyBits) -> 1010
    // ~this.dataReactBits & changed -> ~1110 & 1010 -> 0001 & 1010 -> 0000
    // No update needed because dataReactBits contains all the changed bits
    //
    // Example 2: this.dataReactBits -> 1110, changed (dirtyBits) -> 1101
    // ~this.dataReactBits & changed -> ~1110 & 1101 -> 0001 & 1101 -> 0001
    // Update needed because dataReactBits doesn't contain all the changed bits
    // (bit 0 is dirty but not covered by our reactive scope)
    if (((~this.dataReactBits) & (this.owner.dirtyBits!)) !== 0) {
    this.updateItems();
    }
    }

    Comment on lines +11 to 21
    export const render = (compNode: CompNode | (() => CompNode), container: HTMLElement) => {
    if (container == null) {
    throw new Error('Render target is empty. Please provide a valid DOM element.');
    }
    container.innerHTML = '';
    if (typeof compNode === 'function') {
    compNode = compNode();
    }
    insertNode(container as InulaHTMLNode, compNode, 0);
    runDidMount();
    };
    Copy link

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    🛠️ Refactor suggestion

    Avoid param reassignment; resolve thunk before clearing; prefer replaceChildren() over innerHTML=''

    • Don’t reassign the function parameter; use a local node for clarity and lint-compatibility.
    • Resolve the thunk before clearing to avoid wiping the container if resolution throws.
    • container.replaceChildren() is cleaner and avoids the static analysis innerHTML warning.

    Apply this diff:

     export const render = (compNode: CompNode | (() => CompNode), container: HTMLElement) => {
       if (container == null) {
         throw new Error('Render target is empty. Please provide a valid DOM element.');
       }
    -  container.innerHTML = '';
    -  if (typeof compNode === 'function') {
    -    compNode = compNode();
    -  }
    -  insertNode(container as InulaHTMLNode, compNode, 0);
    +  const node = typeof compNode === 'function' ? compNode() : compNode;
    +  container.replaceChildren();
    +  insertNode(container as InulaHTMLNode, node, 0);
       runDidMount();
     };
    📝 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.

    Suggested change
    export const render = (compNode: CompNode | (() => CompNode), container: HTMLElement) => {
    if (container == null) {
    throw new Error('Render target is empty. Please provide a valid DOM element.');
    }
    container.innerHTML = '';
    if (typeof compNode === 'function') {
    compNode = compNode();
    }
    insertNode(container as InulaHTMLNode, compNode, 0);
    runDidMount();
    };
    export const render = (compNode: CompNode | (() => CompNode), container: HTMLElement) => {
    if (container == null) {
    throw new Error('Render target is empty. Please provide a valid DOM element.');
    }
    const node = typeof compNode === 'function' ? compNode() : compNode;
    container.replaceChildren();
    insertNode(container as InulaHTMLNode, node, 0);
    runDidMount();
    };
    🧰 Tools
    🪛 ast-grep (0.38.6)

    [warning] 14-14: Direct modification of innerHTML or outerHTML properties detected. Modifying these properties with unsanitized user input can lead to XSS vulnerabilities. Use safe alternatives or sanitize content first.
    Context: container.innerHTML = ''
    Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
    - https://owasp.org/www-community/xss-filter-evasion-cheatsheet
    - https://cwe.mitre.org/data/definitions/79.html

    (dom-content-modification)

    🤖 Prompt for AI Agents
    In next-packages/runtime/src/render.ts around lines 11 to 21, avoid reassigning
    the compNode parameter and resolve thunks before mutating the container: create
    a local const node = typeof compNode === 'function' ? compNode() : compNode;
    then clear the container using container.replaceChildren() instead of
    container.innerHTML = '' (so the container is only cleared after the thunk
    successfully resolves), and call insertNode(container as InulaHTMLNode, node, 0)
    followed by runDidMount(); this keeps the parameter immutable and prevents
    wiping the DOM if thunk throws while addressing the innerHTML lint warning.
    

    @HoikanChan HoikanChan closed this Sep 2, 2025
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Labels

    None yet

    Projects

    None yet

    Development

    Successfully merging this pull request may close these issues.

    1 participant