这是indexloc提供的服务,不要输入任何密码
Skip to content

Create tuono html tags package #69

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
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
72 changes: 36 additions & 36 deletions examples/tutorial/src/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,48 +3,48 @@ import { Head, type TuonoProps } from 'tuono'
import PokemonLink from '../components/PokemonLink'

interface Pokemon {
name: string
name: string
}

interface IndexProps {
results: Pokemon[]
results: Pokemon[]
}

export default function IndexPage({
data,
data,
}: TuonoProps<IndexProps>): JSX.Element {
if (!data?.results) {
return <></>
}
if (!data?.results) {
return <></>
}

return (
<>
<Head>
<title>Tuono tutorial</title>
</Head>
<header className="header">
<a href="https://crates.io/crates/tuono" target="_blank">
Crates
</a>
<a href="https://www.npmjs.com/package/tuono" target="_blank">
Npm
</a>
</header>
<div className="title-wrap">
<h1 className="title">
TU<span>O</span>NO
</h1>
<div className="logo">
<img src="rust.svg" className="rust" />
<img src="react.svg" className="react" />
</div>
</div>
<ul style={{ flexWrap: 'wrap', display: 'flex', gap: 10 }}>
<PokemonLink pokemon={{ name: 'GOAT' }} id={0} />
{data.results.map((pokemon, i) => {
return <PokemonLink pokemon={pokemon} id={i + 1} key={i} />
})}
</ul>
</>
)
return (
<>
<Head>
<title>Tuono tutorial</title>
</Head>
<header className="header">
<a href="https://crates.io/crates/tuono" target="_blank">
Crates
</a>
<a href="https://www.npmjs.com/package/tuono" target="_blank">
Npm
</a>
</header>
<div className="title-wrap">
<h1 className="title">
TU<span>O</span>NO
</h1>
<div className="logo">
<img src="rust.svg" className="rust" />
<img src="react.svg" className="react" />
</div>
</div>
<ul style={{ flexWrap: 'wrap', display: 'flex', gap: 10 }}>
<PokemonLink pokemon={{ name: 'GOAT' }} id={0} />
{data.results.map((pokemon, i) => {
return <PokemonLink pokemon={pokemon} id={i + 1} key={i} />
})}
</ul>
</>
)
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
},
"workspaces": [
"tuono",
"tuono-html-tags",
"tuono-lazy-fn-vite-plugin",
"tuono-fs-router-vite-plugin",
"tuono-router"
Expand Down
9 changes: 9 additions & 0 deletions packages/html-tags/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# tuono-html-tags

This is the `tuono` package that holds the logic of the `Head` and `Html` components.

Check [tuono](https://tuono.dev) for more.

## Credits

This package is heavily inspired by [s-yadav/react-meta-tags](https://github.com/s-yadav/react-meta-tags).
63 changes: 63 additions & 0 deletions packages/html-tags/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
{
"name": "tuono-html-tags",
"version": "0.10.4",
"description": "Tuono package that handles the HTML tags",
"scripts": {
"dev": "vite build --watch",
"build": "vite build",
"lint": "eslint --ext .ts,.tsx ./src -c ../../.eslintrc",
"format": "prettier -u --write --ignore-unknown '**/*'",
"format:check": "prettier --check --ignore-unknown '**/*'",
"types": "tsc --noEmit",
"test:watch": "vitest",
"test": "vitest run"
},
"keywords": [],
"author": "Valerio Ageno",
"license": "MIT",
"type": "module",
"types": "dist/esm/index.d.ts",
"main": "dist/cjs/index.cjs",
"module": "dist/esm/index.js",
"files": [
"dist",
"src",
"README.md"
],
"exports": {
"./server": {
"import": {
"types": "./dist/esm/server/index.d.ts",
"default": "./dist/esm/server/index.js"
},
"require": {
"types": "./dist/cjs/server/index.d.ts",
"default": "./dist/cjs/server/index.js"
}
},
".": {
"import": {
"types": "./dist/esm/index.d.ts",
"default": "./dist/esm/index.js"
},
"require": {
"types": "./dist/cjs/index.d.cts",
"default": "./dist/cjs/index.cjs"
}
},
"./package.json": "./package.json"
},
"peerDependencies": {
"react": ">=16.3.0",
"react-dom": ">=16.3.0"
},
"devDependencies": {
"@tanstack/config": "^0.7.11",
"@testing-library/jest-dom": "^6.6.0",
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.5.2",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"vitest": "^2.0.0"
}
}
5 changes: 5 additions & 0 deletions packages/html-tags/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import MetaTagsContext from './meta-tags-context'
import MetaTags from './meta-tags'

export default MetaTags
export { MetaTags, MetaTagsContext }
33 changes: 33 additions & 0 deletions packages/html-tags/src/meta-tags-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import React, { createContext, useContext } from 'react'
import type { ReactNode } from 'react'

type ExtractFn = (elements: ReactNode) => void

interface MetaTagsContextValues {
extract?: ExtractFn
}

const MetaContext = createContext<MetaTagsContextValues>({})

interface MetaTagsContextProps {
children: ReactNode
extract?: ExtractFn
}

export default function MetaContextProvider({
extract,
children,
}: MetaTagsContextProps): ReactNode {
return (
<MetaContext.Provider
value={{
extract,
}}
>
{children}
</MetaContext.Provider>
)
}

export const useMetaTagsContext = (): MetaTagsContextValues =>
useContext(MetaContext)
77 changes: 77 additions & 0 deletions packages/html-tags/src/meta-tags.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import * as React from 'react'
import { afterEach, describe, expect, test } from 'vitest'
import { cleanup, render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'

import '@testing-library/jest-dom'
import MetaContextProvider from './meta-tags-context'
import MetaTags from './meta-tags'

const MockRouter = (): React.ReactNode => {
const [showRoute, setShowRoute] = React.useState(false)

return (
<MetaContextProvider>
<button onClick={() => setShowRoute((st) => !st)}>Toggle route</button>
{showRoute ? (
<MetaTags>
<title>Updated title</title>
</MetaTags>
) : (
<MetaTags>
<title>Tuono</title>
</MetaTags>
)}
</MetaContextProvider>
)
}

describe('Should correctly render the head tags', () => {
afterEach(() => {
cleanup()
})

test('It should correctly render the head element', async () => {
render(
<MetaContextProvider>
<MetaTags>
<title>Tuono</title>
</MetaTags>
</MetaContextProvider>,
)
expect(document.title).toEqual('Tuono')
})

test('It should remove the properties when unmount', async () => {
const { unmount } = render(
<MetaContextProvider>
<MetaTags>
<title>Tuono</title>
</MetaTags>
</MetaContextProvider>,
)
expect(document.title).toEqual('Tuono')

unmount()

expect(document.title).toEqual('')
})

test('It should update the existing values with the newly mounted', async () => {
const user = userEvent.setup()
render(<MockRouter />)

expect(document.title).toEqual('Tuono')

await user.click(screen.getByText('Toggle route'))
await waitFor(() => {
expect(document.title).toEqual('Updated title')
})

await user.click(screen.getByText('Toggle route'))

await waitFor(() => {
expect(document.title).toEqual('Tuono')
})
})
})
73 changes: 73 additions & 0 deletions packages/html-tags/src/meta-tags.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import React, { useEffect, useRef, useState } from 'react'
import type { ReactNode } from 'react'
import { useMetaTagsContext } from './meta-tags-context'
import {
appendChild,
getDuplicateTitle,
removeChild,
getDuplicateMeta,
getDuplicateCanonical,
getDuplicateElementById,
} from './utils'

interface MetaTagsProps {
children: ReactNode
}

export default function MetaTags({ children }: MetaTagsProps): ReactNode {
const elementRef = useRef<HTMLDivElement>(null)
const { extract } = useMetaTagsContext()
const [isClient, setIsClient] = useState<boolean>(false)

const handleChildrens = (): void => {
if (extract || !children) return

let childNodes = Array.prototype.slice.call(elementRef.current?.children)

const head = document.head
const headHtml = head.innerHTML

//filter children remove if children has not been changed
childNodes = childNodes.filter((child) => {
return headHtml.indexOf(child.outerHTML) === -1
})

//create clone of childNodes
childNodes = childNodes.map((child) => child.cloneNode(true))

//remove duplicate title and meta from head
childNodes.forEach((child) => {
const tag = child.tagName.toLowerCase()
if (tag === 'title') {
const title = getDuplicateTitle()
if (title.length > 0) removeChild(head, title)
} else if (child.id) {
// if the element has id defined remove the existing element with that id
const elm = getDuplicateElementById(child)
if (elm.length > 0) removeChild(head, elm)
} else if (tag === 'meta') {
const meta = getDuplicateMeta(child)
if (meta.length > 0) removeChild(head, meta)
} else if (tag === 'link' && child.rel === 'canonical') {
const link = getDuplicateCanonical()
if (link.length > 0) removeChild(head, link)
}
})

appendChild(document.head, childNodes)
}

useEffect(() => {
handleChildrens()
}, [handleChildrens, isClient])

useEffect(() => {
setIsClient(true)
}, [setIsClient])

if (!isClient && extract) {
extract(children)
}

return <div ref={elementRef}>{isClient ? children : <></>}</div>
}
3 changes: 3 additions & 0 deletions packages/html-tags/src/server/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import MetaTagsServer from './meta-tags-server'

export default MetaTagsServer
26 changes: 26 additions & 0 deletions packages/html-tags/src/server/meta-tags-server.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import * as React from 'react'
import { afterEach, describe, expect, test } from 'vitest'
import { cleanup, render } from '@testing-library/react'
import '@testing-library/jest-dom'
import MetaContextProvider from '../meta-tags-context'
import MetaTagsServer from './meta-tags-server'
import MetaTags from '../meta-tags'

describe('Should correctly render the head tags', () => {
afterEach(() => {
cleanup()
})

test('It should correctly render the html elements', () => {
const metaTagsServer = MetaTagsServer()
render(
<MetaContextProvider extract={metaTagsServer.extract}>
<MetaTags>
<title>Tuono</title>
</MetaTags>
</MetaContextProvider>,
)

expect(metaTagsServer.renderToString()).toBe('<title>Tuono</title>')
})
})
Loading
Loading