Skip to content

fix: #34 render nested components declared with absolute usingComponents paths - #119

Closed
lnuxe wants to merge 1 commit into
wechat-miniprogram:masterfrom
lnuxe:fix/34-nested-absolute-using-components
Closed

lnuxe wants to merge 1 commit into
wechat-miniprogram:masterfrom
lnuxe:fix/34-nested-absolute-using-components

Conversation

@lnuxe

@lnuxe lnuxe commented Nov 10, 2026 •

Copy link
Copy Markdown

Root cause

A component declared in usingComponents with an absolute path is resolved against rootPath:

const usingPath = _.isAbsolute(value) ? path.join(rootPath, value) : path.join(path.dirname(componentPath), value)

rootPath defaults to path.dirname(componentPath) of the component passed to simulate.load(). For a page stored in its own directory (page/index), /components/u-icon/index is therefore looked up at <project>/page/components/u-icon/index — which does not exist — so register() skips the entry and the raw path string is handed to j-component/exparser as a component id. exparser then creates an unknown custom element with an empty template, so the nested component silently renders as an empty host node:

<page><wx-view class="page--page"><main-status></main-status></wx-view></page>

The same happens one level deeper: <main-status> declares "u-icon": "/components/u-icon/index", so even when <main-status> renders, <u-icon> stays empty. That is the symptom reported in #34 ("组件嵌套组件时,内部组件没有渲染").

Fix

src/index.js:

  • resolveUsingComponentsPath() keeps the documented behaviour — relative paths are resolved from the component directory, absolute paths from rootPath.
  • When rootPath was not supplied by the user (its default value is only the loaded component's directory, so the project root is unknown), an absolute path that cannot be found there is searched again from the component directory upwards, nearest ancestor first.
  • When such an ancestor is used, the compile root (rootPath) is widened to the common ancestor of the old root and the resolved component; otherwise the official WXML compiler cannot compile a template located outside rootPath (invalid template).
  • Nothing changes when rootPath is passed explicitly, and the relative-path branch is untouched.

Reproducible case

test/comp12 mirrors the issue: page/index.json uses /components/main-status/index, components/main-status/index.json uses /components/u-icon/index, and the page is loaded without rootPath:

const id = simulate.load(path.resolve(__dirname, './page/index'), 'page')
const comp = simulate.render(id)
comp.attach(document.createElement('parent-wrapper'))

Before the fix (src/index.js from master):

$ npx jest test/comp12
FAIL test/comp12/index.test.js
  ✕ comp12 (81 ms)

  ● comp12

    expect(received).toBe(expected) // Object.is equality

    Expected: true
    Received: false

      20 |             </main-status>
      21 |         </wx-view>
    > 22 |     `)).toBe(true)
         |         ^
      23 |   expect(comp.dom.querySelector('main-status').innerHTML).toBeTruthy()
      24 |   expect(comp.dom.querySelector('u-icon').innerHTML).toBeTruthy()
      25 | }

      at runTest (test/comp12/index.test.js:22:9)
      at Object.<anonymous> (test/comp12/index.test.js:29:3)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total

comp.dom.outerHTML of the same fixture on master:

<page><wx-view class="page--page"><main-status></main-status></wx-view></page>

After the fix:

$ npx jest test/comp12
PASS test/comp12/index.test.js
  ✓ comp12 (133 ms)

Tests:       1 passed, 1 total

comp.dom.outerHTML after the fix:

<page><wx-view class="page--page"><main-status><wx-view class="main-status--main--status"><u-icon><wx-view class="u-icon--u-icon">[star]</wx-view></u-icon><wx-view class="main-status--main--msg">这是测试文案</wx-view></wx-view></main-status></wx-view></page>

The regression case runs both with the official compiler and with {compiler: 'simulate'} (jest.resetModules() in between), asserting simulate.match() plus the innerHTML of both nested hosts.

Verification

$ npx jest
Test Suites: 18 passed, 18 total
Tests:       24 passed, 24 total

$ npx eslint "src/**/*.js" "test/**/*.js"
(no output)

The screenshots below are generated by rendering the simulated tree (comp.dom.outerHTML) into an HTML page together with the WXSS the simulator injects (<style> nodes collected from the jsdom document), then photographed with Playwright/Chrome at 1000x780, deviceScaleFactor 2.

beforeafter

Closes #34

@lnuxe
lnuxe requested a review from JuneAndGreen as a code owner November 10, 2026 07:52
@JuneAndGreen

Copy link
Copy Markdown
Contributor

Thanks for the detailed analysis — your root cause is correct. register() silently skips a path that does not resolve, and the raw string then reaches exparser as a component id, which is exactly why #34 renders an empty host instead of failing.

I'm going to close this PR and land the fix myself, without the auto-fallback. Here's why the fallback isn't the direction we want:

  1. It contradicts the documented contract. docs/api.md documents rootPath as "组件所在目录,不传则取当前组件所在目录", and docs/detail.md explicitly says there is no project-root concept in this environment and rootPath is the base used for resolution. Probing the filesystem silently changes rootPath from an explicit anchor into a guess.

  2. The outcome is not reproducible. Nearest-ancestor-first means a directory added anywhere up the tree silently changes which component gets loaded. In a monorepo the walk can reach an ancestor package and pick up an unrelated copy of the same component — it renders "correctly" but wrong, which is harder to debug than today's empty node. Nothing is logged either, so the effective rootPath is invisible to the user.

  3. Widening rootPath is a global side effect, not a local one. rootPath is simultaneously the compile cache key (compilerResCache[config.rootPath]), the directory handed to wxmlToJs(), and the base for path.relative(rootPath, componentPath) in src/compile.js. Widening it mid-walk can compile the same wxml twice under two module ids, and since children are registered before their parent is compiled, parent and child can end up with different compile bases. That's the part of this patch that would be hardest to keep correct.

  4. It hides configuration errors. The bug isn't a missing fallback, it's that a missing component is swallowed. Layering a heuristic over an error-swallowing code path turns an actionable failure into a plausible-but-wrong render.

What I'll land instead is the loud failure, with rootPath staying explicit:

const checkIsAbsolute = _.isAbsolute(value)
const usingPath = checkIsAbsolute ? path.join(rootPath, value) : path.join(path.dirname(componentPath), value)

if (_.readFile(`${usingPath}.json`)) {
  // file path
  const id = register(usingPath, key, cache, hasRegisterCache)
  usingComponents[key] = id
} else if (isPathLike(value)) {
  throw new Error(buildUsingComponentsError({key, value, usingPath, componentPath}))
}

The message names the offending component, the path it resolved to, and asks the user to check the path or pass the correct rootPath — no runtime guessing, no filesystem probing:

invalid usingComponents["u-icon"] in <project>/test/page/index.json: "/components/u-icon/index"
  resolved to <project>/test/page/components/u-icon/index, but the file does not exist.
  Please check the path, or pass the correct rootPath to simulate.load() if the component lives elsewhere.

Relative entries coming from options.usingComponents stay skipped silently, because that map is applied to every registered component and a relative entry is not guaranteed to target the current one.

It goes in as a small change in src/index.js plus a regression test covering both the absolute and the relative case, with the existing suite and eslint green (24 tests). The changelog and the path-resolution section of docs/detail.md will note the behaviour change, since tests that were silently rendering an empty node will now fail at simulate.load().

Thanks for the repro case and the screenshots — they made the failure mode easy to confirm.

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.

组件嵌套组件时,内部组件没有渲染

2 participants