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 type { PluginObj } from '@babel/core';
|
||||||
import * as t from '@babel/types';
|
import * as t from '@babel/types';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This plugin handles every file that runs through our JSX plugin.
|
* 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,
|
* 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) {
|
ExportDeclaration: {
|
||||||
const node = path.node;
|
/**
|
||||||
if (node.exportKind === 'type') return;
|
* For default anonymous function export, we need to give them a unique name
|
||||||
if (node.type === 'ExportAllDeclaration') return;
|
* @param path
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
enter(path) {
|
||||||
|
const node = path.node;
|
||||||
|
if (node.type !== 'ExportDefaultDeclaration') return;
|
||||||
|
|
||||||
if (node.type === 'ExportNamedDeclaration') {
|
if (node.declaration?.type === 'ArrowFunctionExpression') {
|
||||||
if (t.isFunctionDeclaration(node.declaration)) {
|
const uidIdentifier = path.scope.generateUidIdentifier('_arrow_function');
|
||||||
if (node.declaration.id?.name) {
|
path.insertBefore(
|
||||||
const id = node.declaration.id.name;
|
t.variableDeclaration('const', [
|
||||||
const tags = state.get('astro:tags') ?? [];
|
t.variableDeclarator(uidIdentifier, node.declaration),
|
||||||
state.set('astro:tags', [...tags, id]);
|
])
|
||||||
|
);
|
||||||
|
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
|
react-dom: 18.2.0_react@18.2.0
|
||||||
vue: 3.2.39
|
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:
|
packages/astro/test/fixtures/reexport-astro-containing-client-component:
|
||||||
specifiers:
|
specifiers:
|
||||||
'@astrojs/preact': 'workspace:'
|
'@astrojs/preact': 'workspace:'
|
||||||
|
|
Loading…
Reference in a new issue