@dimina-kit/inspect 0.3.0 → 0.4.0-dev.20260716163758
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -13
- package/dist/appdata-accumulator.js +218 -0
- package/dist/appdata-panel-view.js +64 -0
- package/dist/appdata-source.js +1 -0
- package/dist/compile-panel-view.js +92 -0
- package/dist/compile-source.js +1 -0
- package/dist/compile-types.js +4 -0
- package/dist/connected-appdata-panel.js +32 -0
- package/dist/connected-compile-panel.js +72 -0
- package/dist/index.js +1 -0
- package/dist/panel.js +5 -0
- package/dist/use-active-bridge-id.js +59 -0
- package/dist/wxml-extract.js +39 -103
- package/package.json +4 -1
- package/src/appdata-accumulator.test.ts +83 -0
- package/src/appdata-accumulator.ts +246 -0
- package/src/appdata-panel-view-empty-state.test.tsx +65 -0
- package/src/appdata-panel-view.test.tsx +49 -0
- package/src/appdata-panel-view.tsx +165 -0
- package/src/appdata-source.ts +18 -0
- package/src/compile-panel-view-logs.test.tsx +269 -0
- package/src/compile-panel-view.test.tsx +194 -0
- package/src/compile-panel-view.tsx +236 -0
- package/src/compile-source.ts +35 -0
- package/src/compile-types.ts +35 -0
- package/src/connected-appdata-panel.test.tsx +426 -0
- package/src/connected-appdata-panel.tsx +62 -0
- package/src/connected-compile-panel.test.tsx +500 -0
- package/src/connected-compile-panel.tsx +91 -0
- package/src/index.ts +16 -0
- package/src/panel.tsx +5 -0
- package/src/use-active-bridge-id.test.tsx +202 -0
- package/src/use-active-bridge-id.ts +71 -0
- package/src/wxml-extract.test.ts +70 -0
- package/src/wxml-extract.ts +38 -100
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for `useActiveBridgeId`.
|
|
3
|
+
*
|
|
4
|
+
* Validates the synchronous-derivation contract:
|
|
5
|
+
* - `activeBridgeId` must be correct on the **same render** as `bridges`
|
|
6
|
+
* changes — no useEffect lag allowed.
|
|
7
|
+
* - Tracks the last newly-added bridge id automatically.
|
|
8
|
+
* - Manual selection via `setActiveBridge` is respected across rerenders
|
|
9
|
+
* (while no new bridge id appears).
|
|
10
|
+
* - A newly-added bridge always overrides a prior manual selection.
|
|
11
|
+
* - If the selected bridge id is removed, falls back to the last bridge.
|
|
12
|
+
* - Calling `setActiveBridge` with an id not in `bridges` is ignored.
|
|
13
|
+
*/
|
|
14
|
+
import { describe, it, expect } from 'vitest'
|
|
15
|
+
import { renderHook, act } from '@testing-library/react'
|
|
16
|
+
|
|
17
|
+
import { useActiveBridgeId } from './use-active-bridge-id.js'
|
|
18
|
+
|
|
19
|
+
const A = { id: 'A', pagePath: '/a' }
|
|
20
|
+
const B = { id: 'B', pagePath: '/b' }
|
|
21
|
+
const C = { id: 'C', pagePath: '/c' }
|
|
22
|
+
|
|
23
|
+
describe('useActiveBridgeId — synchronous derivation contract', () => {
|
|
24
|
+
// Step 1: initial empty bridges → activeBridgeId is null
|
|
25
|
+
it('step 1: returns null when bridges is empty', () => {
|
|
26
|
+
const { result } = renderHook(() => useActiveBridgeId([]))
|
|
27
|
+
// Must be correct synchronously — no await / flush needed
|
|
28
|
+
expect(result.current.activeBridgeId).toBeNull()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
// Step 2: first bridge appears → activeBridgeId follows immediately
|
|
32
|
+
it('step 2: tracks the only bridge synchronously on rerender', () => {
|
|
33
|
+
const { result, rerender } = renderHook(
|
|
34
|
+
({ bridges }) => useActiveBridgeId(bridges),
|
|
35
|
+
{ initialProps: { bridges: [] as typeof A[] } },
|
|
36
|
+
)
|
|
37
|
+
expect(result.current.activeBridgeId).toBeNull()
|
|
38
|
+
|
|
39
|
+
rerender({ bridges: [A] })
|
|
40
|
+
// Synchronous check — if the impl uses useEffect, this will still be null → red
|
|
41
|
+
expect(result.current.activeBridgeId).toBe('A')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
// Step 3: new bridge B is appended → activeBridgeId follows the newest (last) id
|
|
45
|
+
it('step 3: follows the newest bridge when a new id is added', () => {
|
|
46
|
+
const { result, rerender } = renderHook(
|
|
47
|
+
({ bridges }) => useActiveBridgeId(bridges),
|
|
48
|
+
{ initialProps: { bridges: [] as Array<{ id: string; pagePath: string | null }> } },
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
rerender({ bridges: [A] })
|
|
52
|
+
expect(result.current.activeBridgeId).toBe('A')
|
|
53
|
+
|
|
54
|
+
rerender({ bridges: [A, B] })
|
|
55
|
+
// B is new — must be selected synchronously
|
|
56
|
+
expect(result.current.activeBridgeId).toBe('B')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// Step 4 + 5: manual setActiveBridge is respected and persists across rerenders
|
|
60
|
+
// without new bridge ids
|
|
61
|
+
it('step 4+5: setActiveBridge selects a valid id and persists across rerenders with no new bridges', () => {
|
|
62
|
+
const { result, rerender } = renderHook(
|
|
63
|
+
({ bridges }) => useActiveBridgeId(bridges),
|
|
64
|
+
{ initialProps: { bridges: [A, B] } },
|
|
65
|
+
)
|
|
66
|
+
// After initial render with [A, B], B is last → selected
|
|
67
|
+
expect(result.current.activeBridgeId).toBe('B')
|
|
68
|
+
|
|
69
|
+
// Step 4: manual select A
|
|
70
|
+
act(() => {
|
|
71
|
+
result.current.setActiveBridge('A')
|
|
72
|
+
})
|
|
73
|
+
expect(result.current.activeBridgeId).toBe('A')
|
|
74
|
+
|
|
75
|
+
// Step 5: rerender with the same ids — no new bridge → manual selection persists
|
|
76
|
+
rerender({ bridges: [A, B] })
|
|
77
|
+
expect(result.current.activeBridgeId).toBe('A')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
// Step 6: selected bridge is removed → falls back to last bridge
|
|
81
|
+
it('step 6: falls back to last bridge when the selected bridge is removed', () => {
|
|
82
|
+
const { result, rerender } = renderHook(
|
|
83
|
+
({ bridges }) => useActiveBridgeId(bridges),
|
|
84
|
+
{ initialProps: { bridges: [A, B] } },
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
// Manually select A
|
|
88
|
+
act(() => {
|
|
89
|
+
result.current.setActiveBridge('A')
|
|
90
|
+
})
|
|
91
|
+
expect(result.current.activeBridgeId).toBe('A')
|
|
92
|
+
|
|
93
|
+
// Remove A — only B remains
|
|
94
|
+
rerender({ bridges: [B] })
|
|
95
|
+
expect(result.current.activeBridgeId).toBe('B')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
// Step 7: new bridge C appears even when A was manually selected → C takes over
|
|
99
|
+
it('step 7: new bridge overrides prior manual selection synchronously', () => {
|
|
100
|
+
const { result, rerender } = renderHook(
|
|
101
|
+
({ bridges }) => useActiveBridgeId(bridges),
|
|
102
|
+
{ initialProps: { bridges: [A, B] } },
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
// Manual select A
|
|
106
|
+
act(() => {
|
|
107
|
+
result.current.setActiveBridge('A')
|
|
108
|
+
})
|
|
109
|
+
expect(result.current.activeBridgeId).toBe('A')
|
|
110
|
+
|
|
111
|
+
// C is a newly-added bridge id → must override manual selection
|
|
112
|
+
rerender({ bridges: [A, B, C] })
|
|
113
|
+
// Synchronous check — C must be selected in this render, not a future one
|
|
114
|
+
expect(result.current.activeBridgeId).toBe('C')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
// Extra: setActiveBridge with unknown id is silently ignored
|
|
118
|
+
it('ignores setActiveBridge calls with an id not present in bridges', () => {
|
|
119
|
+
const { result } = renderHook(() => useActiveBridgeId([A, B]))
|
|
120
|
+
// Default: B (last)
|
|
121
|
+
expect(result.current.activeBridgeId).toBe('B')
|
|
122
|
+
|
|
123
|
+
act(() => {
|
|
124
|
+
result.current.setActiveBridge('DOES_NOT_EXIST')
|
|
125
|
+
})
|
|
126
|
+
// Still B — invalid id was ignored
|
|
127
|
+
expect(result.current.activeBridgeId).toBe('B')
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
describe('useActiveBridgeId — follows the simulator active page', () => {
|
|
132
|
+
// Auto-follow targets the page on screen, NOT just the last-inited bridge.
|
|
133
|
+
it('selects the bridge matching activePagePath instead of the last bridge', () => {
|
|
134
|
+
// B is last, but the active page is A → A wins.
|
|
135
|
+
const { result } = renderHook(() => useActiveBridgeId([A, B], '/a'))
|
|
136
|
+
expect(result.current.activeBridgeId).toBe('A')
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
// Switching a tabBar tab re-inits no bridge; only activePagePath changes.
|
|
140
|
+
it('re-follows when activePagePath changes with the same bridge set', () => {
|
|
141
|
+
const { result, rerender } = renderHook(
|
|
142
|
+
({ active }) => useActiveBridgeId([A, B, C], active),
|
|
143
|
+
{ initialProps: { active: '/a' } },
|
|
144
|
+
)
|
|
145
|
+
expect(result.current.activeBridgeId).toBe('A')
|
|
146
|
+
|
|
147
|
+
rerender({ active: '/c' })
|
|
148
|
+
expect(result.current.activeBridgeId).toBe('C')
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
// A tab switch overrides a stale manual pick — the panel returns to the page
|
|
152
|
+
// the user is now looking at.
|
|
153
|
+
it('drops a manual pick when the active page changes', () => {
|
|
154
|
+
const { result, rerender } = renderHook(
|
|
155
|
+
({ active }) => useActiveBridgeId([A, B, C], active),
|
|
156
|
+
{ initialProps: { active: '/a' } },
|
|
157
|
+
)
|
|
158
|
+
act(() => {
|
|
159
|
+
result.current.setActiveBridge('B')
|
|
160
|
+
})
|
|
161
|
+
expect(result.current.activeBridgeId).toBe('B')
|
|
162
|
+
|
|
163
|
+
// User switches the simulator to page C → follow C, not the pinned B.
|
|
164
|
+
rerender({ active: '/c' })
|
|
165
|
+
expect(result.current.activeBridgeId).toBe('C')
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
// While the active page is unchanged, a manual pick is still respected.
|
|
169
|
+
it('keeps a manual pick while activePagePath is unchanged', () => {
|
|
170
|
+
const { result, rerender } = renderHook(
|
|
171
|
+
({ active }) => useActiveBridgeId([A, B, C], active),
|
|
172
|
+
{ initialProps: { active: '/a' } },
|
|
173
|
+
)
|
|
174
|
+
act(() => {
|
|
175
|
+
result.current.setActiveBridge('B')
|
|
176
|
+
})
|
|
177
|
+
expect(result.current.activeBridgeId).toBe('B')
|
|
178
|
+
|
|
179
|
+
rerender({ active: '/a' })
|
|
180
|
+
expect(result.current.activeBridgeId).toBe('B')
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
// pagePath comparison tolerates a leading slash on either side.
|
|
184
|
+
it('matches activePagePath regardless of a leading slash', () => {
|
|
185
|
+
const bareA = { id: 'A', pagePath: 'pages/a/a' }
|
|
186
|
+
const bareB = { id: 'B', pagePath: 'pages/b/b' }
|
|
187
|
+
const { result } = renderHook(() => useActiveBridgeId([bareA, bareB], '/pages/a/a'))
|
|
188
|
+
expect(result.current.activeBridgeId).toBe('A')
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
// No active page / no matching bridge → keep the legacy newest-bridge default.
|
|
192
|
+
it('falls back to the last bridge when the active page is empty or unmatched', () => {
|
|
193
|
+
const { result, rerender } = renderHook(
|
|
194
|
+
({ active }) => useActiveBridgeId([A, B], active),
|
|
195
|
+
{ initialProps: { active: '' } },
|
|
196
|
+
)
|
|
197
|
+
expect(result.current.activeBridgeId).toBe('B')
|
|
198
|
+
|
|
199
|
+
rerender({ active: '/zzz' })
|
|
200
|
+
expect(result.current.activeBridgeId).toBe('B')
|
|
201
|
+
})
|
|
202
|
+
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
|
|
3
|
+
function normalizePagePath(p: string | null | undefined): string {
|
|
4
|
+
return (p ?? '').replace(/^\/+/, '')
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Derive which AppData page tab is active, synchronously.
|
|
9
|
+
*
|
|
10
|
+
* `activeBridgeId` is a pure derivation of (current bridges, the simulator's
|
|
11
|
+
* active page, manual pick), computed during render — so it is never stale for
|
|
12
|
+
* a frame the way a useEffect-derived value would be. Auto-follow tracks the
|
|
13
|
+
* page the user is actually looking at: when `activePagePath` matches a
|
|
14
|
+
* bridge's pagePath the panel snaps to it, so switching tabBar tabs (which
|
|
15
|
+
* re-inits no bridge) still moves the panel. A brand-new bridge id appearing or
|
|
16
|
+
* the active page changing both drop a prior manual pick back to auto-follow.
|
|
17
|
+
*/
|
|
18
|
+
export function useActiveBridgeId(
|
|
19
|
+
bridges: ReadonlyArray<{ id: string; pagePath: string | null }>,
|
|
20
|
+
activePagePath = '',
|
|
21
|
+
): { activeBridgeId: string | null; setActiveBridge: (id: string) => void } {
|
|
22
|
+
const ids = bridges.map((b) => b.id)
|
|
23
|
+
// NUL separator: ids are runtime-generated and never contain it, so two
|
|
24
|
+
// distinct id lists can never collide on the same key.
|
|
25
|
+
const idsKey = ids.join('\x00')
|
|
26
|
+
|
|
27
|
+
// The user's manual tab pick; null means "auto-follow the active page".
|
|
28
|
+
const [selectedBridgeId, setSelectedBridgeId] = useState<string | null>(null)
|
|
29
|
+
// The id list from the previous render — lets us spot a freshly-inited page.
|
|
30
|
+
const [prevIdsKey, setPrevIdsKey] = useState('')
|
|
31
|
+
// The active page from the previous render — lets us spot a tab switch.
|
|
32
|
+
const [prevActivePath, setPrevActivePath] = useState(activePagePath)
|
|
33
|
+
|
|
34
|
+
if (idsKey !== prevIdsKey) {
|
|
35
|
+
// The bridge set changed. If a never-before-seen id appeared, a new page
|
|
36
|
+
// inited → drop back to auto-follow. Adjusting state during render is the
|
|
37
|
+
// React-sanctioned alternative to an effect: React re-renders synchronously
|
|
38
|
+
// before the commit, so `activeBridgeId` below is correct this frame.
|
|
39
|
+
const prevIds = prevIdsKey ? prevIdsKey.split('\x00') : []
|
|
40
|
+
if (ids.some((id) => !prevIds.includes(id))) {
|
|
41
|
+
setSelectedBridgeId(null)
|
|
42
|
+
}
|
|
43
|
+
setPrevIdsKey(idsKey)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (activePagePath !== prevActivePath) {
|
|
47
|
+
// The simulator navigated to a different active page (e.g. a tabBar switch).
|
|
48
|
+
// Drop the manual pick so the panel re-follows the page on screen — without
|
|
49
|
+
// this, switching tabs leaves the panel stuck on whichever page inited last.
|
|
50
|
+
setSelectedBridgeId(null)
|
|
51
|
+
setPrevActivePath(activePagePath)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Auto-follow target: the bridge whose pagePath matches the active page.
|
|
55
|
+
const activeFollowId = activePagePath
|
|
56
|
+
? bridges.find((b) => normalizePagePath(b.pagePath) === normalizePagePath(activePagePath))?.id ?? null
|
|
57
|
+
: null
|
|
58
|
+
|
|
59
|
+
const activeBridgeId
|
|
60
|
+
= selectedBridgeId && ids.includes(selectedBridgeId)
|
|
61
|
+
? selectedBridgeId
|
|
62
|
+
// Prefer the active page; fall back to the newest bridge when the active
|
|
63
|
+
// page is unknown or has no matching bridge yet.
|
|
64
|
+
: (activeFollowId ?? ids.at(-1) ?? null)
|
|
65
|
+
|
|
66
|
+
const setActiveBridge = (id: string): void => {
|
|
67
|
+
if (ids.includes(id)) setSelectedBridgeId(id)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { activeBridgeId, setActiveBridge }
|
|
71
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { resolveTagName } from './wxml-extract.js'
|
|
3
|
+
import type { ComponentInstance } from './wxml-extract.js'
|
|
4
|
+
|
|
5
|
+
function instance(type: Record<string, unknown>, extra: Record<string, unknown> = {}): ComponentInstance {
|
|
6
|
+
return { type, ...extra } as ComponentInstance
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe('resolveTagName: authoritative Vue name = source path', () => {
|
|
10
|
+
it('surfaces a custom component whose name is its full module path', () => {
|
|
11
|
+
// The render runtime sets `name: componentPath` on every usingComponents
|
|
12
|
+
// entry, so the panel tags the node with its source path (WeChat parity).
|
|
13
|
+
expect(resolveTagName(instance({ name: '/components/ubt/index' }))).toBe('components/ubt/index')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('surfaces a page whose name is its module path', () => {
|
|
17
|
+
expect(resolveTagName(instance({ name: 'pages/tab/home/index' }))).toBe('pages/tab/home/index')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('strips the leading slash so tags never start with `/`', () => {
|
|
21
|
+
expect(resolveTagName(instance({ name: '/pages/index/index' }))).toBe('pages/index/index')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('accepts the path off `__name` when `name` is absent', () => {
|
|
25
|
+
expect(resolveTagName(instance({ __name: '/node-modules/pickpic/thumbnail/image' })))
|
|
26
|
+
.toBe('node-modules/pickpic/thumbnail/image')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('resolves from `type.name` alone, without any provides chain', () => {
|
|
30
|
+
// Robustness over the legacy provide/inject reconstruction, which needed a
|
|
31
|
+
// live parent provides chain; the per-type name carries the path directly.
|
|
32
|
+
expect(resolveTagName(instance({ name: '/components/card/card' }, { parent: undefined, provides: undefined })))
|
|
33
|
+
.toBe('components/card/card')
|
|
34
|
+
})
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
describe('resolveTagName: non-path names keep the legacy fallback behavior', () => {
|
|
38
|
+
it('kebab-cases a built-in PascalCase name (no slash → not a source path)', () => {
|
|
39
|
+
expect(resolveTagName(instance({ name: 'ScrollView' }))).toBe('scroll-view')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('does NOT mislabel a nameless `__scopeId` wrapper as a second page', () => {
|
|
43
|
+
// A Taro template wrapper (`taro_tmpl`/`tmpl_0_3`) carries `__scopeId` but no
|
|
44
|
+
// path-name and is not a page; it must fall back to `template`, never `page`.
|
|
45
|
+
const wrapper = instance(
|
|
46
|
+
{ __scopeId: 'data-v-abc' },
|
|
47
|
+
{ parent: { type: { components: {} } }, appContext: { components: {} } },
|
|
48
|
+
)
|
|
49
|
+
expect(resolveTagName(wrapper)).not.toBe('page')
|
|
50
|
+
expect(resolveTagName(wrapper)).toBe('template')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('no longer reconstructs a path from the provides chain (heuristic removed)', () => {
|
|
54
|
+
// Identity now comes solely from the authoritative `name`. An instance that
|
|
55
|
+
// only carries the legacy signals (proxy.__page__ + provides.path, no
|
|
56
|
+
// path-like name) is NOT upgraded to its path — it falls back like any
|
|
57
|
+
// nameless __scopeId wrapper. Real pages/components always carry `name`.
|
|
58
|
+
const legacy = instance(
|
|
59
|
+
{ __scopeId: 'data-v-x' },
|
|
60
|
+
{
|
|
61
|
+
proxy: { __page__: true },
|
|
62
|
+
provides: { path: '/pages/legacy/legacy' },
|
|
63
|
+
parent: { type: { components: {} } },
|
|
64
|
+
appContext: { components: {} },
|
|
65
|
+
},
|
|
66
|
+
)
|
|
67
|
+
expect(resolveTagName(legacy)).not.toBe('pages/legacy/legacy')
|
|
68
|
+
expect(resolveTagName(legacy)).toBe('template')
|
|
69
|
+
})
|
|
70
|
+
})
|
package/src/wxml-extract.ts
CHANGED
|
@@ -55,71 +55,25 @@ function pascalToKebab(name: string): string {
|
|
|
55
55
|
return name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
|
|
59
|
-
* Read the page's source path off Vue's provide/inject. dimina runtime.js 在
|
|
60
|
-
* dd-page 的 setup() 里调用 `provide('path', path)`,Vue 把它存到
|
|
61
|
-
* `instance.provides`。我们利用这个把页面节点的 tagName 从硬编码 `page`
|
|
62
|
-
* 升级为页面全路径(如 `pages/index/index`),对齐微信开发者工具。
|
|
63
|
-
*/
|
|
64
|
-
function resolvePagePath(instance: ComponentInstance): string | null {
|
|
65
|
-
// dimina runtime 在 dd-page 的 setup 里 `instance.proxy.__page__ = true` 并
|
|
66
|
-
// `provide('path', path)`。两个标记同时具备才视为页面层级,避免 dd-page
|
|
67
|
-
// 的子节点继承 provides.path 后被误判(Vue 用 Object.create 链式 provides)。
|
|
68
|
-
const proxy = (instance as Record<string, unknown>).proxy as
|
|
69
|
-
| Record<string, unknown>
|
|
70
|
-
| undefined
|
|
71
|
-
if (proxy?.__page__ !== true) return null
|
|
72
|
-
const provides = (instance as Record<string, unknown>).provides as
|
|
73
|
-
| Record<string, unknown>
|
|
74
|
-
| undefined
|
|
75
|
-
const path = provides?.path
|
|
76
|
-
if (typeof path !== 'string' || !path) return null
|
|
77
|
-
return path.startsWith('/') ? path.slice(1) : path
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* A NATIVE dimina custom component (a `usingComponents` entry) calls
|
|
82
|
-
* `provide('path', componentPath)` in its setup (render runtime), so its
|
|
83
|
-
* provided `path` DIFFERS from its parent's. A plain descendant inherits the
|
|
84
|
-
* same provides object (identical `path`) and is not a boundary; a Taro template
|
|
85
|
-
* wrapper (`dd-tpl-*`) never provides, so it never differs either. Comparing
|
|
86
|
-
* against the parent's provided path therefore isolates exactly the native
|
|
87
|
-
* component roots — matching WeChat, which shows each custom component as a node
|
|
88
|
-
* tagged with its full registered path (with a synthetic `#shadow-root` added by
|
|
89
|
-
* `wrapInShadowRoot` since the path contains `/`). Pages are handled earlier via
|
|
90
|
-
* their authoritative `__page__` marker, so this only fires for components.
|
|
91
|
-
*/
|
|
92
|
-
function resolveComponentPath(instance: ComponentInstance): string | null {
|
|
93
|
-
const provides = (instance as Record<string, unknown>).provides as Record<string, unknown> | undefined
|
|
94
|
-
const path = provides?.path
|
|
95
|
-
if (typeof path !== 'string' || !path) return null
|
|
96
|
-
const parent = instance.parent as Record<string, unknown> | undefined
|
|
97
|
-
const parentProvides = parent?.provides as Record<string, unknown> | undefined
|
|
98
|
-
if (path === parentProvides?.path) return null
|
|
99
|
-
return path.startsWith('/') ? path.slice(1) : path
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function resolveTagName(instance: ComponentInstance): string {
|
|
58
|
+
export function resolveTagName(instance: ComponentInstance): string {
|
|
103
59
|
const type = instance.type
|
|
104
60
|
if (!type) return 'unknown'
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (componentPath) return componentPath
|
|
61
|
+
// Authoritative source-path identity: the render runtime sets each compiled
|
|
62
|
+
// page/custom-component's Vue `name` to its miniprogram module path. A name
|
|
63
|
+
// containing `/` IS that source path — trust it directly (WeChat parity).
|
|
64
|
+
// It is per-type (not inherited via provides), so a descendant never leaks its
|
|
65
|
+
// ancestor's path, and it needs no live provide/inject chain to reconstruct.
|
|
66
|
+
const nameId = (type.name || type.__name) as string | undefined
|
|
67
|
+
if (nameId && nameId.includes('/')) {
|
|
68
|
+
return nameId.startsWith('/') ? nameId.slice(1) : nameId
|
|
69
|
+
}
|
|
115
70
|
if (typeof type.__tagName === 'string') return type.__tagName
|
|
116
71
|
const name = (type.__name || type.name) as string | undefined
|
|
117
72
|
if (!name) {
|
|
118
|
-
// A nameless component carrying a `__scopeId` is a
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
// wrapper) and fall back to `template`. Defaulting to `page` here would
|
|
73
|
+
// A nameless component carrying a `__scopeId` is a framework template
|
|
74
|
+
// wrapper (e.g. a Taro `taro_tmpl`/`tmpl_0_3`) — pages and custom components
|
|
75
|
+
// carry a path `name` and are resolved above. Recover the registered tag and
|
|
76
|
+
// fall back to `template`; never default to `page` here, which would
|
|
123
77
|
// mislabel every such wrapper as a second page root.
|
|
124
78
|
if (type.__scopeId) return resolveTemplateNameFromParent(instance) || 'template'
|
|
125
79
|
return 'unknown'
|
|
@@ -190,12 +144,35 @@ function getElementSid(instance: ComponentInstance): string | undefined {
|
|
|
190
144
|
return registerSyntheticSid(el)
|
|
191
145
|
}
|
|
192
146
|
|
|
147
|
+
/**
|
|
148
|
+
* The absolute component path a `dd-wrapper` carries in its `name` prop/attr, or
|
|
149
|
+
* null. dimina wraps EVERY custom-component template root in
|
|
150
|
+
* `<dd-wrapper name="/components/foo/foo">`; the path always starts with `/`,
|
|
151
|
+
* which a user-authored `name` attr essentially never does.
|
|
152
|
+
*/
|
|
153
|
+
function wrapperPathName(instance: ComponentInstance): string | null {
|
|
154
|
+
for (const raw of [instance.props, instance.attrs]) {
|
|
155
|
+
const n = (raw as Record<string, unknown> | undefined)?.name
|
|
156
|
+
if (typeof n === 'string' && n.startsWith('/')) return n
|
|
157
|
+
}
|
|
158
|
+
return null
|
|
159
|
+
}
|
|
160
|
+
|
|
193
161
|
function isTransparentComponent(instance: ComponentInstance, tagName: string): boolean {
|
|
194
162
|
if (tagName === 'unknown') return true
|
|
195
163
|
// A framework template wrapper (Taro `taro_tmpl` / `tmpl_0_3`, whether named
|
|
196
164
|
// via `props.is` or recovered from its registration) is compiler scaffolding,
|
|
197
165
|
// not user content — pass its children straight through.
|
|
198
166
|
if (FRAMEWORK_TEMPLATE_RE.test(tagName)) return true
|
|
167
|
+
// dimina wraps every custom-component template root in a
|
|
168
|
+
// `<dd-wrapper name="/path">`. Under the name-path model the ENCLOSING
|
|
169
|
+
// component instance already carries that same source path as its own node
|
|
170
|
+
// (its Vue `name` IS the path), so surfacing the wrapper too would emit a
|
|
171
|
+
// SECOND identical path node — the doubled `<components/foo/foo>` layer. The
|
|
172
|
+
// wrapper is redundant scaffolding: pass its children through. Gated on a
|
|
173
|
+
// path-shaped `name` so a user-authored `<wrapper name="x">` is never
|
|
174
|
+
// swallowed (dimina paths always start with `/`).
|
|
175
|
+
if (tagName === 'wrapper' && wrapperPathName(instance)) return true
|
|
199
176
|
if (tagName === 'template') {
|
|
200
177
|
const props = instance.props as Record<string, unknown> | undefined
|
|
201
178
|
const is = props?.is as string | undefined
|
|
@@ -296,45 +273,6 @@ function extractChildrenFromVNode(vnode: Record<string, unknown> | null | undefi
|
|
|
296
273
|
return result
|
|
297
274
|
}
|
|
298
275
|
|
|
299
|
-
/**
|
|
300
|
-
* Reverse-map Dimina's `<wrapper name="/components/foo/foo">` (used to host
|
|
301
|
-
* every user-defined component) back to the source-level tag the user wrote
|
|
302
|
-
* in WXML (e.g. `<foo>`).
|
|
303
|
-
*
|
|
304
|
-
* The wrapper carries the registered component path in its `name` Vue prop.
|
|
305
|
-
* By convention (also followed by miniprogram tooling), the registered key is
|
|
306
|
-
* the last path segment, optionally stripping a trailing `/index`. We can't
|
|
307
|
-
* see the page's `usingComponents` map from inside the Vue instance, so this
|
|
308
|
-
* convention-based recovery is best-effort: if the user picked a different
|
|
309
|
-
* registration key than the directory name (e.g. `usingComponents:
|
|
310
|
-
* { myCounter: '/components/counter/counter' }`), the panel will show
|
|
311
|
-
* `counter`, not `myCounter`.
|
|
312
|
-
*
|
|
313
|
-
* The path heuristic also resolves a name-collision risk: a user-written
|
|
314
|
-
* `<counter name="x">` would land in the same `attrs.name` slot as the
|
|
315
|
-
* wrapper-internal path. We only unwrap when the value looks like an absolute
|
|
316
|
-
* component path (leading `/`), which dimina always emits but a user would
|
|
317
|
-
* almost never type as a literal attr.
|
|
318
|
-
*/
|
|
319
|
-
function unwrapCustomComponent(node: WxmlNode): WxmlNode {
|
|
320
|
-
if (node.tagName !== 'wrapper') return node
|
|
321
|
-
const path = node.attrs?.name
|
|
322
|
-
if (typeof path !== 'string' || !path.startsWith('/')) return node
|
|
323
|
-
// 去掉前导 `/` 后保留原路径形式(保留所有斜杠,不做 kebab/dash 转换)。
|
|
324
|
-
// 仅当路径以 `/index` 结尾且剥离后仍至少剩一段时才剥(`/index` 单独存在则
|
|
325
|
-
// 退回 wrapper,避免出现空 tagName)。
|
|
326
|
-
const stripped = path.replace(/^\//, '')
|
|
327
|
-
const withoutIndex = stripped.endsWith('/index') ? stripped.slice(0, -'/index'.length) : stripped
|
|
328
|
-
const recovered = withoutIndex || stripped
|
|
329
|
-
if (!recovered || recovered === 'index') return node
|
|
330
|
-
const nextAttrs: Record<string, string> = {}
|
|
331
|
-
for (const [k, v] of Object.entries(node.attrs)) {
|
|
332
|
-
if (k === 'name') continue
|
|
333
|
-
nextAttrs[k] = v
|
|
334
|
-
}
|
|
335
|
-
return { ...node, tagName: recovered, attrs: nextAttrs }
|
|
336
|
-
}
|
|
337
|
-
|
|
338
276
|
/**
|
|
339
277
|
* 把"用户授权"层级(页面 / 自定义组件,tagName 以路径形式呈现,含 `/`)
|
|
340
278
|
* 的 children 包一层合成 `#shadow-root`,对齐微信开发者工具:组件本身
|
|
@@ -362,5 +300,5 @@ export function walkInstance(instance: ComponentInstance, depth: number): WxmlNo
|
|
|
362
300
|
const node: WxmlNode = { tagName, attrs: extractProps(instance), children }
|
|
363
301
|
const sid = getElementSid(instance)
|
|
364
302
|
if (sid) node.sid = sid
|
|
365
|
-
return wrapInShadowRoot(
|
|
303
|
+
return wrapInShadowRoot(node)
|
|
366
304
|
}
|