Fix: Invalid hook call when user use export jsx function (#4831)
* update vite-jsx-plugin for export * update vite-jsx-plugin for export * update changeset level Co-authored-by: Yuhang <dong_yu_hang@126.com>
This commit is contained in:
parent
e3c78c5b16
commit
29b29e6a8a
13 changed files with 244 additions and 19 deletions
5
.changeset/cyan-chefs-marry.md
Normal file
5
.changeset/cyan-chefs-marry.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'astro': patch
|
||||
---
|
||||
|
||||
Update vite-jsx-plugin for jsx export
|
|
@ -1,6 +1,8 @@
|
|||
import type { PluginObj } from '@babel/core';
|
||||
import * as t from '@babel/types';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* This plugin handles every file that runs through our JSX plugin.
|
||||
* Since we statically match every JSX file to an Astro renderer based on import scanning,
|
||||
|
@ -50,29 +52,71 @@ export default function tagExportsWithRenderer({
|
|||
}
|
||||
},
|
||||
},
|
||||
ExportDeclaration(path, state) {
|
||||
const node = path.node;
|
||||
if (node.exportKind === 'type') return;
|
||||
if (node.type === 'ExportAllDeclaration') return;
|
||||
ExportDeclaration: {
|
||||
/**
|
||||
* For default anonymous function export, we need to give them a unique name
|
||||
* @param path
|
||||
* @returns
|
||||
*/
|
||||
enter(path) {
|
||||
const node = path.node;
|
||||
if (node.type !== 'ExportDefaultDeclaration') return;
|
||||
|
||||
if (node.type === 'ExportNamedDeclaration') {
|
||||
if (t.isFunctionDeclaration(node.declaration)) {
|
||||
if (node.declaration.id?.name) {
|
||||
const id = node.declaration.id.name;
|
||||
const tags = state.get('astro:tags') ?? [];
|
||||
state.set('astro:tags', [...tags, id]);
|
||||
if (node.declaration?.type === 'ArrowFunctionExpression') {
|
||||
const uidIdentifier = path.scope.generateUidIdentifier('_arrow_function');
|
||||
path.insertBefore(
|
||||
t.variableDeclaration('const', [
|
||||
t.variableDeclarator(uidIdentifier, node.declaration),
|
||||
])
|
||||
);
|
||||
node.declaration = uidIdentifier;
|
||||
} else if (
|
||||
node.declaration?.type === 'FunctionDeclaration' &&
|
||||
!node.declaration.id?.name
|
||||
) {
|
||||
const uidIdentifier = path.scope.generateUidIdentifier('_function');
|
||||
node.declaration.id = uidIdentifier;
|
||||
}
|
||||
},
|
||||
exit(path, state) {
|
||||
const node = path.node;
|
||||
if (node.exportKind === 'type') return;
|
||||
if (node.type === 'ExportAllDeclaration') return;
|
||||
const addTag = (id: string) => {
|
||||
const tags = state.get('astro:tags') ?? [];
|
||||
state.set('astro:tags', [...tags, id]);
|
||||
};
|
||||
if (node.type === 'ExportNamedDeclaration' || node.type === 'ExportDefaultDeclaration') {
|
||||
if (t.isIdentifier(node.declaration)) {
|
||||
addTag(node.declaration.name);
|
||||
} else if (t.isFunctionDeclaration(node.declaration) && node.declaration.id?.name) {
|
||||
addTag(node.declaration.id.name);
|
||||
} else if (t.isVariableDeclaration(node.declaration)) {
|
||||
node.declaration.declarations?.forEach((declaration) => {
|
||||
if (
|
||||
t.isArrowFunctionExpression(declaration.init) &&
|
||||
t.isIdentifier(declaration.id)
|
||||
) {
|
||||
addTag(declaration.id.name);
|
||||
}
|
||||
});
|
||||
} else if (t.isObjectExpression(node.declaration)) {
|
||||
node.declaration.properties?.forEach((property) => {
|
||||
if (t.isProperty(property) && t.isIdentifier(property.key)) {
|
||||
addTag(property.key.name);
|
||||
}
|
||||
});
|
||||
} else if (t.isExportNamedDeclaration(node)) {
|
||||
node.specifiers.forEach((specifier) => {
|
||||
if (t.isExportSpecifier(specifier) && t.isIdentifier(specifier.exported)) {
|
||||
addTag(specifier.local.name);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (node.type === 'ExportDefaultDeclaration') {
|
||||
if (t.isFunctionDeclaration(node.declaration)) {
|
||||
if (node.declaration.id?.name) {
|
||||
const id = node.declaration.id.name;
|
||||
const tags = state.get('astro:tags') ?? [];
|
||||
state.set('astro:tags', [...tags, id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
8
packages/astro/test/fixtures/react-jsx-export/astro.config.mjs
vendored
Normal file
8
packages/astro/test/fixtures/react-jsx-export/astro.config.mjs
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
import { defineConfig } from 'astro/config';
|
||||
import react from '@astrojs/react';
|
||||
export default defineConfig({
|
||||
integrations: [
|
||||
react()
|
||||
]
|
||||
|
||||
})
|
13
packages/astro/test/fixtures/react-jsx-export/package.json
vendored
Normal file
13
packages/astro/test/fixtures/react-jsx-export/package.json
vendored
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "@test/react-jsx-export",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"astro": "workspace:*",
|
||||
"@astrojs/react": "workspace:*"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.1.0",
|
||||
"react-dom": "^18.1.0"
|
||||
}
|
||||
}
|
16
packages/astro/test/fixtures/react-jsx-export/src/components/DeclarationExportTest.jsx
vendored
Normal file
16
packages/astro/test/fixtures/react-jsx-export/src/components/DeclarationExportTest.jsx
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { useState } from "react"
|
||||
|
||||
export const ConstDeclarationExport = () => {
|
||||
const [example] = useState('Example')
|
||||
return <h2 id="export_const_declaration">{example}</h2>
|
||||
}
|
||||
|
||||
export let LetDeclarationExport = () => {
|
||||
const [example] = useState('Example')
|
||||
return <h2 id="export_let_declaration">{example}</h2>
|
||||
}
|
||||
|
||||
export function FunctionDeclarationExport() {
|
||||
const [example] = useState('Example')
|
||||
return <h2 id="export_function_declaration">{example}</h2>
|
||||
}
|
26
packages/astro/test/fixtures/react-jsx-export/src/components/ListExportTest.jsx
vendored
Normal file
26
packages/astro/test/fixtures/react-jsx-export/src/components/ListExportTest.jsx
vendored
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { useState } from "react"
|
||||
|
||||
const ListExport = () => {
|
||||
const [example] = useState('Example')
|
||||
return <h2 id="default_list_export">{example}</h2>
|
||||
}
|
||||
|
||||
export {ListExport}
|
||||
|
||||
const OriginListExport = () => {
|
||||
const [example] = useState('Example')
|
||||
return <h2 id="renamed_list_export">{example}</h2>
|
||||
}
|
||||
|
||||
export {
|
||||
OriginListExport as RenamedListExport
|
||||
}
|
||||
|
||||
const ListAsDefaultExport = () => {
|
||||
const [example] = useState('Example')
|
||||
return <h2 id="list_as_default_export">{example}</h2>
|
||||
}
|
||||
|
||||
export {
|
||||
ListAsDefaultExport as default
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import { useState } from "react"
|
||||
|
||||
export default () => {
|
||||
const [example] = useState('Example')
|
||||
return <h2 id="anonymous_arrow_default_export">{example}</h2>
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import { useState } from "react"
|
||||
|
||||
export default function() {
|
||||
const [example] = useState('Example')
|
||||
return <h2 id="anonymous_function_default_export">{example}</h2>
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import { useState } from "react";
|
||||
|
||||
const NamedArrowDefaultExport = () => {
|
||||
const [example] = useState('Example')
|
||||
return <h2 id="named_arrow_default_export">{example}</h2>
|
||||
}
|
||||
|
||||
export default NamedArrowDefaultExport;
|
|
@ -0,0 +1,6 @@
|
|||
import { useState } from "react"
|
||||
|
||||
export default function NamedFunctionDefaultExport() {
|
||||
const [example] = useState('Example')
|
||||
return <h2 id="named_Function_default_export">{example}</h2>
|
||||
}
|
23
packages/astro/test/fixtures/react-jsx-export/src/pages/index.astro
vendored
Normal file
23
packages/astro/test/fixtures/react-jsx-export/src/pages/index.astro
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
---
|
||||
import ListAsDefaultExport, {ListExport, RenamedListExport} from '../components/ListExportTest'
|
||||
import {ConstDeclarationExport, LetDeclarationExport, FunctionDeclarationExport} from '../components/DeclarationExportTest'
|
||||
import AnonymousArrowDefaultExport from '../components/defaultExport/AnonymousArrowDefaultExport'
|
||||
import AnonymousFunctionDefaultExport from '../components/defaultExport/AnonymousFunctionDefaultExport'
|
||||
import NamedArrowDefaultExport from '../components/defaultExport/NamedArrowDefaultExport'
|
||||
import NamedFunctionDefaultExport from '../components/defaultExport/NamedFunctionDefaultExport'
|
||||
---
|
||||
|
||||
<h1>React JSX Export Test</h1>
|
||||
|
||||
<ListAsDefaultExport />
|
||||
<ListExport />
|
||||
<RenamedListExport />
|
||||
|
||||
<ConstDeclarationExport />
|
||||
<LetDeclarationExport />
|
||||
<FunctionDeclarationExport />
|
||||
|
||||
<AnonymousArrowDefaultExport />
|
||||
<AnonymousFunctionDefaultExport />
|
||||
<NamedArrowDefaultExport />
|
||||
<NamedFunctionDefaultExport />
|
51
packages/astro/test/react-jsx-export.test.js
Normal file
51
packages/astro/test/react-jsx-export.test.js
Normal file
|
@ -0,0 +1,51 @@
|
|||
import { expect } from 'chai';
|
||||
import * as cheerio from 'cheerio';
|
||||
import { loadFixture } from './test-utils.js';
|
||||
|
||||
describe('react-jsx-export', () => {
|
||||
let fixture;
|
||||
let logs = [];
|
||||
|
||||
const ids = [
|
||||
'anonymous_arrow_default_export',
|
||||
'anonymous_function_default_export',
|
||||
'named_arrow_default_export',
|
||||
'named_Function_default_export',
|
||||
'export_const_declaration',
|
||||
'export_let_declaration',
|
||||
'export_function_declaration',
|
||||
'default_list_export',
|
||||
'renamed_list_export',
|
||||
'list_as_default_export',
|
||||
];
|
||||
|
||||
const reactInvalidHookWarning =
|
||||
'Warning: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons';
|
||||
before(async () => {
|
||||
const logging = {
|
||||
dest: {
|
||||
write(chunk) {
|
||||
logs.push(chunk);
|
||||
},
|
||||
},
|
||||
level: 'warn',
|
||||
};
|
||||
fixture = await loadFixture({
|
||||
root: './fixtures/react-jsx-export/',
|
||||
});
|
||||
await fixture.build({ logging });
|
||||
});
|
||||
|
||||
it('Can load all JSX components', async () => {
|
||||
const html = await fixture.readFile('/index.html');
|
||||
const $ = cheerio.load(html);
|
||||
|
||||
ids.forEach((id) => {
|
||||
expect($(`#${id}`).text()).to.equal('Example');
|
||||
});
|
||||
});
|
||||
|
||||
it('Can not output React Invalid Hook warning', async () => {
|
||||
expect(logs.every((log) => log.message.indexOf(reactInvalidHookWarning) === -1)).to.be.true;
|
||||
});
|
||||
});
|
|
@ -1799,6 +1799,19 @@ importers:
|
|||
react-dom: 18.2.0_react@18.2.0
|
||||
vue: 3.2.39
|
||||
|
||||
packages/astro/test/fixtures/react-jsx-export:
|
||||
specifiers:
|
||||
'@astrojs/react': workspace:*
|
||||
astro: workspace:*
|
||||
react: ^18.1.0
|
||||
react-dom: ^18.1.0
|
||||
dependencies:
|
||||
react: 18.2.0
|
||||
react-dom: 18.2.0_react@18.2.0
|
||||
devDependencies:
|
||||
'@astrojs/react': link:../../../../integrations/react
|
||||
astro: link:../../..
|
||||
|
||||
packages/astro/test/fixtures/reexport-astro-containing-client-component:
|
||||
specifiers:
|
||||
'@astrojs/preact': 'workspace:'
|
||||
|
|
Loading…
Reference in a new issue