diff --git a/.changeset/cyan-chefs-marry.md b/.changeset/cyan-chefs-marry.md new file mode 100644 index 000000000..ba2f9aa9c --- /dev/null +++ b/.changeset/cyan-chefs-marry.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Update vite-jsx-plugin for jsx export diff --git a/packages/astro/src/vite-plugin-jsx/tag.ts b/packages/astro/src/vite-plugin-jsx/tag.ts index 12bb3bcdd..bead224e0 100644 --- a/packages/astro/src/vite-plugin-jsx/tag.ts +++ b/packages/astro/src/vite-plugin-jsx/tag.ts @@ -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]); - } - } - } + }, }, + }, }; } diff --git a/packages/astro/test/fixtures/react-jsx-export/astro.config.mjs b/packages/astro/test/fixtures/react-jsx-export/astro.config.mjs new file mode 100644 index 000000000..12e5977e9 --- /dev/null +++ b/packages/astro/test/fixtures/react-jsx-export/astro.config.mjs @@ -0,0 +1,8 @@ +import { defineConfig } from 'astro/config'; +import react from '@astrojs/react'; +export default defineConfig({ + integrations: [ + react() + ] + +}) diff --git a/packages/astro/test/fixtures/react-jsx-export/package.json b/packages/astro/test/fixtures/react-jsx-export/package.json new file mode 100644 index 000000000..1e9c0e303 --- /dev/null +++ b/packages/astro/test/fixtures/react-jsx-export/package.json @@ -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" + } +} diff --git a/packages/astro/test/fixtures/react-jsx-export/src/components/DeclarationExportTest.jsx b/packages/astro/test/fixtures/react-jsx-export/src/components/DeclarationExportTest.jsx new file mode 100644 index 000000000..0986da235 --- /dev/null +++ b/packages/astro/test/fixtures/react-jsx-export/src/components/DeclarationExportTest.jsx @@ -0,0 +1,16 @@ +import { useState } from "react" + +export const ConstDeclarationExport = () => { + const [example] = useState('Example') + return

{example}

+} + +export let LetDeclarationExport = () => { + const [example] = useState('Example') + return

{example}

+} + +export function FunctionDeclarationExport() { + const [example] = useState('Example') + return

{example}

+} \ No newline at end of file diff --git a/packages/astro/test/fixtures/react-jsx-export/src/components/ListExportTest.jsx b/packages/astro/test/fixtures/react-jsx-export/src/components/ListExportTest.jsx new file mode 100644 index 000000000..e695821d1 --- /dev/null +++ b/packages/astro/test/fixtures/react-jsx-export/src/components/ListExportTest.jsx @@ -0,0 +1,26 @@ +import { useState } from "react" + +const ListExport = () => { + const [example] = useState('Example') + return

{example}

+} + +export {ListExport} + +const OriginListExport = () => { + const [example] = useState('Example') + return

{example}

+} + +export { + OriginListExport as RenamedListExport +} + +const ListAsDefaultExport = () => { + const [example] = useState('Example') + return

{example}

+} + +export { + ListAsDefaultExport as default +} \ No newline at end of file diff --git a/packages/astro/test/fixtures/react-jsx-export/src/components/defaultExport/AnonymousArrowDefaultExport.jsx b/packages/astro/test/fixtures/react-jsx-export/src/components/defaultExport/AnonymousArrowDefaultExport.jsx new file mode 100644 index 000000000..7fe5ad1e5 --- /dev/null +++ b/packages/astro/test/fixtures/react-jsx-export/src/components/defaultExport/AnonymousArrowDefaultExport.jsx @@ -0,0 +1,6 @@ +import { useState } from "react" + +export default () => { + const [example] = useState('Example') + return

{example}

+} \ No newline at end of file diff --git a/packages/astro/test/fixtures/react-jsx-export/src/components/defaultExport/AnonymousFunctionDefaultExport.jsx b/packages/astro/test/fixtures/react-jsx-export/src/components/defaultExport/AnonymousFunctionDefaultExport.jsx new file mode 100644 index 000000000..eed9c6579 --- /dev/null +++ b/packages/astro/test/fixtures/react-jsx-export/src/components/defaultExport/AnonymousFunctionDefaultExport.jsx @@ -0,0 +1,6 @@ +import { useState } from "react" + +export default function() { + const [example] = useState('Example') + return

{example}

+} \ No newline at end of file diff --git a/packages/astro/test/fixtures/react-jsx-export/src/components/defaultExport/NamedArrowDefaultExport.jsx b/packages/astro/test/fixtures/react-jsx-export/src/components/defaultExport/NamedArrowDefaultExport.jsx new file mode 100644 index 000000000..c859808bc --- /dev/null +++ b/packages/astro/test/fixtures/react-jsx-export/src/components/defaultExport/NamedArrowDefaultExport.jsx @@ -0,0 +1,8 @@ +import { useState } from "react"; + +const NamedArrowDefaultExport = () => { + const [example] = useState('Example') + return

{example}

+} + +export default NamedArrowDefaultExport; \ No newline at end of file diff --git a/packages/astro/test/fixtures/react-jsx-export/src/components/defaultExport/NamedFunctionDefaultExport.jsx b/packages/astro/test/fixtures/react-jsx-export/src/components/defaultExport/NamedFunctionDefaultExport.jsx new file mode 100644 index 000000000..8b3897409 --- /dev/null +++ b/packages/astro/test/fixtures/react-jsx-export/src/components/defaultExport/NamedFunctionDefaultExport.jsx @@ -0,0 +1,6 @@ +import { useState } from "react" + +export default function NamedFunctionDefaultExport() { + const [example] = useState('Example') + return

{example}

+} \ No newline at end of file diff --git a/packages/astro/test/fixtures/react-jsx-export/src/pages/index.astro b/packages/astro/test/fixtures/react-jsx-export/src/pages/index.astro new file mode 100644 index 000000000..92a3833a4 --- /dev/null +++ b/packages/astro/test/fixtures/react-jsx-export/src/pages/index.astro @@ -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' +--- + +

React JSX Export Test

+ + + + + + + + + + + + + diff --git a/packages/astro/test/react-jsx-export.test.js b/packages/astro/test/react-jsx-export.test.js new file mode 100644 index 000000000..4a5b951b2 --- /dev/null +++ b/packages/astro/test/react-jsx-export.test.js @@ -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; + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0eb9602f5..6e92f7572 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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:'