Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 92 additions & 17 deletions src/context.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import type { Node } from '@babel/types'
import { isMp } from '@uni-helper/uni-env'
import type { AttributeNode, DirectiveNode, ElementNode, SimpleExpressionNode } from '@vue/compiler-core'
import type {
AttributeNode,
DirectiveNode,
ElementNode,
SimpleExpressionNode,
} from '@vue/compiler-core'
import { babelParse, walkAST } from 'ast-kit'
import MagicString from 'magic-string'
import { kebabCase } from 'scule'
Expand Down Expand Up @@ -71,7 +76,9 @@ export class Context {
// layout name is empty
if (!pageLayoutName)
return
pageLayout = this.layouts.find(l => l.name === (pageLayoutName as string))
pageLayout = this.layouts.find(
l => l.name === (pageLayoutName as string),
)
// can not find layout
if (!pageLayout)
return
Expand Down Expand Up @@ -100,18 +107,58 @@ export class Context {
})
}

// 检查是否有 page-meta 组件
let pageMetaNodes: ElementNode[] = []
if (sfc.template?.ast) {
pageMetaNodes = sfc.template.ast.children.filter(
v =>
v.type === 1
&& (kebabCase(v.tag) === 'page-meta' || v.tag === 'page-meta'),
) as ElementNode[]
}

if (disabled) {
// find dynamic layout
const uniLayoutNode = sfc.template?.ast.children.find(v => v.type === 1 && kebabCase(v.tag) === 'uni-layout') as ElementNode
const uniLayoutNode = sfc.template?.ast!.children.find(
v => v.type === 1 && kebabCase(v.tag) === 'uni-layout',
) as ElementNode
// not found
if (!uniLayoutNode)
return

ms.overwrite(uniLayoutNode.loc.start.offset, uniLayoutNode.loc.end.offset, this.generateDynamicLayout(uniLayoutNode))
ms.overwrite(
uniLayoutNode.loc.start.offset,
uniLayoutNode.loc.end.offset,
this.generateDynamicLayout(uniLayoutNode),
)
}
else {
if (sfc.template?.loc.start.offset && sfc.template?.loc.end.offset)
ms.overwrite(sfc.template?.loc.start.offset, sfc.template?.loc.end.offset, `\n<layout-${pageLayout?.kebabName}-uni ${pageLayoutProps.join(' ')}>${sfc.template.content}</layout-${pageLayout?.kebabName}-uni>\n`)
if (sfc.template?.loc.start.offset && sfc.template?.loc.end.offset) {
// 提取 page-meta 组件内容
const pageMetaContent = pageMetaNodes
.map(node => node.loc.source)
.join('\n')

// 从原内容中移除 page-meta 组件
let contentWithoutPageMeta = sfc.template.content
for (const node of pageMetaNodes) {
contentWithoutPageMeta = contentWithoutPageMeta.replace(
node.loc.source,
'',
)
}

// 在布局外部添加 page-meta
ms.overwrite(
sfc.template?.loc.start.offset,
sfc.template?.loc.end.offset,
`\n${pageMetaContent}<layout-${
pageLayout?.kebabName
}-uni ${pageLayoutProps.join(
' ',
)}>${contentWithoutPageMeta}</layout-${pageLayout?.kebabName}-uni>\n`,
)
}
Comment on lines +136 to +161
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix offset truthiness bug and guard reinjection; handle absence of page-meta cleanly.

Using truthy checks can skip when start.offset === 0. Also, only reflow page-meta for MP; otherwise, keep existing wrapper behavior without the page-meta preface.

Apply this refactor:

-      if (sfc.template?.loc.start.offset && sfc.template?.loc.end.offset) {
-        // 提取 page-meta 组件内容
-        const pageMetaContent = pageMetaNodes
-          .map(node => node.loc.source)
-          .join('\n')
-
-        // 从原内容中移除 page-meta 组件
-        let contentWithoutPageMeta = sfc.template.content
-        for (const node of pageMetaNodes) {
-          contentWithoutPageMeta = contentWithoutPageMeta.replace(
-            node.loc.source,
-            '',
-          )
-        }
-
-        // 在布局外部添加 page-meta
-        ms.overwrite(
-          sfc.template?.loc.start.offset,
-          sfc.template?.loc.end.offset,
-          `\n${pageMetaContent}<layout-${
-            pageLayout?.kebabName
-          }-uni ${pageLayoutProps.join(
-            ' ',
-          )}>${contentWithoutPageMeta}</layout-${pageLayout?.kebabName}-uni>\n`,
-        )
-      }
+      const start = sfc.template?.loc.start.offset
+      const end = sfc.template?.loc.end.offset
+      if (start != null && end != null) {
+        const hasPageMeta = isMp && pageMetaNodes.length > 0
+        let pageMetaContent = ''
+        let contentWithoutPageMeta = sfc.template.content
+        if (hasPageMeta) {
+          pageMetaContent = pageMetaNodes.map(n => n.loc.source).join('\n')
+          for (const n of pageMetaNodes)
+            contentWithoutPageMeta = contentWithoutPageMeta.replace(n.loc.source, '')
+        }
+        const prefix = hasPageMeta ? `${pageMetaContent}\n` : ''
+        ms.overwrite(
+          start,
+          end,
+          `${prefix}<layout-${pageLayout?.kebabName}-uni ${pageLayoutProps.join(' ')}>${contentWithoutPageMeta}</layout-${pageLayout?.kebabName}-uni>`,
+        )
+      }

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

🤖 Prompt for AI Agents
In src/context.ts around lines 136 to 161, the current truthy checks on
sfc.template.loc.start.offset and end.offset skip valid zero offsets and always
reinject page-meta into the layout; change the condition to explicitly check for
null/undefined (e.g., start.offset != null && end.offset != null), only perform
the overwrite if those offsets exist, and additionally guard the page-meta
reinjection so it runs only for MP targets (check the existing isMP/pageRuntime
flag) — if pageMetaNodes is empty or target is non-MP, preserve the existing
wrapper behavior (wrap content without prepending pageMetaContent) and avoid
modifying the template. Ensure contentWithoutPageMeta is computed only when
pageMetaNodes.length > 0 and that ms.overwrite uses the correct offsets and
chosen variant (with or without pageMetaContent) accordingly.

}

if (ms.hasChanged()) {
Expand Down Expand Up @@ -155,42 +202,70 @@ export default {
v => v.type === 6 && v.name === 'name' && v.value?.content,
) as AttributeNode
const dynamicLayoutNameBind = node.props.find(
v => v.type === 7 && v.name === 'bind' && v.arg?.type === 4 && v.arg?.content === 'name' && v.exp?.type === 4 && v.exp.content,
v =>
v.type === 7
&& v.name === 'bind'
&& v.arg?.type === 4
&& v.arg?.content === 'name'
&& v.exp?.type === 4
&& v.exp.content,
) as DirectiveNode
const slotsSource = node.children.map(v => v.loc.source).join('\n')
const nodeProps = node.props.filter(prop => !(prop === dynamicLayoutNameBind || prop === staticLayoutNameBind)).map(v => v.loc.source)
const nodeProps = node.props
.filter(
prop =>
!(prop === dynamicLayoutNameBind || prop === staticLayoutNameBind),
)
.map(v => v.loc.source)

if (!(staticLayoutNameBind || dynamicLayoutNameBind))
console.warn('[vite-plugin-uni-layouts] Dynamic layout not found name bind')
if (!(staticLayoutNameBind || dynamicLayoutNameBind)) {
console.warn(
'[vite-plugin-uni-layouts] Dynamic layout not found name bind',
)
}
Comment on lines +221 to +225
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Avoid runtime crash when no name bind is present.

You warn if neither static nor dynamic name is bound, but later dereference dynamicLayoutNameBind.exp unconditionally. Bail out early by returning slotsSource.

-    if (!(staticLayoutNameBind || dynamicLayoutNameBind)) {
-      console.warn(
-        '[vite-plugin-uni-layouts] Dynamic layout not found name bind',
-      )
-    }
+    if (!(staticLayoutNameBind || dynamicLayoutNameBind)) {
+      console.warn(
+        '[vite-plugin-uni-layouts] Dynamic layout not found name bind',
+      )
+      return slotsSource
+    }
📝 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 (!(staticLayoutNameBind || dynamicLayoutNameBind)) {
console.warn(
'[vite-plugin-uni-layouts] Dynamic layout not found name bind',
)
}
if (!(staticLayoutNameBind || dynamicLayoutNameBind)) {
console.warn(
'[vite-plugin-uni-layouts] Dynamic layout not found name bind',
)
return slotsSource
}
🤖 Prompt for AI Agents
In src/context.ts around lines 221 to 225, the code warns when neither
staticLayoutNameBind nor dynamicLayoutNameBind exists but then proceeds to
dereference dynamicLayoutNameBind.exp later, risking a runtime crash; change the
flow to bail out immediately by returning slotsSource right after the warning
when both binds are absent so no subsequent code attempts to access
dynamicLayoutNameBind.exp.


if (isMp) {
const props: string[] = [...nodeProps]
if (staticLayoutNameBind) {
const layout = staticLayoutNameBind.value?.content
return `<layout-${layout}-uni ${props.join(' ')}>${slotsSource}</layout-${layout}-uni>`
return `<layout-${layout}-uni ${props.join(
' ',
)}>${slotsSource}</layout-${layout}-uni>`
}

const bind = (dynamicLayoutNameBind.exp as SimpleExpressionNode).content
const defaultSlot = node.children.filter((v) => {
if (v.type === 1 && v.tagType === 3) {
const slot = v.props.find(v => v.type === 7 && v.name === 'slot' && v.arg?.type === 4) as any
const slot = v.props.find(
v => v.type === 7 && v.name === 'slot' && v.arg?.type === 4,
) as any
if (slot)
return slot.arg.content === 'default'
}
return v
})
const defaultSlotSource = defaultSlot.map(v => v.loc.source).join('\n')
const layouts = this.layouts.map((layout, index) => `<layout-${layout.kebabName}-uni v-${index === 0 ? 'if' : 'else-if'}="${bind} ==='${layout.kebabName}'" ${props.join(' ')}>${slotsSource}</layout-${layout.kebabName}-uni>`)
const layouts = this.layouts.map(
(layout, index) => `<layout-${layout.kebabName}-uni v-${
index === 0 ? 'if' : 'else-if'
}="${bind} ==='${layout.kebabName}'" ${props.join(
' ',
)}>${slotsSource}</layout-${layout.kebabName}-uni>`,
)
layouts.push(`<template v-else>${defaultSlotSource}</template>`)

return layouts.join('\n')
}
else {
const props: string[] = [...nodeProps]
if (staticLayoutNameBind)
props.push(`is="layout-${staticLayoutNameBind.value?.content}-uni"`)
else
props.push(`:is="\`layout-\${${(dynamicLayoutNameBind.exp as SimpleExpressionNode).content}}-uni\`"`)
if (staticLayoutNameBind) { props.push(`is="layout-${staticLayoutNameBind.value?.content}-uni"`) }
else {
props.push(
`:is="\`layout-\${${
(dynamicLayoutNameBind.exp as SimpleExpressionNode).content
}}-uni\`"`,
)
}
Comment on lines +261 to +268
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix ESLint max-statements-per-line and guard dynamic bind access.

Split the one-liner if/else and avoid undefined access. This also resolves the reported ESLint error.

As per ESLint hint.

-      if (staticLayoutNameBind) { props.push(`is="layout-${staticLayoutNameBind.value?.content}-uni"`) }
-      else {
-        props.push(
-          `:is="\`layout-\${${
-            (dynamicLayoutNameBind.exp as SimpleExpressionNode).content
-          }}-uni\`"`,
-        )
-      }
+      if (staticLayoutNameBind) {
+        props.push(`is="layout-${staticLayoutNameBind.value?.content}-uni"`)
+      }
+      else {
+        const exp = (dynamicLayoutNameBind!.exp as SimpleExpressionNode).content
+        props.push(`:is="\`layout-\${${exp}}-uni\`"`)
+      }
📝 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 (staticLayoutNameBind) { props.push(`is="layout-${staticLayoutNameBind.value?.content}-uni"`) }
else {
props.push(
`:is="\`layout-\${${
(dynamicLayoutNameBind.exp as SimpleExpressionNode).content
}}-uni\`"`,
)
}
if (staticLayoutNameBind) {
props.push(`is="layout-${staticLayoutNameBind.value?.content}-uni"`)
}
else {
const exp = (dynamicLayoutNameBind!.exp as SimpleExpressionNode).content
props.push(`:is="\`layout-\${${exp}}-uni\`"`)
}
🧰 Tools
🪛 ESLint

[error] 261-261: This line has 2 statements. Maximum allowed is 1.

(style/max-statements-per-line)

🤖 Prompt for AI Agents
In src/context.ts around lines 261 to 268, the current one-line if/else both
violates ESLint max-statements-per-line and risks accessing
dynamicLayoutNameBind.exp when undefined; refactor by expanding the conditional
into multi-line branches, add a guard to ensure dynamicLayoutNameBind and
dynamicLayoutNameBind.exp exist and are of the expected SimpleExpressionNode
shape before accessing .content, extract the content into a temporary variable,
and push the static or dynamic `is="layout-...-uni"` string accordingly so there
are no multiple statements on a single line and no unsafe property access.

return `<component ${props.join(' ')}>${slotsSource}</component>`
}
}
Expand Down