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:
董雨航 2022-09-22 03:53:58 +08:00 committed by GitHub
parent e3c78c5b16
commit 29b29e6a8a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 244 additions and 19 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Update vite-jsx-plugin for jsx export

View file

@ -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]);
}
}
}
},
},
},
};
}

View file

@ -0,0 +1,8 @@
import { defineConfig } from 'astro/config';
import react from '@astrojs/react';
export default defineConfig({
integrations: [
react()
]
})

View 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"
}
}

View 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>
}

View 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
}

View file

@ -0,0 +1,6 @@
import { useState } from "react"
export default () => {
const [example] = useState('Example')
return <h2 id="anonymous_arrow_default_export">{example}</h2>
}

View file

@ -0,0 +1,6 @@
import { useState } from "react"
export default function() {
const [example] = useState('Example')
return <h2 id="anonymous_function_default_export">{example}</h2>
}

View file

@ -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;

View file

@ -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>
}

View 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 />

View 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;
});
});

View file

@ -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:'