├── .gitattributes ├── docs ├── .gitignore ├── styles.css ├── public │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── apple-touch-icon.png │ ├── logo-sm-black-white.png │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ └── site.webmanifest ├── pages │ ├── _app.js │ ├── advanced │ │ ├── _meta.json │ │ ├── slot-function.mdx │ │ ├── multiple-override-node.mdx │ │ └── composing-slotted-components.mdx │ ├── _meta.json │ ├── tutorial │ │ ├── _meta.json │ │ ├── node-prop.mdx │ │ ├── template-as-prop.mdx │ │ ├── enforcing-node-type.mdx │ │ ├── templates.mdx │ │ ├── slot-and-has-slot.mdx │ │ ├── manipulating-slot-content.mdx │ │ ├── type-safety.mdx │ │ ├── overriding-props.mdx │ │ └── slot-pattern.mdx │ ├── troubleshooting.mdx │ ├── recommendations.mdx │ ├── caveats.mdx │ └── installation.mdx ├── next.config.js ├── next-env.d.ts ├── package.json └── tsconfig.json ├── .prettierrc.json ├── examples └── vite │ ├── src │ ├── vite-env.d.ts │ ├── main.tsx │ ├── components │ │ ├── dialog │ │ │ ├── DialogTriggerContent.tsx │ │ │ ├── Dialog.tsx │ │ │ └── DialogTrigger.tsx │ │ ├── button │ │ │ └── Button.tsx │ │ └── accordion │ │ │ ├── Accordion.tsx │ │ │ └── AccordionList.tsx │ ├── App.tsx │ ├── hooks │ │ └── useStateControl.ts │ └── App.css │ ├── tsconfig.node.json │ ├── vite.config.ts │ ├── .gitignore │ ├── index.html │ ├── .eslintrc.cjs │ ├── tsconfig.json │ ├── package.json │ ├── README.md │ └── public │ └── vite.svg ├── .gitignore ├── .vscode └── settings.json ├── pnpm-workspace.yaml ├── packages ├── unplugin-transform-react-slots │ ├── global.d.ts │ ├── src │ │ ├── esbuild.ts │ │ ├── rollup.ts │ │ ├── vite.ts │ │ ├── webpack.ts │ │ ├── core │ │ │ └── options.ts │ │ └── index.ts │ ├── README.md │ ├── test │ │ ├── entries │ │ │ ├── working.jsx │ │ │ ├── working-ts.tsx │ │ │ ├── disabled-with-pragma.jsx │ │ │ ├── disabled-no-import.jsx │ │ │ ├── disabled-no-import-ts.tsx │ │ │ └── disabled-with-pragma-ts.tsx │ │ ├── esbuild │ │ │ ├── esbuild.config.js │ │ │ └── esbuild.test.ts │ │ ├── vite │ │ │ ├── vite.config.js │ │ │ └── vite.test.ts │ │ ├── bundleTests.ts │ │ └── rollup │ │ │ ├── rollup.config.js │ │ │ └── rollup.test.ts │ ├── tsup.config.ts │ ├── tsconfig.json │ ├── CHANGELOG.md │ └── package.json ├── babel-plugin-transform-react-slots │ ├── test │ │ ├── fixtures │ │ │ ├── throw-when copy │ │ │ │ ├── options.js │ │ │ │ └── code.js │ │ │ ├── throw-when-slottable-parent-called │ │ │ │ ├── options.js │ │ │ │ └── code.js │ │ │ ├── throw-when-useSlot-parent-called │ │ │ │ ├── options.js │ │ │ │ └── code.js │ │ │ ├── throw-when-var-used-on-slottable │ │ │ │ ├── options.js │ │ │ │ └── code.js │ │ │ ├── throw-when-var-used-on-useSlot │ │ │ │ ├── options.js │ │ │ │ └── code.js │ │ │ ├── throw-when-bad-member-expression-on-useSlot │ │ │ │ ├── options.js │ │ │ │ ├── code.js │ │ │ │ └── throw-when-bad-member-expression-on-useSlot-2 │ │ │ │ │ ├── options.js │ │ │ │ │ └── code.js │ │ │ ├── throw-when-bad-member-expression-on-slottable-2 │ │ │ │ ├── options.js │ │ │ │ └── code.js │ │ │ ├── throw-when-bad-member-expression-on-slottable │ │ │ │ ├── options.js │ │ │ │ └── code.js │ │ │ ├── throw-when-bad-member-expression-on-useSlot-2 │ │ │ │ ├── options.js │ │ │ │ └── code.js │ │ │ ├── throw-when-useSlot-passed-as-function-argument │ │ │ │ ├── options.js │ │ │ │ └── code.js │ │ │ ├── throw-when-useSlot-parent-used-as-function-argument │ │ │ │ ├── options.js │ │ │ │ └── code.js │ │ │ ├── throw-when-slottable-passed-as-function-argument │ │ │ │ ├── code.ts │ │ │ │ └── options.js │ │ │ ├── throw-when-useSlot-passed-as-function-argument-ts │ │ │ │ ├── code.ts │ │ │ │ └── options.js │ │ │ ├── all-allowed-syntax-ts │ │ │ │ ├── options.js │ │ │ │ ├── code.tsx │ │ │ │ └── output.mjs │ │ │ └── all-allowed-syntax-js │ │ │ │ ├── code.jsx │ │ │ │ └── output.mjs │ │ └── plugin-tester.test.ts │ ├── global.d.ts │ ├── README.md │ ├── src │ │ ├── rawCodeStore.ts │ │ ├── errors.ts │ │ ├── utils.ts │ │ └── index.ts │ ├── CHANGELOG.md │ ├── tsconfig.json │ └── package.json └── react-slots │ ├── src │ ├── HiddenArg.ts │ ├── index.ts │ ├── typeUtils.ts │ ├── constants.ts │ ├── forEachNode.ts │ ├── template.ts │ ├── typeGuards.ts │ ├── useSlot.ts │ ├── children.tsx │ ├── types.ts │ └── OverrideNode.tsx │ ├── tsconfig.json │ ├── LICENSE │ ├── package.json │ ├── CHANGELOG.md │ └── README.md ├── .prettierignore ├── .changeset ├── config.json └── README.md ├── .github └── workflows │ ├── main.yml │ └── publish.yml ├── package.json └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | .next 2 | node_modules 3 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { "useTabs": false, "proseWrap": "always" } 2 | -------------------------------------------------------------------------------- /docs/styles.css: -------------------------------------------------------------------------------- 1 | details > * { 2 | padding: 0.5rem !important; 3 | } 4 | -------------------------------------------------------------------------------- /examples/vite/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules/ 3 | 4 | # production 5 | dist/ 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules\\typescript\\lib" 3 | } 4 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/*" 3 | - "examples/*" 4 | - "docs" 5 | -------------------------------------------------------------------------------- /docs/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flammae/react-slots/HEAD/docs/public/favicon.ico -------------------------------------------------------------------------------- /packages/unplugin-transform-react-slots/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@babel/plugin-syntax-typescript"; 2 | -------------------------------------------------------------------------------- /docs/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flammae/react-slots/HEAD/docs/public/favicon-16x16.png -------------------------------------------------------------------------------- /docs/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flammae/react-slots/HEAD/docs/public/favicon-32x32.png -------------------------------------------------------------------------------- /docs/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flammae/react-slots/HEAD/docs/public/apple-touch-icon.png -------------------------------------------------------------------------------- /docs/public/logo-sm-black-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flammae/react-slots/HEAD/docs/public/logo-sm-black-white.png -------------------------------------------------------------------------------- /packages/unplugin-transform-react-slots/src/esbuild.ts: -------------------------------------------------------------------------------- 1 | import unplugin from "."; 2 | 3 | export default unplugin.esbuild; 4 | -------------------------------------------------------------------------------- /packages/unplugin-transform-react-slots/src/rollup.ts: -------------------------------------------------------------------------------- 1 | import unplugin from "."; 2 | 3 | export default unplugin.rollup; 4 | -------------------------------------------------------------------------------- /docs/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flammae/react-slots/HEAD/docs/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /docs/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flammae/react-slots/HEAD/docs/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /packages/babel-plugin-transform-react-slots/test/fixtures/throw-when copy/options.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | throws: true, 3 | }; 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Ignore artifacts: 2 | build 3 | fixtures 4 | # # A bug when using code block inside tab elements 5 | # docs/pages/index.mdx -------------------------------------------------------------------------------- /packages/babel-plugin-transform-react-slots/test/fixtures/throw-when-slottable-parent-called/options.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | throws: true, 3 | }; 4 | -------------------------------------------------------------------------------- /packages/babel-plugin-transform-react-slots/test/fixtures/throw-when-useSlot-parent-called/options.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | throws: true, 3 | }; 4 | -------------------------------------------------------------------------------- /packages/babel-plugin-transform-react-slots/test/fixtures/throw-when-var-used-on-slottable/options.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | throws: true, 3 | }; 4 | -------------------------------------------------------------------------------- /packages/babel-plugin-transform-react-slots/test/fixtures/throw-when-var-used-on-useSlot/options.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | throws: true, 3 | }; 4 | -------------------------------------------------------------------------------- /packages/react-slots/src/HiddenArg.ts: -------------------------------------------------------------------------------- 1 | export class HiddenArg { 2 | arg: T; 3 | constructor(arg: T) { 4 | this.arg = arg; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /packages/babel-plugin-transform-react-slots/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@babel/plugin-syntax-typescript"; 2 | declare module "@babel/plugin-syntax-jsx"; 3 | -------------------------------------------------------------------------------- /docs/pages/_app.js: -------------------------------------------------------------------------------- 1 | import "../styles.css"; 2 | 3 | export default function MyApp({ Component, pageProps }) { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /packages/babel-plugin-transform-react-slots/test/fixtures/throw-when-bad-member-expression-on-useSlot/options.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | throws: true, 3 | }; 4 | -------------------------------------------------------------------------------- /packages/babel-plugin-transform-react-slots/test/fixtures/throw-when-bad-member-expression-on-slottable-2/options.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | throws: true, 3 | }; 4 | -------------------------------------------------------------------------------- /packages/babel-plugin-transform-react-slots/test/fixtures/throw-when-bad-member-expression-on-slottable/options.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | throws: true, 3 | }; 4 | -------------------------------------------------------------------------------- /packages/babel-plugin-transform-react-slots/test/fixtures/throw-when-bad-member-expression-on-useSlot-2/options.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | throws: true, 3 | }; 4 | -------------------------------------------------------------------------------- /packages/babel-plugin-transform-react-slots/test/fixtures/throw-when-useSlot-passed-as-function-argument/options.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | throws: true, 3 | }; 4 | -------------------------------------------------------------------------------- /packages/babel-plugin-transform-react-slots/test/fixtures/throw-when-useSlot-parent-used-as-function-argument/options.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | throws: true, 3 | }; 4 | -------------------------------------------------------------------------------- /packages/babel-plugin-transform-react-slots/test/fixtures/throw-when-useSlot-parent-called/code.js: -------------------------------------------------------------------------------- 1 | import * as ReactSlots from "@beqa/react-slots"; 2 | 3 | ReactSlots(); 4 | -------------------------------------------------------------------------------- /packages/babel-plugin-transform-react-slots/test/fixtures/throw-when-var-used-on-useSlot/code.js: -------------------------------------------------------------------------------- 1 | import * as ReactSlots from "@beqa/react-slots"; 2 | 3 | var x = ReactSlots; 4 | -------------------------------------------------------------------------------- /docs/next.config.js: -------------------------------------------------------------------------------- 1 | const withNextra = require('nextra')({ 2 | theme: 'nextra-theme-docs', 3 | themeConfig: './theme.config.tsx', 4 | }) 5 | 6 | module.exports = withNextra() 7 | -------------------------------------------------------------------------------- /packages/babel-plugin-transform-react-slots/test/fixtures/throw-when copy/code.js: -------------------------------------------------------------------------------- 1 | import * as ReactSlots from "@beqa/react-slots"; 2 | 3 | let x = ReactSlots["use" + "Slot"]; 4 | -------------------------------------------------------------------------------- /packages/babel-plugin-transform-react-slots/test/fixtures/throw-when-useSlot-passed-as-function-argument/code.js: -------------------------------------------------------------------------------- 1 | import { useSlot } from "@beqa/react-slots"; 2 | 3 | x(useSlot); 4 | -------------------------------------------------------------------------------- /packages/unplugin-transform-react-slots/README.md: -------------------------------------------------------------------------------- 1 | ### Vite, Rollup, esbuild plugin for @beqa/react-slots 2 | 3 | Read the [@beqa/react-slots docs](https://github.com/Flammae/react-slots) 4 | -------------------------------------------------------------------------------- /packages/babel-plugin-transform-react-slots/README.md: -------------------------------------------------------------------------------- 1 | ### babel transformation plugin for @beqa/react-slots 2 | 3 | Read the [@beqa/react-slots docs](https://github.com/Flammae/react-slots) 4 | -------------------------------------------------------------------------------- /packages/babel-plugin-transform-react-slots/test/fixtures/throw-when-useSlot-parent-used-as-function-argument/code.js: -------------------------------------------------------------------------------- 1 | import * as ReactSlots from "@beqa/react-slots"; 2 | 3 | x(ReactSlots); 4 | -------------------------------------------------------------------------------- /packages/babel-plugin-transform-react-slots/test/fixtures/throw-when-var-used-on-slottable/code.js: -------------------------------------------------------------------------------- 1 | import * as ReactSlots from "@beqa/react-slots"; 2 | 3 | let x = ReactSlots["use" + "Slot"]; 4 | -------------------------------------------------------------------------------- /packages/babel-plugin-transform-react-slots/test/fixtures/throw-when-bad-member-expression-on-slottable/code.js: -------------------------------------------------------------------------------- 1 | import * as ReactSlots from "@beqa/react-slots"; 2 | 3 | var x = ReactSlots.useSlot(); 4 | -------------------------------------------------------------------------------- /packages/babel-plugin-transform-react-slots/test/fixtures/throw-when-bad-member-expression-on-useSlot/code.js: -------------------------------------------------------------------------------- 1 | import * as ReactSlots from "@beqa/react-slots"; 2 | 3 | let x = ReactSlots["use" + "Slot"]; 4 | -------------------------------------------------------------------------------- /packages/babel-plugin-transform-react-slots/test/fixtures/throw-when-bad-member-expression-on-useSlot/throw-when-bad-member-expression-on-useSlot-2/options.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | throws: true, 3 | }; 4 | -------------------------------------------------------------------------------- /packages/babel-plugin-transform-react-slots/test/fixtures/throw-when-bad-member-expression-on-useSlot-2/code.js: -------------------------------------------------------------------------------- 1 | import * as ReactSlots from "@beqa/react-slots"; 2 | let useSlot; 3 | 4 | let x = ReactSlots[useSlot]; 5 | -------------------------------------------------------------------------------- /packages/babel-plugin-transform-react-slots/test/fixtures/throw-when-slottable-parent-called/code.js: -------------------------------------------------------------------------------- 1 | import * as ReactSlots from "@beqa/react-slots"; 2 | 3 | let { slot } = ReactSlots.useSlot(); 4 | 5 | foo(slot.default); 6 | -------------------------------------------------------------------------------- /packages/babel-plugin-transform-react-slots/test/fixtures/throw-when-slottable-passed-as-function-argument/code.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | 3 | import { useSlot } from "@beqa/react-slots"; 4 | 5 | x(useSlot as any); 6 | -------------------------------------------------------------------------------- /packages/babel-plugin-transform-react-slots/test/fixtures/throw-when-useSlot-passed-as-function-argument-ts/code.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | 3 | import { useSlot } from "@beqa/react-slots"; 4 | 5 | x(useSlot as any); 6 | -------------------------------------------------------------------------------- /packages/babel-plugin-transform-react-slots/test/fixtures/throw-when-bad-member-expression-on-slottable-2/code.js: -------------------------------------------------------------------------------- 1 | import * as ReactSlots from "@beqa/react-slots"; 2 | 3 | const x = ReactSlots.useSlot(); 4 | 5 | x[someValue]; 6 | -------------------------------------------------------------------------------- /docs/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /packages/unplugin-transform-react-slots/test/entries/working.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { useSlot } from "@beqa/react-slots"; 3 | 4 | const { slot } = useSlot(); 5 | 6 | ; // should transform to slot.default(); 7 | -------------------------------------------------------------------------------- /packages/babel-plugin-transform-react-slots/src/rawCodeStore.ts: -------------------------------------------------------------------------------- 1 | let _rawCode: string; 2 | 3 | export default { 4 | get() { 5 | return _rawCode; 6 | }, 7 | 8 | set(rawCode: string) { 9 | return (_rawCode = rawCode); 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /packages/babel-plugin-transform-react-slots/test/fixtures/throw-when-bad-member-expression-on-useSlot/throw-when-bad-member-expression-on-useSlot-2/code.js: -------------------------------------------------------------------------------- 1 | import * as ReactSlots from "@beqa/react-slots"; 2 | let useSlot; 3 | 4 | let x = ReactSlots[useSlot]; 5 | -------------------------------------------------------------------------------- /packages/react-slots/src/index.ts: -------------------------------------------------------------------------------- 1 | export { useSlot } from "./useSlot"; 2 | export { template, createTemplate } from "./template"; 3 | export { OverrideNode } from "./OverrideNode"; 4 | export type { Slot, SlotChildren, CreateTemplate, CreateSlot } from "./types"; 5 | -------------------------------------------------------------------------------- /docs/pages/advanced/_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "multiple-override-node": "Multiple OverrideNode Elements", 3 | "composing-slotted-components": "Composing Slotted Components", 4 | "slot-function": "Slot Function", 5 | "slot-content-keys": "Caution: Slot Content Keys" 6 | } 7 | -------------------------------------------------------------------------------- /packages/unplugin-transform-react-slots/tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: ["./src/*.ts"], 5 | format: ["cjs", "esm"], 6 | target: "node16.14", 7 | clean: true, 8 | dts: true, 9 | }); 10 | -------------------------------------------------------------------------------- /examples/vite/src/main.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from "react-dom/client"; 2 | import App from "./App.tsx"; 3 | import Modal from "react-modal"; 4 | 5 | Modal.setAppElement(document.getElementById("root")!); 6 | ReactDOM.createRoot(document.getElementById("root")!).render(); 7 | -------------------------------------------------------------------------------- /docs/pages/_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "index": "Introduction", 3 | "installation": "Installation", 4 | "tutorial": "Tutorial", 5 | "recommendations": "Recommendations", 6 | "advanced": "Advanced", 7 | "caveats": "Caveats", 8 | "troubleshooting": "Troubleshooting" 9 | } 10 | -------------------------------------------------------------------------------- /packages/unplugin-transform-react-slots/test/entries/working-ts.tsx: -------------------------------------------------------------------------------- 1 | import { useSlot } from "@beqa/react-slots"; 2 | 3 | const { slot } = useSlot("children" as any); 4 | 5 | // @ts-ignore unchecked index access config 6 | ; // should transform to slot.default(); 7 | -------------------------------------------------------------------------------- /examples/vite/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /examples/vite/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | import unplugin from "@beqa/unplugin-transform-react-slots"; 4 | 5 | // https://vitejs.dev/config/ 6 | export default defineConfig({ 7 | plugins: [unplugin.vite(), react()], 8 | }); 9 | -------------------------------------------------------------------------------- /packages/react-slots/src/typeUtils.ts: -------------------------------------------------------------------------------- 1 | export type UnionToIntersection = ( 2 | U extends any ? (k: U) => void : never 3 | ) extends (k: infer I) => void 4 | ? I 5 | : never; 6 | 7 | export type Pretty = { 8 | [K in keyof T]: T[K]; 9 | } & {}; 10 | 11 | export type NoInfer = [T][T extends any ? 0 : never]; 12 | -------------------------------------------------------------------------------- /packages/unplugin-transform-react-slots/src/vite.ts: -------------------------------------------------------------------------------- 1 | import unplugin from "."; 2 | import { Options } from "./core/options"; 3 | 4 | // If no type assertion is used tsup throws: 5 | // The type of this expression cannot be named without a 'resolution-mode' assertion, 6 | // which is an unstable feature.s 7 | export default unplugin.vite as (options: Options) => any; 8 | -------------------------------------------------------------------------------- /examples/vite/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /packages/babel-plugin-transform-react-slots/test/plugin-tester.test.ts: -------------------------------------------------------------------------------- 1 | import { pluginTester } from "babel-plugin-tester"; 2 | import plugin from "../src/index"; 3 | import path from "path"; 4 | 5 | pluginTester({ 6 | plugin, 7 | fixtures: path.join(__dirname, "fixtures"), 8 | fixtureOutputExt: ".mjs", 9 | babelOptions: { 10 | parserOpts: { strictMode: true }, 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /packages/unplugin-transform-react-slots/test/entries/disabled-with-pragma.jsx: -------------------------------------------------------------------------------- 1 | /** Should not transform slot.default */ 2 | 3 | // Line comment before pragma is ok 4 | 5 | // @disable-transform-react-slots 6 | // @ts-ignore 7 | import * as React from "react"; 8 | // @ts-ignore 9 | import { useSlot } from "@beqa/react-slots"; 10 | 11 | const { slot } = useSlot(); 12 | 13 | ; 14 | -------------------------------------------------------------------------------- /packages/unplugin-transform-react-slots/test/entries/disabled-no-import.jsx: -------------------------------------------------------------------------------- 1 | // Checks that transformation is not done when useSlot is not imported 2 | import * as React from "react"; 3 | 4 | function useSlot() { 5 | return { 6 | slot: { 7 | default: function () { 8 | return
; 9 | }, 10 | }, 11 | }; 12 | } 13 | 14 | const { slot } = useSlot(); 15 | 16 | ; 17 | -------------------------------------------------------------------------------- /packages/unplugin-transform-react-slots/test/entries/disabled-no-import-ts.tsx: -------------------------------------------------------------------------------- 1 | // Checks that transformation is not done when useSlot is not imported 2 | import * as React from "react"; 3 | 4 | function useSlot() { 5 | return { 6 | slot: { 7 | default: function () { 8 | return
; 9 | }, 10 | }, 11 | }; 12 | } 13 | 14 | const { slot } = useSlot(); 15 | 16 | ; 17 | -------------------------------------------------------------------------------- /docs/pages/tutorial/_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "slot-pattern": "Slot Pattern", 3 | "slot-and-has-slot": "Slot Element and hasSlot", 4 | "templates": "Templates", 5 | "type-safety": "Type-Safety", 6 | "template-as-prop": "Template's as Prop", 7 | "manipulating-slot-content": "Manipulating Slot Content With OverrideNode", 8 | "enforcing-node-type": "Enforcing Node Type", 9 | "overriding-props": "Overriding Props" 10 | } 11 | -------------------------------------------------------------------------------- /examples/vite/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite + React + TS 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /packages/babel-plugin-transform-react-slots/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @beqa/babel-plugin-transform-react-slots 2 | 3 | ## 1.0.0 4 | 5 | ### Major Changes 6 | 7 | - V1 of @beqa/react-slots required to work. 8 | 9 | ## 0.6.2 10 | 11 | ### Patch Changes 12 | 13 | - Fixed the doc link 14 | 15 | ## 0.6.1 16 | 17 | ### Patch Changes 18 | 19 | - edb171a: Temp docs 20 | 21 | ## 0.6.0 22 | 23 | ### Minor Changes 24 | 25 | - initialized monorepo 26 | -------------------------------------------------------------------------------- /packages/unplugin-transform-react-slots/test/entries/disabled-with-pragma-ts.tsx: -------------------------------------------------------------------------------- 1 | /** Should not transform slot.default */ 2 | 3 | // Line comment before pragma is ok 4 | 5 | // @disable-transform-react-slots 6 | import * as React from "react"; 7 | import { type SlotChildren, type Slot, useSlot } from "@beqa/react-slots"; 8 | 9 | const { slot } = useSlot("children" as SlotChildren); 10 | 11 | if ("default" in slot) ; 12 | -------------------------------------------------------------------------------- /packages/babel-plugin-transform-react-slots/test/fixtures/all-allowed-syntax-ts/options.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | babelOptions: { 3 | presets: [ 4 | [ 5 | "@babel/preset-env", 6 | { 7 | // Leave import syntax alone 8 | modules: false, 9 | targets: "maintained node versions", 10 | }, 11 | ], 12 | ["@babel/preset-typescript", { allowDeclareFields: true }], 13 | ], 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /packages/unplugin-transform-react-slots/test/esbuild/esbuild.config.js: -------------------------------------------------------------------------------- 1 | import { build } from "esbuild"; 2 | import unplugin from "../../dist"; 3 | import glob from "tiny-glob"; 4 | 5 | async function main() { 6 | const entryPoints = await glob("../entries/*"); 7 | 8 | await build({ 9 | entryPoints, 10 | bundle: true, 11 | outdir: "./dist", 12 | external: ["*"], 13 | plugins: [unplugin.esbuild()], 14 | }); 15 | } 16 | 17 | main(); 18 | -------------------------------------------------------------------------------- /packages/unplugin-transform-react-slots/src/webpack.ts: -------------------------------------------------------------------------------- 1 | import unplugin from "."; 2 | import { Options } from "./core/options"; 3 | 4 | /* 5 | If no type assertion is used tsup throws: 6 | 7 | The inferred type of 'default' cannot be named without a reference to 8 | '.pnpm/webpack@5.89.0_esbuild@0.19.3/node_modules/webpack'. 9 | This is likely not portable. A type annotation is necessary. 10 | */ 11 | export default unplugin.webpack as (options: Options) => any; 12 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json", 3 | "changelog": "@changesets/cli/changelog", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [ 7 | [ 8 | "@beqa/babel-plugin-transform-react-slots", 9 | "@beqa/unplugin-transform-react-slots" 10 | ] 11 | ], 12 | "access": "public", 13 | "baseBranch": "main", 14 | "updateInternalDependencies": "patch", 15 | "ignore": [] 16 | } 17 | -------------------------------------------------------------------------------- /docs/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "beqa/react-slots", 3 | "short_name": "React Slots", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /packages/react-slots/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "target": "ESNext", 5 | "module": "Node16", 6 | "lib": ["es2022"], 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "moduleResolution": "Node16", 10 | "forceConsistentCasingInFileNames": true, 11 | "skipLibCheck": true, 12 | "noUnusedLocals": true, 13 | "noUncheckedIndexedAccess": false, 14 | "jsx": "preserve", 15 | "types": ["vitest/globals"] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/babel-plugin-transform-react-slots/test/fixtures/throw-when-slottable-passed-as-function-argument/options.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | throws: true, 3 | babelOptions: { 4 | presets: [ 5 | [ 6 | "@babel/preset-env", 7 | { 8 | // Leave import syntax alone 9 | modules: false, 10 | targets: "maintained node versions", 11 | }, 12 | ], 13 | ["@babel/preset-typescript", { allowDeclareFields: true }], 14 | ], 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /packages/babel-plugin-transform-react-slots/test/fixtures/throw-when-useSlot-passed-as-function-argument-ts/options.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | throws: true, 3 | babelOptions: { 4 | presets: [ 5 | [ 6 | "@babel/preset-env", 7 | { 8 | // Leave import syntax alone 9 | modules: false, 10 | targets: "maintained node versions", 11 | }, 12 | ], 13 | ["@babel/preset-typescript", { allowDeclareFields: true }], 14 | ], 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: ["*"] 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: pnpm/action-setup@v2 12 | with: 13 | version: 8 14 | - uses: actions/setup-node@v3 15 | with: 16 | node-version: 18.x 17 | cache: pnpm 18 | - run: pnpm install --frozen-lockfile 19 | - run: pnpm run build && pnpm run test && pnpm run typecheck 20 | -------------------------------------------------------------------------------- /packages/babel-plugin-transform-react-slots/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "target": "ESNext", 5 | "module": "Node16", 6 | "lib": ["es2022"], 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "moduleResolution": "Node16", 10 | "forceConsistentCasingInFileNames": true, 11 | "skipLibCheck": true, 12 | "noUnusedLocals": true, 13 | "noUncheckedIndexedAccess": true, 14 | "types": ["vitest/globals"], 15 | "jsx": "preserve" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /packages/unplugin-transform-react-slots/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noEmit": true, 4 | "target": "ESNext", 5 | "module": "Node16", 6 | "lib": ["es2022"], 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "moduleResolution": "Node16", 10 | "forceConsistentCasingInFileNames": true, 11 | "skipLibCheck": true, 12 | "noUnusedLocals": true, 13 | "noUncheckedIndexedAccess": true, 14 | "jsx": "preserve", 15 | "resolveJsonModule": true, 16 | "types": ["node"] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "private": true, 4 | "description": "@beqa/react-slots docs", 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "license": "MIT", 11 | "dependencies": { 12 | "next": "^13.0.6", 13 | "nextra": "latest", 14 | "nextra-theme-docs": "latest", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0" 17 | }, 18 | "devDependencies": { 19 | "@types/node": "18.11.10", 20 | "typescript": "^4.9.3" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/vite/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | "eslint:recommended", 6 | "plugin:@typescript-eslint/recommended", 7 | "plugin:react-hooks/recommended", 8 | ], 9 | ignorePatterns: ["dist", ".eslintrc.cjs"], 10 | parser: "@typescript-eslint/parser", 11 | plugins: ["react-refresh"], 12 | // rules: { 13 | // "react-refresh/only-export-components": [ 14 | // "warn", 15 | // { allowConstantExport: true }, 16 | // ], 17 | // }, 18 | }; 19 | -------------------------------------------------------------------------------- /packages/unplugin-transform-react-slots/test/vite/vite.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import unplugin from "../../dist"; 3 | import glob from "tiny-glob"; 4 | 5 | async function main() { 6 | const entry = await glob("../entries/*"); 7 | 8 | return /** @type {import('vite').UserConfig} */ ({ 9 | plugins: [unplugin.vite()], 10 | build: { 11 | lib: { 12 | entry, 13 | formats: ["es"], 14 | }, 15 | minify: false, 16 | outDir: "./dist", 17 | sourcemap: false, 18 | }, 19 | }); 20 | } 21 | 22 | export default main(); 23 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": false, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "incremental": true, 11 | "esModuleInterop": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "jsx": "preserve" 17 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/unplugin-transform-react-slots/src/core/options.ts: -------------------------------------------------------------------------------- 1 | type FilterPattern = RegExp | RegExp[]; 2 | 3 | export interface Options { 4 | include?: RegExp; 5 | exclude?: FilterPattern; 6 | } 7 | 8 | export const defaultInclude = 9 | /\.(js)|(jsx)|(cjs)|(cjsx)|(mjs)|(mjsx)|(tsx)|(ctsx)|(mtsx)/; 10 | 11 | export function resolveOption(options: Options = {}): Required { 12 | const include = options.include ?? defaultInclude; 13 | 14 | let exclude: FilterPattern = [/node_modules/]; 15 | if (options.exclude) { 16 | exclude = exclude.concat(options.exclude); 17 | } 18 | 19 | return { 20 | include, 21 | exclude, 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /examples/vite/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "react-jsx", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true 22 | }, 23 | "include": ["src"], 24 | "references": [{ "path": "./tsconfig.node.json" }] 25 | } 26 | -------------------------------------------------------------------------------- /packages/react-slots/src/constants.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | /** 4 | * Used for specifying slot name on a TemplateComponent. 5 | * We set it as a property on the function 6 | */ 7 | export const SLOT_NAME: unique symbol = Symbol("Slot name"); 8 | 9 | export const COMPONENT_TYPE: unique symbol = Symbol("Component type"); 10 | 11 | export const SLOT_TYPE_IDENTIFIER = "slot" as const; 12 | 13 | export const TEMPLATE_TYPE_IDENTIFIER = "template" as const; 14 | 15 | export const DEFAULT_TEMPLATE_AS = React.Fragment; 16 | 17 | export const DEFAULT_SLOT_NAME = "default"; 18 | 19 | export const SLOT_NAME_ATTR = "slot-name"; 20 | 21 | export type SlotNameAttr = typeof SLOT_NAME_ATTR; 22 | 23 | export type DefaultSlotName = typeof DEFAULT_SLOT_NAME; 24 | 25 | // dummy text 2 26 | -------------------------------------------------------------------------------- /examples/vite/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-example", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "devDependencies": { 13 | "@beqa/react-slots": "workspace:^", 14 | "@beqa/unplugin-transform-react-slots": "workspace:^", 15 | "@types/react-modal": "^3.16.2", 16 | "@typescript-eslint/eslint-plugin": "^6.0.0", 17 | "@typescript-eslint/parser": "^6.0.0", 18 | "@vitejs/plugin-react": "^4.1.0", 19 | "eslint": "^8.45.0", 20 | "eslint-plugin-react-hooks": "^4.6.0", 21 | "eslint-plugin-react-refresh": "^0.4.3" 22 | }, 23 | "dependencies": { 24 | "react-modal": "^3.16.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/unplugin-transform-react-slots/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @beqa/unplugin-transform-react-slots 2 | 3 | ## 1.0.0 4 | 5 | ### Major Changes 6 | 7 | - v1 of babel-plugin-transform-react-slots required to work 8 | 9 | ### Minor changes: 10 | 11 | - Webpack support 12 | 13 | ### Patch Changes 14 | 15 | - Updated dependencies 16 | - @beqa/babel-plugin-transform-react-slots@1.0.0 17 | 18 | ## 0.6.2 19 | 20 | ### Patch Changes 21 | 22 | - Fixed the doc link 23 | - Updated dependencies 24 | - @beqa/babel-plugin-transform-react-slots@0.6.2 25 | 26 | ## 0.6.1 27 | 28 | ### Patch Changes 29 | 30 | - 479d04c: changed wrong options.include type. It can't be an array 31 | - edb171a: Temp docs 32 | - Updated dependencies [edb171a] 33 | - @beqa/babel-plugin-transform-react-slots@0.6.1 34 | 35 | ## 0.6.0 36 | 37 | ### Minor Changes 38 | 39 | - initialized monorepo 40 | 41 | ### Patch Changes 42 | 43 | - Updated dependencies 44 | - @beqa/babel-plugin-transform-react-slots@0.6.0 45 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | on: 3 | workflow_run: 4 | workflows: [CI] 5 | branches: [main] 6 | types: [completed] 7 | 8 | concurrency: ${{ github.workflow }}-${{ github.ref }} 9 | 10 | permissions: 11 | contents: write 12 | pull-requests: write 13 | 14 | jobs: 15 | publish: 16 | if: ${{ github.event.workflow_run.conclusion == 'success'}} 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: pnpm/action-setup@v2 21 | with: 22 | version: 8 23 | - uses: actions/setup-node@v3 24 | with: 25 | node-version: 18.x 26 | cache: pnpm 27 | 28 | - run: pnpm install --frozen-lockfile 29 | - name: Create Release Pull Request or Publish 30 | uses: changesets/action@v1 31 | with: 32 | publish: pnpm run release 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 36 | -------------------------------------------------------------------------------- /examples/vite/src/components/dialog/DialogTriggerContent.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const defaultValue = { 4 | close: () => {}, 5 | titleId: "", 6 | }; 7 | 8 | type Props = typeof defaultValue & { children: React.ReactNode }; 9 | 10 | export const DialogTriggerContext = React.createContext< 11 | undefined | typeof defaultValue 12 | >(undefined); 13 | 14 | export function DialogTriggerContextProvider({ 15 | close, 16 | titleId, 17 | children, 18 | }: Props) { 19 | const value = React.useMemo(() => ({ close, titleId }), [close, titleId]); 20 | 21 | return ( 22 | 23 | {children} 24 | 25 | ); 26 | } 27 | 28 | export function useDialogTriggerContext() { 29 | const setIsOpen = React.useContext(DialogTriggerContext); 30 | 31 | if (!setIsOpen) { 32 | throw new Error("useDialogTriggerContext used outside context provider"); 33 | } 34 | 35 | return setIsOpen; 36 | } 37 | -------------------------------------------------------------------------------- /docs/pages/troubleshooting.mdx: -------------------------------------------------------------------------------- 1 | # Troubleshooting 2 | 3 | ## ``Unsupported syntax: `useSlot` or an object holding a nested `useSlot` value used inside ...`` 4 | 5 | If you encounter this error message after initializing the compile-time plugin 6 | for your project, it likely indicates that the plugin is applied after React 7 | elements have already been transpiled. To resolve this issue, you should adjust 8 | your configuration to ensure that the plugin runs before other syntax 9 | transformations. 10 | 11 | This error occurs when useSlot or the return value of useSlot is used in a way 12 | that could potentially mutate slots before they are used. It's important to note 13 | that this is a specific error related to the compile-time plugin. 14 | 15 | If you wish to disable the transformation for a specific file where this error 16 | occurs, you can add the following comment at the beginning of the file: 17 | 18 | ```js 19 | // @disable-transform-react-slots 20 | ``` 21 | 22 | After adding this comment, you should only use the function signature of slots 23 | in that file. 24 | -------------------------------------------------------------------------------- /packages/babel-plugin-transform-react-slots/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@beqa/babel-plugin-transform-react-slots", 3 | "version": "1.0.0", 4 | "description": "The JSX to slot function transpilation plugin for babel", 5 | "author": "Beqa", 6 | "license": "MIT", 7 | "keywords": [ 8 | "babel", 9 | "react-slots" 10 | ], 11 | "bugs": { 12 | "url": "https://github.com/Flammae/react-slots/issues" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/Flammae/react-slots.git" 17 | }, 18 | "publishConfig": { 19 | "access": "public" 20 | }, 21 | "main": "./dist/index.js", 22 | "module": "./dist/index.mjs", 23 | "types": "./dist/index.d.ts", 24 | "scripts": { 25 | "build": "tsup src/index.ts --format cjs,esm --dts", 26 | "test": "vitest run --globals", 27 | "test:watch": "vitest --globals", 28 | "typecheck": "tsc" 29 | }, 30 | "dependencies": { 31 | "@babel/code-frame": "^7.22.13", 32 | "@babel/core": "^7.23.0", 33 | "@babel/helper-plugin-utils": "^7.22.5", 34 | "@babel/plugin-syntax-jsx": "^7.22.5" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/unplugin-transform-react-slots/test/bundleTests.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from "child_process"; 2 | import glob from "tiny-glob"; 3 | 4 | async function main() { 5 | const testDir = await glob("./test/*", { absolute: true }); 6 | 7 | // const entries = testDir.find((val) => val.includes("entries")); 8 | const esbuild = testDir.find((val) => val.includes("esbuild")); 9 | const rollup = testDir.find((val) => val.includes("rollup")); 10 | const vite = testDir.find((val) => val.includes("vite")); 11 | 12 | console.log("\nEsbuild"); 13 | execSync("npx esbuild --version", { 14 | cwd: esbuild, 15 | stdio: "inherit", 16 | }); 17 | execSync("tsx esbuild.config.js", { cwd: esbuild, stdio: "inherit" }); 18 | 19 | console.log("\nRollup"); 20 | execSync("npx rollup --version", { cwd: rollup, stdio: "inherit" }); 21 | execSync("npx rollup -c --bundleConfigAsCjs", { 22 | cwd: rollup, 23 | stdio: "inherit", 24 | }); 25 | 26 | console.log("\nVite"); 27 | execSync("npx vite --version", { cwd: vite, stdio: "inherit" }); 28 | execSync("npx vite build", { cwd: vite, stdio: "inherit" }); 29 | } 30 | 31 | main(); 32 | -------------------------------------------------------------------------------- /packages/react-slots/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Beka Meqvabishvili 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/vite/src/components/button/Button.tsx: -------------------------------------------------------------------------------- 1 | import { Slot, SlotChildren, useSlot } from "@beqa/react-slots"; 2 | import * as React from "react"; 3 | 4 | type HTMLButtonProps = React.ButtonHTMLAttributes; 5 | 6 | type Props = Omit & { 7 | children: SlotChildren | Slot | Slot<"right">>; 8 | variant?: "primary" | "secondary"; 9 | onClick?: React.MouseEventHandler; 10 | className?: string; 11 | }; 12 | 13 | export default function Button({ 14 | children, 15 | variant = "primary", 16 | onClick, 17 | className, 18 | ...rest 19 | }: Props) { 20 | const { slot, hasSlot } = useSlot(children); 21 | return ( 22 | 41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /docs/pages/advanced/slot-function.mdx: -------------------------------------------------------------------------------- 1 | # Slot Function 2 | 3 | Slot elements are essentially functions and are transformed back into functions 4 | by the `transform-react-slots` plugin during build time. Whether you've chosen 5 | to enable the build-time plugin or not, you can always use slots as functions. 6 | 7 | You can provide fallback content to slot functions using the first argument and 8 | props with the second argument. Here are some examples showing slot elements and 9 | their equivalent slot functions: 10 | 11 | ```jsx 12 | ; 13 | // Is equivalent to: 14 | slot.default(); 15 | 16 | Fallback; 17 | // Is equivalent to: 18 | slot.foo("Fallback"); 19 | 20 | ; 21 | // Is equivalent to: 22 | slot.foo(null, { prop1: "foo", prop2: 42, ...spreadProps }); 23 | 24 | 25 | Fallback 1 26 | Fallback 2 27 | ; 28 | // Is equivalent to: 29 | slot.bar([Fallback 1, "Fallback 2"], { prop: 42 }); 30 | 31 | 32 | Fallback 33 | ; 34 | // Is equivalent to: 35 | slot.default("Fallback", { prop: "foo", key: 1 }); 36 | // It is also equivalent to: 37 | slot.default("Fallback", { prop: "foo" }, 1); 38 | ``` 39 | -------------------------------------------------------------------------------- /docs/pages/tutorial/node-prop.mdx: -------------------------------------------------------------------------------- 1 | # Tutorial: Overriding Node With the `node` Prop 2 | 3 | Sometimes you may need to do more with the provided node than simply overriding 4 | props. The `node` prop is designed to address advanced use cases. 5 | 6 | The `node` prop is a function that takes each allowed node as an argument and 7 | expects to return a new node. 8 | 9 | You can use the `node` function to: 10 | 11 | - Wrap a node in an element. 12 | - Change the type of a node. 13 | - Modify the props of an element. 14 | - Implement custom validation for the node. 15 | - Perform any operation that requires full access to the node. 16 | 17 | ## Examples 18 | 19 | ```jsx 20 | // Render each item for the default slot in a bullet list 21 | 22 | ( 25 |
  • 26 | 27 | {node} 28 |
  • 29 | )} 30 | /> 31 |
    ; 32 | 33 | // Throw an error if the provided node is not an intrinsic HTML element 34 | 35 | { 37 | if (typeof node === "object" && typeof node.type !== "string") { 38 | throw "The default slot must be an HTML element"; 39 | } 40 | return node; 41 | }} 42 | /> 43 | ; 44 | ``` 45 | -------------------------------------------------------------------------------- /packages/babel-plugin-transform-react-slots/src/errors.ts: -------------------------------------------------------------------------------- 1 | import { types as t } from "@babel/core"; 2 | import { codeFrameColumns } from "@babel/code-frame"; 3 | import rawCodeStore from "./rawCodeStore"; 4 | 5 | export class UnsupportedSyntaxError extends Error { 6 | codeFrame: string; 7 | 8 | constructor(location: t.SourceLocation | null | undefined) { 9 | super(); 10 | this.codeFrame = location 11 | ? codeFrameColumns(rawCodeStore.get(), location) 12 | : ""; 13 | } 14 | 15 | throw(message: string) { 16 | throw new Error( 17 | `${message}\n\n${this.codeFrame}. This restriction ensures slottable values remain unaltered until used as JSX elements. To prevent compilation for a specific file, add the comment \`disable-transform-react-slots\` at the file's top and use an alternative call signature for the slots.`, 18 | ); 19 | } 20 | } 21 | 22 | export class VarDeclarationError extends UnsupportedSyntaxError { 23 | kind: t.VariableDeclaration["kind"]; 24 | 25 | constructor( 26 | kind: t.VariableDeclaration["kind"], 27 | location: t.SourceLocation | null | undefined, 28 | ) { 29 | super(location); 30 | this.kind = kind; 31 | } 32 | } 33 | 34 | export class UnsupportedMemberExpressionError extends UnsupportedSyntaxError {} 35 | 36 | export class JSXNamespacedNameError extends UnsupportedSyntaxError {} 37 | -------------------------------------------------------------------------------- /examples/vite/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## Expanding the ESLint configuration 11 | 12 | If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: 13 | 14 | - Configure the top-level `parserOptions` property like this: 15 | 16 | ```js 17 | parserOptions: { 18 | ecmaVersion: 'latest', 19 | sourceType: 'module', 20 | project: ['./tsconfig.json', './tsconfig.node.json'], 21 | tsconfigRootDir: __dirname, 22 | }, 23 | ``` 24 | 25 | - Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` 26 | - Optionally add `plugin:@typescript-eslint/stylistic-type-checked` 27 | - Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list 28 | -------------------------------------------------------------------------------- /packages/react-slots/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@beqa/react-slots", 3 | "private": false, 4 | "version": "1.1.3", 5 | "description": "The react-slots runtime library", 6 | "author": "Beqa", 7 | "license": "MIT", 8 | "keywords": [ 9 | "react", 10 | "slots", 11 | "template", 12 | "useSlot", 13 | "slot-name", 14 | "slots in react", 15 | "vue slots in react", 16 | "svelte slots in react", 17 | "web component slots in react" 18 | ], 19 | "publishConfig": { 20 | "access": "public" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/Flammae/react-slots/issues" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/Flammae/react-slots.git" 28 | }, 29 | "main": "./dist/index.js", 30 | "module": "./dist/index.mjs", 31 | "types": "./dist/index.d.ts", 32 | "scripts": { 33 | "build": "tsup src/index.ts --format cjs,esm --dts", 34 | "release": "pnpm run build && changeset publish", 35 | "test": "vitest run --globals --environment jsdom", 36 | "test:watch": "vitest --globals --environment jsdom", 37 | "typecheck": "vitest typecheck --watch=false" 38 | }, 39 | "peerDependencies": { 40 | "react": ">=17", 41 | "react-dom": ">=17" 42 | }, 43 | "peerDependenciesMeta": { 44 | "react-dom": { 45 | "optional": true 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/react-slots/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @beqa/react-slots 2 | 3 | ## 1.1.2 4 | 5 | ### Patch Changes 6 | 7 | - Removed ts-toolbelt dependency 8 | 9 | ## 1.1.1 10 | 11 | ### Patch Changes 12 | 13 | - Fixed typo in the README 14 | 15 | ## 1.1.0 16 | 17 | ### Minor Changes 18 | 19 | - Changed enforce="throw" of OverrideNode to only throw in development 20 | 21 | ## 1.0.2 22 | 23 | ### Patch Changes 24 | 25 | - Added Examples for the README 26 | 27 | ## 1.0.1 28 | 29 | ### Patch Changes 30 | 31 | - 6311ae8: Fixed stringified functions displaying in an error 32 | 33 | ## 1.0.0 34 | 35 | ### Major Changes 36 | 37 | - Restricted children of Template's `as` prop to extend ReactNode. Previously it 38 | could be any type. 39 | - v1 of unplugin-transform-react-slots or babel-plugin-transform-react-slots 40 | required. 41 | 42 | ### Minor Changes: 43 | 44 | - Introduced OverrideNode element. 45 | - Allowed use of "slot-name" attribute on slot elements 46 | 47 | ### Patch changes 48 | 49 | - Empty arrays no longer count as provided content 50 | 51 | ## 0.6.3 52 | 53 | ### Patch Changes 54 | 55 | - Added introduction to the docs 56 | 57 | ## 0.6.2 58 | 59 | ### Patch Changes 60 | 61 | - Fixed the doc link 62 | 63 | ## 0.6.1 64 | 65 | ### Patch Changes 66 | 67 | - 111fc0a: BugFix: children not updating 68 | - edb171a: Temp docs 69 | 70 | ## 0.6.0 71 | 72 | ### Minor Changes 73 | 74 | - initialized monorepo 75 | - ec6f17a: Built the react-slots API 76 | -------------------------------------------------------------------------------- /examples/vite/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/react-slots/src/forEachNode.ts: -------------------------------------------------------------------------------- 1 | export function shouldDiscard(child: any): child is null | undefined | boolean { 2 | switch (child) { 3 | case null: 4 | case undefined: 5 | case true: 6 | case false: 7 | return true; 8 | default: { 9 | if (Array.isArray(child) && child.length === 0) { 10 | return true; 11 | } 12 | } 13 | } 14 | return false; 15 | } 16 | 17 | export function forEachNode( 18 | children: T, 19 | callback: (child: Exclude>) => void, 20 | ): void { 21 | if (shouldDiscard(children)) { 22 | return; 23 | } 24 | 25 | if ( 26 | typeof children !== "string" && 27 | typeof (children as any)?.[Symbol.iterator] === "function" 28 | ) { 29 | for (const child of children as any) { 30 | forEachNode(child, callback); 31 | } 32 | } else { 33 | callback(children as any); 34 | } 35 | } 36 | 37 | export function forEachNodeReplace( 38 | children: T, 39 | callback: (child: Exclude>) => T, 40 | ): T { 41 | if (shouldDiscard(children)) { 42 | return children; 43 | } 44 | 45 | let newChildren; 46 | 47 | if ( 48 | typeof children !== "string" && 49 | typeof (children as any)?.[Symbol.iterator] === "function" 50 | ) { 51 | newChildren = []; 52 | for (const child of children as any) { 53 | newChildren.push(forEachNodeReplace(child, callback)); 54 | } 55 | } else { 56 | newChildren = callback(children as any); 57 | } 58 | 59 | return newChildren as any; 60 | } 61 | -------------------------------------------------------------------------------- /examples/vite/src/components/accordion/Accordion.tsx: -------------------------------------------------------------------------------- 1 | import { Slot, SlotChildren, useSlot } from "@beqa/react-slots"; 2 | import * as React from "react"; 3 | import Button from "../button/Button"; 4 | import { useStateControl } from "../../hooks/useStateControl"; 5 | 6 | type Props = { 7 | children?: SlotChildren | Slot>; 8 | onToggle?: (nextIsOpen: boolean) => void; 9 | isOpen?: boolean; 10 | defaultIsOpen?: boolean; 11 | }; 12 | 13 | export function Accordion(props: Props) { 14 | const { slot } = useSlot(props.children); 15 | 16 | // Support both controlled and uncontrolled state 17 | const [isOpen, setIsOpen] = useStateControl( 18 | props.isOpen, 19 | props.defaultIsOpen, 20 | props.onToggle, 21 | ); 22 | 23 | const panelId = React.useId(); 24 | const buttonId = React.useId(); 25 | 26 | return ( 27 |
    28 | 41 |
    46 | {isOpen && } 47 |
    48 |
    49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "main": "index.js", 4 | "scripts": { 5 | "all": "pnpm run build && pnpm run test && pnpm run typecheck", 6 | "release": "pnpm run build && changeset publish", 7 | "build": "pnpm -F \"./packages/**\" run -r build", 8 | "test": "pnpm -F \"./packages/**\" run -r test", 9 | "typecheck": "pnpm -F \"./packages/**\" run -r typecheck" 10 | }, 11 | "keywords": [], 12 | "license": "MIT", 13 | "devDependencies": { 14 | "@babel/plugin-syntax-typescript": "^7.22.5", 15 | "@babel/preset-env": "^7.22.20", 16 | "@babel/preset-react": "^7.22.15", 17 | "@babel/preset-typescript": "^7.23.0", 18 | "@babel/traverse": "^7.23.0", 19 | "@babel/types": "^7.23.0", 20 | "@changesets/cli": "^2.26.2", 21 | "@rollup/plugin-babel": "^6.0.3", 22 | "@rollup/plugin-commonjs": "^25.0.4", 23 | "@rollup/plugin-node-resolve": "^15.2.1", 24 | "@testing-library/react": "^14.0.0", 25 | "@types/babel__code-frame": "^7.0.4", 26 | "@types/babel__core": "^7.20.2", 27 | "@types/babel__helper-plugin-utils": "^7.10.1", 28 | "@types/babel__traverse": "^7.20.2", 29 | "@types/node": "^20.7.0", 30 | "@types/react": "^18.2.23", 31 | "babel-plugin-tester": "^11.0.4", 32 | "esbuild": "0.19.3", 33 | "jsdom": "^22.1.0", 34 | "prettier": "3.0.3", 35 | "react": "^18.2.0", 36 | "react-dom": "^18.2.0", 37 | "rollup": "^3.29.3", 38 | "rollup-plugin-typescript2": "^0.36.0", 39 | "tiny-glob": "^0.2.9", 40 | "tsup": "^7.2.0", 41 | "tsx": "^3.13.0", 42 | "typescript": "^5.2.2", 43 | "vite": "^4.4.9", 44 | "vitest": "0.34.5" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/react-slots/src/template.ts: -------------------------------------------------------------------------------- 1 | import { 2 | COMPONENT_TYPE, 3 | SLOT_NAME, 4 | TEMPLATE_TYPE_IDENTIFIER, 5 | } from "./constants"; 6 | import type { TemplateComponent, CreateTemplate, SlotChildren } from "./types"; 7 | 8 | const templateFnCache: Map> = new Map(); 9 | 10 | /** 11 | * Gets TemplateComponent from cache or creates a new one. 12 | */ 13 | function getTemplateComponent( 14 | slotName: T, 15 | ): TemplateComponent { 16 | if (templateFnCache.has(slotName)) { 17 | return templateFnCache.get(slotName)!; 18 | } 19 | 20 | const TemplateComponent = Object.assign( 21 | () => { 22 | const name = String(TemplateComponent[SLOT_NAME]); 23 | throw new Error( 24 | `\`\` was rendered outside of \`useSlot()\`. \ 25 | Make sure the \`children\` is passed to \`useSlot\` as an argument, \ 26 | and that \`Template\` is a direct child (not nested in another element)`, 27 | ); 28 | }, 29 | { 30 | [COMPONENT_TYPE]: TEMPLATE_TYPE_IDENTIFIER, 31 | [SLOT_NAME]: slotName, 32 | }, 33 | ); 34 | 35 | templateFnCache.set(slotName, TemplateComponent); 36 | 37 | return TemplateComponent; 38 | } 39 | 40 | export const template = new Proxy({} as CreateTemplate, { 41 | get: (target, prop) => { 42 | if (typeof prop === "symbol") { 43 | return Reflect.get(target, prop); 44 | } 45 | // Whatever consumer provides as prop will be the new named template 46 | return getTemplateComponent(prop); 47 | }, 48 | }); 49 | 50 | /** Create Type-safe Template */ 51 | export function createTemplate() { 52 | return template as CreateTemplate; 53 | } 54 | -------------------------------------------------------------------------------- /examples/vite/src/components/accordion/AccordionList.tsx: -------------------------------------------------------------------------------- 1 | import { OverrideNode, SlotChildren, useSlot } from "@beqa/react-slots"; 2 | import * as React from "react"; 3 | import { Accordion } from "./Accordion"; 4 | import { useStateControl } from "../../hooks/useStateControl"; 5 | 6 | type Props = { 7 | children: SlotChildren; 8 | defaultIsOpen?: React.Key | null; 9 | isOpen?: React.Key | null; 10 | onToggle?: (openKey: React.Key | null) => void; 11 | }; 12 | 13 | export function AccordionList(props: Props) { 14 | const { slot } = useSlot(props.children); 15 | 16 | const [openKey, setOpenKey] = useStateControl( 17 | props.isOpen, 18 | props.defaultIsOpen, 19 | props.onToggle, 20 | ); 21 | 22 | return ( 23 |
    24 | 25 | { 31 | if (!el.key) { 32 | console.error( 33 | "When using AccordionList each Accordion should have a unique key", 34 | ); 35 | } 36 | 37 | // open selected Accordion 38 | return React.cloneElement(el, { 39 | defaultIsOpen: undefined, 40 | isOpen: !!openKey && el.key === openKey, 41 | onToggle(nextIsOpen: boolean) { 42 | console.log("toggle", nextIsOpen); 43 | setOpenKey(nextIsOpen ? el.key : null); 44 | }, 45 | }); 46 | }} 47 | /> 48 | 49 |
    50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /examples/vite/src/App.tsx: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | import { DialogTrigger } from "./components/dialog/DialogTrigger"; 3 | import Button from "./components/button/Button"; 4 | import Dialog from "./components/dialog/Dialog"; 5 | import { Accordion } from "./components/accordion/Accordion"; 6 | import { AccordionList } from "./components/accordion/AccordionList"; 7 | 8 | export default function App() { 9 | return ( 10 |
    11 | 12 | 13 | 14 | Look Ma, No External State 15 |

    ... And no event handlers.

    16 |

    Closes automatically on button click.

    17 |

    Can work with external state if desired.

    18 | 24 | 25 |
    26 |
    27 | 28 | 29 | 30 | First Accordion 31 | This part of Accordion is hidden 32 | 33 | 34 | Second Accordion 35 | AccordionList makes it so that only one Accordion is open at a time 36 | 37 | 38 | Third Accordion 39 | No external state required 40 | 41 | 42 |
    43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /packages/unplugin-transform-react-slots/test/rollup/rollup.config.js: -------------------------------------------------------------------------------- 1 | import unplugin from "../../dist/index.mjs"; 2 | import typescript from "rollup-plugin-typescript2"; 3 | import nodeResolve from "@rollup/plugin-node-resolve"; 4 | import commonjs from "@rollup/plugin-commonjs"; 5 | import babel from "@rollup/plugin-babel"; 6 | import glob from "tiny-glob"; 7 | 8 | async function main() { 9 | const jsxEntries = await glob("../entries/*.jsx"); 10 | 11 | const tsxEntries = await glob("../entries/*.tsx"); 12 | 13 | return [ 14 | { 15 | input: jsxEntries, 16 | output: { 17 | dir: "./dist", 18 | }, 19 | plugins: [ 20 | unplugin.rollup(), 21 | nodeResolve({ 22 | extensions: [".js"], 23 | }), 24 | babel({ 25 | babelHelpers: "bundled", 26 | presets: ["@babel/preset-react"], 27 | extensions: [".js", ".jsx"], 28 | }), 29 | commonjs(), 30 | ], 31 | external: ["react"], // For cleaner build. Real projects won't need to specify this 32 | }, 33 | { 34 | input: tsxEntries, 35 | output: { 36 | dir: "./dist", 37 | }, 38 | plugins: [ 39 | unplugin.rollup(), 40 | nodeResolve({ 41 | extensions: [".js"], 42 | }), 43 | commonjs(), 44 | typescript({ 45 | tsconfigOverride: { 46 | compilerOptions: { 47 | module: "ESNext", 48 | moduleResolution: "node10", 49 | jsx: "react", 50 | }, 51 | include: ["./test/entries/**.tsx"], 52 | }, 53 | }), 54 | ], 55 | external: ["react"], // For cleaner build. Real projects won't need to specify this 56 | }, 57 | ]; 58 | } 59 | 60 | export default main(); 61 | -------------------------------------------------------------------------------- /docs/pages/tutorial/template-as-prop.mdx: -------------------------------------------------------------------------------- 1 | import { Callout } from "nextra/components"; 2 | 3 | # Template `as` Prop 4 | 5 | By default, when a slot encounters a template element, it renders the template's 6 | `children` within a `React.Fragment`. You can change the wrapper element to any 7 | valid React element using the `as` prop. Any props you pass to a template with 8 | the `as` prop specified will be applied to that element. 9 | 10 | ```jsx 11 | function Child({ children }) { 12 | const { slot } = useSlot(); 13 | return ; 14 | } 15 | 16 | // Intrinsic elements 17 | 18 | 19 | Content 20 | 21 | ; 22 | // Expected HTML output: 23 |
    Content
    ; 24 | 25 | // The template element's child function is executed first 26 | 27 | 28 | {() => Content} 29 | 30 | ; 31 | // Expected HTML output: 32 |

    33 | Content 34 |

    ; 35 | 36 | // Custom components 37 | function Heading({ level, children }) { 38 | const Element = "h" + level; 39 | return {children}; 40 | } 41 | 42 | 43 | Content 44 | 45 | ; 46 | // Expected HTML output: 47 |

    48 | Content 49 |

    ; 50 | ``` 51 | 52 | 53 | Note: Only elements with an available `children` prop of type `ReactNode` can 54 | be used for the template's `as` prop. 55 | 56 | 57 | 58 | Be careful when providing custom components for template's as prop: [Template 59 | as Custom Component Footgun](/caveats#template-as-custom-component-footgun) 60 | 61 | -------------------------------------------------------------------------------- /packages/react-slots/src/typeGuards.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { 3 | SlotComponent, 4 | TemplateAsSlotComponentLikeElement, 5 | TemplateComponent, 6 | TemplateComponentLikeElement, 7 | } from "./types"; 8 | import { 9 | COMPONENT_TYPE, 10 | SLOT_TYPE_IDENTIFIER, 11 | TEMPLATE_TYPE_IDENTIFIER, 12 | } from "./constants"; 13 | 14 | function isReactSlotsComponent( 15 | component: string | React.JSXElementConstructor, 16 | ): component is TemplateComponent | SlotComponent

    { 17 | return ( 18 | typeof component === "function" && component.hasOwnProperty(COMPONENT_TYPE) 19 | ); 20 | } 21 | 22 | export function isTemplateComponent( 23 | component: string | React.JSXElementConstructor, 24 | ): component is TemplateComponent { 25 | return ( 26 | isReactSlotsComponent(component) && 27 | component[COMPONENT_TYPE] === TEMPLATE_TYPE_IDENTIFIER 28 | ); 29 | } 30 | 31 | export function isSlotComponent

    ( 32 | component: string | React.JSXElementConstructor, 33 | ): component is SlotComponent

    { 34 | return ( 35 | isReactSlotsComponent(component) && 36 | component[COMPONENT_TYPE] === SLOT_TYPE_IDENTIFIER 37 | ); 38 | } 39 | 40 | export function isTemplateElement( 41 | element: React.ReactNode, 42 | ): element is 43 | | TemplateComponentLikeElement 44 | | TemplateAsSlotComponentLikeElement { 45 | return React.isValidElement(element) && isTemplateComponent(element.type); 46 | } 47 | 48 | /** Note: {"slot-name": undefined} also passes */ 49 | export function isNamedSlot( 50 | element: React.ReactElement, 51 | ): element is React.ReactElement<{ ["slot-name"]?: N }> { 52 | return element.props.hasOwnProperty("slot-name"); 53 | } 54 | -------------------------------------------------------------------------------- /examples/vite/src/hooks/useStateControl.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | /** 4 | * Makes it possible for components to be both controlled and uncontrolled: 5 | * - Derives and maintains one source of truth from value and defaultValue. 6 | * - calls onChange on value change 7 | * */ 8 | export function useStateControl( 9 | value: T | undefined, 10 | defaultValue: T | undefined, 11 | onChange: ((v: T, ...args: unknown[]) => void) | undefined, 12 | ): [T | undefined, (nextValue: T) => void] { 13 | const isControlled = value !== undefined; 14 | 15 | const [internalState, setInternalState] = React.useState(() => 16 | isControlled ? value : defaultValue, 17 | ); 18 | 19 | if ( 20 | !isControlled && 21 | internalState === undefined && 22 | defaultValue !== undefined 23 | ) { 24 | // defaultValue was undefined at first but changed to some value 25 | setInternalState(defaultValue); 26 | } 27 | 28 | if (isControlled && value !== internalState) { 29 | // is controlled and a new value was provided. Sync internal state 30 | setInternalState(value); 31 | } 32 | 33 | const isControlledRef = React.useRef(isControlled); 34 | if (isControlledRef.current !== isControlled) { 35 | const wasControlled = isControlledRef.current; 36 | console.error( 37 | `A component changed from ${ 38 | wasControlled ? "controlled" : "uncontrolled" 39 | } to ${ 40 | isControlled ? "controlled" : "uncontrolled" 41 | }. This may lead to unexpected behavior.`, 42 | ); 43 | isControlledRef.current = isControlled; 44 | } 45 | 46 | const setState = React.useCallback( 47 | (value: T) => { 48 | if (onChange && value !== internalState) { 49 | onChange(value); 50 | } 51 | 52 | if (!isControlled) { 53 | setInternalState(value); 54 | } 55 | }, 56 | [internalState, isControlled, onChange], 57 | ); 58 | 59 | return [internalState, setState]; 60 | } 61 | -------------------------------------------------------------------------------- /docs/pages/recommendations.mdx: -------------------------------------------------------------------------------- 1 | # Recommendations 2 | 3 | ## Better IDE Experience With Typescript 4 | 5 | If you're working with TypeScript, you can take steps to streamline your 6 | development process by moving the `Slot` unions into the template argument of 7 | your component like this: 8 | 9 | ```tsx 10 | type Props = { 11 | children: SlotChildren, 12 | someOtherProp1: string, 13 | someOtherProp2: number 14 | } 15 | 16 | function MyComponent | Slot<"foo">>(props: Props) { 17 | return ... 18 | } 19 | ``` 20 | 21 | While this syntax may seem a bit more substantial, it offers the advantage of 22 | allowing you to easily identify the available slots for a component by hovering 23 | over its name. This means you won't need to navigate through the file to see 24 | which slots are supported. 25 | 26 | ## Untyped Templates and `noUncheckedIndexedAccess` 27 | 28 | In TypeScript, it's not possible to type an object that includes every possible 29 | property, which `template` and `slot` do. This can be frustrating when using 30 | untyped `template` or `slot` in your code, as it may lead to an error like this: 31 | 32 | ``` 33 | 'template.default' cannot be used as a JSX component. 34 | Its type 'TemplateComponent | undefined' is not a valid JSX element type. 35 | Type 'undefined' is not assignable to type 'ElementType'. 36 | ``` 37 | 38 | This error will only appear if you have `noUncheckedIndexedAccess` set to `true` 39 | in your `tsconfig` (by default, this rule is off). If you prefer to keep this 40 | rule enabled, you have two alternative options: 41 | 42 | 1. Always type your `children` with `SlotChildren` and `Slot` and utilize typed 43 | templates created with `CreateTemplate`. 44 | 2. Check if the property exists before accessing it, like this: 45 | `template.default && `. However, please note that this 46 | check is obsolete because every property on the `template` and `slot` objects 47 | is always defined. 48 | -------------------------------------------------------------------------------- /packages/unplugin-transform-react-slots/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@beqa/unplugin-transform-react-slots", 3 | "version": "1.0.1", 4 | "description": "JSX to slot function transpilation plugin for some of the common build systems", 5 | "author": "Beqa", 6 | "license": "MIT", 7 | "keywords": [ 8 | "unplugin", 9 | "rollup", 10 | "vite", 11 | "esbuild", 12 | "react-slots" 13 | ], 14 | "publishConfig": { 15 | "access": "public" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/Flammae/react-slots/issues" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/Flammae/react-slots.git" 23 | }, 24 | "main": "./dist/index.js", 25 | "module": "./dist/index.mjs", 26 | "types": "./dist/index.d.ts", 27 | "exports": { 28 | ".": { 29 | "require": "./dist/index.js", 30 | "import": "./dist/index.mjs" 31 | }, 32 | "./vite": { 33 | "require": "./dist/vite.js", 34 | "import": "./dist/vite.mjs" 35 | }, 36 | "./rollup": { 37 | "require": "./dist/rollup.js", 38 | "import": "./dist/rollup.mjs" 39 | }, 40 | "./esbuild": { 41 | "require": "./dist/esbuild.js", 42 | "import": "./dist/esbuild.mjs" 43 | }, 44 | "./webpack": { 45 | "require": "./dist/webpack.js", 46 | "import": "./dist/webpack.mjs" 47 | } 48 | }, 49 | "scripts": { 50 | "build": "tsup", 51 | "test": "pnpm run build && pnpm run test:build && pnpm run test:check", 52 | "test:watch": "pnpm run build && pnpm run test:build && vitest", 53 | "test:build": "tsx test/bundleTests", 54 | "test:check": "vitest run", 55 | "typecheck": "tsc" 56 | }, 57 | "dependencies": { 58 | "@babel/core": "^7.23.0", 59 | "@babel/plugin-syntax-typescript": "^7.22.5", 60 | "@beqa/babel-plugin-transform-react-slots": "workspace:*", 61 | "@rollup/pluginutils": "^5.0.4", 62 | "unplugin": "^1.5.0" 63 | }, 64 | "devDependencies": { 65 | "@beqa/react-slots": "workspace:*" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /examples/vite/src/components/dialog/Dialog.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | CreateTemplate, 3 | OverrideNode, 4 | Slot, 5 | SlotChildren, 6 | template, 7 | useSlot, 8 | } from "@beqa/react-slots"; 9 | import { useDialogTriggerContext } from "./DialogTriggerContent"; 10 | import Button from "../button/Button"; 11 | 12 | type Props = { 13 | children: SlotChildren< 14 | Slot<"title"> | Slot<"content"> | Slot<"primary"> | Slot<"secondary"> 15 | >; 16 | disableAutoClose?: boolean; 17 | }; 18 | 19 | export default function Dialog(props: Props) { 20 | const { close, titleId } = useDialogTriggerContext(); 21 | const { slot } = useSlot(props.children); 22 | 23 | // Auto close dialog if uncontrolled after any button click 24 | const onClickOverride = OverrideNode.chainAfter(() => { 25 | if (props.disableAutoClose) { 26 | return; 27 | } 28 | close(); 29 | }); 30 | 31 | return ( 32 |

    33 |

    34 | 35 |

    36 | 37 |
    38 | 39 |
    40 | 41 |
    42 | 43 | prop || "secondary", 49 | }} 50 | /> 51 | 52 | 53 | prop || "primary", 59 | }} 60 | /> 61 | 62 |
    63 |
    64 | ); 65 | } 66 | 67 | export const dialogTemplate = template as CreateTemplate; 68 | -------------------------------------------------------------------------------- /packages/unplugin-transform-react-slots/test/vite/vite.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, describe } from "vitest"; 2 | import fs from "fs/promises"; 3 | import path from "path"; 4 | 5 | async function readFile(relative: string) { 6 | return await fs.readFile(path.resolve(__dirname, relative), "utf-8"); 7 | } 8 | 9 | function includesLast(file: string, test: string) { 10 | return file.lastIndexOf(test) > 0; 11 | } 12 | 13 | describe("Esbuild config", () => { 14 | const disabledNoImport = readFile("./dist/disabled-no-import.mjs"); 15 | const disabledNoImportTS = readFile("./dist/disabled-no-import-ts.mjs"); 16 | 17 | const disabledWithPragma = readFile("./dist/disabled-with-pragma.mjs"); 18 | const disabledWithPragmaTS = readFile("./dist/disabled-with-pragma-ts.mjs"); 19 | 20 | const working = readFile("./dist/working.mjs"); 21 | const workingTS = readFile("./dist/working-ts.mjs"); 22 | 23 | test("won't transform when not imported", async () => { 24 | const [js, ts] = await Promise.all([disabledNoImport, disabledNoImportTS]); 25 | expect(includesLast(js, "createElement(slot.default, null)")).toBe(true); 26 | expect(includesLast(ts, "createElement(slot.default, null)")).toBe(true); 27 | }); 28 | test("won't transform when disabled with pragma", async () => { 29 | const [js, ts] = await Promise.all([ 30 | disabledWithPragma, 31 | disabledWithPragmaTS, 32 | ]); 33 | expect(includesLast(js, "createElement(slot.default, null)")).toBe(true); 34 | expect(includesLast(ts, "createElement(slot.default, null)")).toBe(true); 35 | }); 36 | test("transforms slot elements to functions", async () => { 37 | const [js, ts] = await Promise.all([working, workingTS]); 38 | expect( 39 | includesLast( 40 | js, 41 | 'slot.default(/* @__PURE__ */ reactExports.createElement("default-content-wrapper", null));', 42 | ), 43 | ).toBe(true); 44 | expect( 45 | includesLast( 46 | ts, 47 | 'slot.default(/* @__PURE__ */ React.createElement("default-content-wrapper", null));', 48 | ), 49 | ).toBe(true); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /packages/unplugin-transform-react-slots/test/esbuild/esbuild.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect, describe } from "vitest"; 2 | import fs from "fs/promises"; 3 | import path from "path"; 4 | 5 | async function readFile(relative: string) { 6 | return await fs.readFile(path.resolve(__dirname, relative), "utf-8"); 7 | } 8 | 9 | function includesLast(file: string, test: string) { 10 | return file.lastIndexOf(test) > 0; 11 | } 12 | 13 | describe("Esbuild config", () => { 14 | const disabledNoImport = readFile("./dist/disabled-no-import.js"); 15 | const disabledNoImportTS = readFile("./dist/disabled-no-import-ts.js"); 16 | 17 | const disabledWithPragma = readFile("./dist/disabled-with-pragma.js"); 18 | const disabledWithPragmaTS = readFile("./dist/disabled-with-pragma-ts.js"); 19 | 20 | const working = readFile("./dist/working.js"); 21 | const workingTS = readFile("./dist/working-ts.js"); 22 | 23 | test("won't transform when not imported", async () => { 24 | const [js, ts] = await Promise.all([disabledNoImport, disabledNoImportTS]); 25 | expect(includesLast(js, "React.createElement(slot.default, null)")).toBe( 26 | true, 27 | ); 28 | expect(includesLast(ts, "React.createElement(slot.default, null)")).toBe( 29 | true, 30 | ); 31 | }); 32 | test("won't transform when disabled with pragma", async () => { 33 | const [js, ts] = await Promise.all([ 34 | disabledWithPragma, 35 | disabledWithPragmaTS, 36 | ]); 37 | expect(includesLast(js, "React.createElement(slot.default, null)")).toBe( 38 | true, 39 | ); 40 | expect(includesLast(ts, "React.createElement(slot.default, null)")).toBe( 41 | true, 42 | ); 43 | }); 44 | test("transforms slot elements to functions", async () => { 45 | const [js, ts] = await Promise.all([working, workingTS]); 46 | expect( 47 | includesLast( 48 | js, 49 | `slot.default(/* @__PURE__ */ React.createElement("default-content-wrapper", null));`, 50 | ), 51 | ).toBe(true); 52 | expect( 53 | includesLast( 54 | ts, 55 | `slot.default(/* @__PURE__ */ React.createElement("default-content-wrapper", null));`, 56 | ), 57 | ).toBe(true); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /packages/babel-plugin-transform-react-slots/test/fixtures/all-allowed-syntax-js/code.jsx: -------------------------------------------------------------------------------- 1 | import _defaultExport, { useSlot, _anythingElse } from "@beqa/react-slots"; 2 | import * as ReactSlots from "@beqa/react-slots"; 3 | import { useSlot as useSlotAlias } from "@beqa/react-slots"; 4 | 5 | let a = useSlotAlias; 6 | const b = ReactSlots; 7 | let c = b.useSlot; 8 | const d = ReactSlots.useSlot; 9 | let { 10 | slot: { ...e }, 11 | } = b.useSlot(); // but won't transform because it's lower case 12 | 13 | // Transformation can be done in any scope 14 | if (true) { 15 | a(); // Ignore, not using returned value 16 | let e = b.useSlot(); // 17 | const { f } = c(); // Ignore, f is not a slot 18 | let { slot, g } = d(); // 19 | const { slot: h } = useSlot(); // 20 | let { 21 | slot: { i: SlotName }, 22 | } = useSlot(); // 23 | const { ...j } = e; // ; 24 | let { k: Anything, ...l } = ReactSlots.useSlot().slot; // 25 | 26 | e.slot.anything(); // Ignore, not a jsx element 27 | ; // MUST TRANSFORM 28 | children; // MUST TRANSFORM 29 | ; // MUST TRANSFORM 30 |
    31 | {/* MUST TRANSFORM */} 32 | 33 |
    ; 34 |
    38 | 39 | 40 | } 41 | />; 42 | } 43 | 44 | ; // won't transform because it's a lowercase name and jsx won't treat it as a variable name. 45 | ; // won't transform because h is defined in a different scope 46 | 47 | d; // Nothing to see here 48 | 49 | let f = c(); 50 | ; // won't transform because not accessing slot property on c(); 51 | 52 | function _functionName() { 53 | let f = c; 54 | let { 55 | slot: { ...g }, 56 | } = f(); // 57 | 58 | return ; // MUST TRANSFORM 59 | } 60 | 61 | // The following syntax does nothing but should not throw; 62 | if (useSlotAlias) { 63 | } 64 | if (useSlotAlias().slot.name) { 65 | } 66 | ((useSlotAlias && useSlotAlias().slot) || useSlotAlias().slot.default) ?? ( 67 | // Must transform 68 | ); 69 | -------------------------------------------------------------------------------- /examples/vite/src/components/dialog/DialogTrigger.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Slot, 3 | SlotChildren, 4 | useSlot, 5 | OverrideNode, 6 | template, 7 | CreateTemplate, 8 | } from "@beqa/react-slots"; 9 | import Modal from "react-modal"; 10 | import { useStateControl } from "../../hooks/useStateControl"; 11 | import { DialogTriggerContextProvider } from "./DialogTriggerContent"; 12 | import Button from "../button/Button"; 13 | import { useCallback } from "react"; 14 | 15 | const DIALOG_TITLE_ID = "dialog-title"; 16 | 17 | type Props = { 18 | children: SlotChildren< 19 | Slot | Slot<"dialog", { close: () => void; titleId: string }> 20 | >; 21 | onToggle?: (nextIsOpen: boolean) => void; 22 | // Supports both controlled and uncontrolled variants (similar to value and defaultValue props on ) 23 | isOpen?: boolean; 24 | defaultIsOpen?: boolean; 25 | }; 26 | 27 | export function DialogTrigger(props: Props) { 28 | const { slot } = useSlot(props.children); 29 | 30 | const [isOpen, setIsOpen] = useStateControl( 31 | props.isOpen, 32 | props.defaultIsOpen, 33 | props.onToggle, 34 | ); 35 | 36 | const close = useCallback(() => setIsOpen(false), [setIsOpen]); 37 | 38 | return ( 39 | <> 40 | 41 | 42 | { 47 | setIsOpen(!isOpen); 48 | }), 49 | }} 50 | > 51 | 52 | 53 | 54 | {/* Wrap whatever element consumer provides for the dialog slot in a context so it can use setIsOpen inside */} 55 | 56 | 57 | {/* Passing these props up isn't necessary if consumer only uses Dialog for this slot, but if they decide to provide something custom instead, it could come in handy */} 58 | setIsOpen(false)} 61 | /> 62 | 63 | 64 | 65 | ); 66 | } 67 | 68 | export const dialogTriggerTemplate = template as CreateTemplate< 69 | Props["children"] 70 | >; 71 | -------------------------------------------------------------------------------- /docs/pages/advanced/multiple-override-node.mdx: -------------------------------------------------------------------------------- 1 | # Using Multiple `OverrideNode` Elements 2 | 3 | You can include multiple `OverrideNode` elements for a single slot as long as 4 | they are all top-level children. When you use multiple `OverrideNode` elements, 5 | their overrides are applied from top to bottom to the parent-provided content. 6 | The result of the previous override becomes the input for the next 7 | `OverrideNode`. When the parent doesn't provide content, only the `OverrideNode` 8 | elements that wrap the fallback content will be applied. 9 | 10 | In this example, a component restricts the node type for the "trigger" slot to 11 | be either a button element or a string and then handles their overrides 12 | separately: 13 | 14 | ```jsx 15 | 16 | 17 | {/* Override string nodes */} 18 | { 22 | ; 23 | }} 24 | /> 25 | {/* Override button elements */} 26 | 33 | {/* Nothing was provided, render fallback */} 34 | 35 | 36 | ``` 37 | 38 | Here's an example that demonstrates the difference between applying overrides to 39 | provided content versus fallback content: 40 | 41 | ```jsx 42 | function MyComponent(children) { 43 | const { slot } = useSlot(children); 44 | 45 | return ( 46 | 47 |
    {node}
    } /> 48 | "added-class" }}> 49 | Fallback 50 | 51 | "added-id" }}> 52 |
    Second Fallback
    53 |
    54 |
    55 | ); 56 | } 57 | 58 | // Providing content: 59 | Content; 60 | // Expected HTML output: 61 |
    62 | Content 63 |
    ; 64 | 65 | // No content: 66 | ; 67 | // Expected HTML output: 68 | Fallback; 69 |
    Second Fallback
    ; 70 | ``` 71 | -------------------------------------------------------------------------------- /packages/babel-plugin-transform-react-slots/test/fixtures/all-allowed-syntax-ts/code.tsx: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | 3 | import _defaultExport, { useSlot, _anythingElse } from "@beqa/react-slots"; 4 | import * as ReactSlots from "@beqa/react-slots"; 5 | import { useSlot as useSlotAlias } from "@beqa/react-slots"; 6 | 7 | let a = useSlotAlias; 8 | const b = ReactSlots; 9 | let c = (b as SomeType).useSlot; 10 | const d = ReactSlots.useSlot; 11 | let { 12 | slot: { ...e }, 13 | }: any = b.useSlot() as SomeType; // but won't transform because it's lower case 14 | 15 | // Transformation can be done in any scope 16 | if (true) { 17 | a(); // Ignore, not using returned value 18 | let e: any = (b satisfies any).useSlot() as any; // 19 | const { f }: SomeAnnotation = c(); // Ignore, f is not a slot 20 | let { slot, g } = d() as unknown as SomeType; // 21 | const { slot: h } = useSlot(); // 22 | let { 23 | slot: { i: SlotName }, 24 | } = useSlot(); // 25 | const { ...j } = e; // ; 26 | let { k: Anything, ...l } = ReactSlots.useSlot().slot; // 27 | 28 | l as any; // Ignore, expression statement 29 | 30 | e.slot.anything(); // Ignore, not a jsx element 31 | />; // MUST TRANSFORM 32 | >children; // MUST TRANSFORM 33 | ; // MUST TRANSFORM 34 |
    35 | {/* MUST TRANSFORM */} 36 | 37 |
    ; 38 |
    42 | prop1={1} prop2="string" prop3 /> 43 | 44 | } 45 | />; 46 | } 47 | 48 | ; // won't transform because it's a lowercase name and jsx won't treat it as a variable name. 49 | ; // won't transform because h is defined in a different scope 50 | 51 | d; // Nothing to see here 52 | 53 | let f = c(); 54 | ; // won't transform because not accessing slot property on c(); 55 | 56 | function _functionName() { 57 | let f: SomeTypeWithArgs = 58 | c satisfies any satisfies unknown as SomeTypeWithArgs; 59 | let { 60 | slot: { ...g }, 61 | } = f(); // 62 | 63 | return ; // MUST TRANSFORM 64 | } 65 | 66 | // The following syntax does nothing but should not throw; 67 | if (useSlotAlias) { 68 | } 69 | if (useSlotAlias().slot.name as unknown) { 70 | } 71 | (((useSlotAlias as any) && (useSlotAlias().slot as Something)) || 72 | useSlotAlias().slot.default) ?? 73 | f.slot.default(null); // Must transform 74 | -------------------------------------------------------------------------------- /packages/babel-plugin-transform-react-slots/test/fixtures/all-allowed-syntax-js/output.mjs: -------------------------------------------------------------------------------- 1 | import _defaultExport, { useSlot, _anythingElse } from "@beqa/react-slots"; 2 | import * as ReactSlots from "@beqa/react-slots"; 3 | import { useSlot as useSlotAlias } from "@beqa/react-slots"; 4 | let a = useSlotAlias; 5 | const b = ReactSlots; 6 | let c = b.useSlot; 7 | const d = ReactSlots.useSlot; 8 | let { 9 | slot: { ...e }, 10 | } = b.useSlot(); // but won't transform because it's lower case 11 | 12 | // Transformation can be done in any scope 13 | if (true) { 14 | a(); // Ignore, not using returned value 15 | let e = b.useSlot(); // 16 | const { f } = c(); // Ignore, f is not a slot 17 | let { slot, g } = d(); // 18 | const { slot: h } = useSlot(); // 19 | let { 20 | slot: { i: SlotName }, 21 | } = useSlot(); // 22 | const { ...j } = e; // ; 23 | let { k: Anything, ...l } = ReactSlots.useSlot().slot; // 24 | 25 | e.slot.anything(); // Ignore, not a jsx element 26 | slot.anything(); // MUST TRANSFORM 27 | h.anything(children); // MUST TRANSFORM 28 | SlotName(, { 29 | prop1: 1, 30 | prop2: "string", 31 | prop3: true, 32 | }); // MUST TRANSFORM 33 |
    34 | {/* MUST TRANSFORM */} 35 | {j.slot.anything()} 36 |
    ; 37 |
    42 | {l.anythingElse(, { 43 | prop1: 1, 44 | prop2: "string", 45 | prop3: true, 46 | })} 47 | , 48 | { 49 | prop1: 1, 50 | prop2: "string", 51 | prop3: true, 52 | } 53 | ) 54 | } 55 | />; 56 | } 57 | ; // won't transform because it's a lowercase name and jsx won't treat it as a variable name. 58 | ; // won't transform because h is defined in a different scope 59 | 60 | d; // Nothing to see here 61 | 62 | let f = c(); 63 | ; // won't transform because not accessing slot property on c(); 64 | 65 | function _functionName() { 66 | let f = c; 67 | let { 68 | slot: { ...g }, 69 | } = f(); // 70 | 71 | return g.anything(); // MUST TRANSFORM 72 | } 73 | 74 | // The following syntax does nothing but should not throw; 75 | if (useSlotAlias) { 76 | } 77 | if (useSlotAlias().slot.name) { 78 | } 79 | ((useSlotAlias && useSlotAlias().slot) || useSlotAlias().slot.default) ?? 80 | f.slot.default(); // Must transform -------------------------------------------------------------------------------- /packages/babel-plugin-transform-react-slots/test/fixtures/all-allowed-syntax-ts/output.mjs: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | 3 | import { useSlot } from "@beqa/react-slots"; 4 | import * as ReactSlots from "@beqa/react-slots"; 5 | import { useSlot as useSlotAlias } from "@beqa/react-slots"; 6 | let a = useSlotAlias; 7 | const b = ReactSlots; 8 | let c = b.useSlot; 9 | const d = ReactSlots.useSlot; 10 | let { 11 | slot: { ...e }, 12 | } = b.useSlot(); // but won't transform because it's lower case 13 | 14 | // Transformation can be done in any scope 15 | if (true) { 16 | a(); // Ignore, not using returned value 17 | let e = b.useSlot(); // 18 | const { f } = c(); // Ignore, f is not a slot 19 | let { slot, g } = d(); // 20 | const { slot: h } = useSlot(); // 21 | let { 22 | slot: { i: SlotName }, 23 | } = useSlot(); // 24 | const { ...j } = e; // ; 25 | let { k: Anything, ...l } = ReactSlots.useSlot().slot; // 26 | 27 | l; // Ignore, expression statement 28 | 29 | e.slot.anything(); // Ignore, not a jsx element 30 | slot.anything(); // MUST TRANSFORM 31 | h.anything(children); // MUST TRANSFORM 32 | SlotName(, { 33 | prop1: 1, 34 | prop2: "string", 35 | prop3: true, 36 | }); // MUST TRANSFORM 37 |
    38 | {/* MUST TRANSFORM */} 39 | {j.slot.anything()} 40 |
    ; 41 |
    46 | {l.anythingElse(, { 47 | prop1: 1, 48 | prop2: "string", 49 | prop3: true, 50 | })} 51 | , 52 | { 53 | prop1: 1, 54 | prop2: "string", 55 | prop3: true, 56 | } 57 | ) 58 | } 59 | />; 60 | } 61 | ; // won't transform because it's a lowercase name and jsx won't treat it as a variable name. 62 | ; // won't transform because h is defined in a different scope 63 | 64 | d; // Nothing to see here 65 | 66 | let f = c(); 67 | ; // won't transform because not accessing slot property on c(); 68 | 69 | function _functionName() { 70 | let f = c; 71 | let { 72 | slot: { ...g }, 73 | } = f(); // 74 | 75 | return g.anything(); // MUST TRANSFORM 76 | } 77 | 78 | // The following syntax does nothing but should not throw; 79 | if (useSlotAlias) { 80 | } 81 | if (useSlotAlias().slot.name) { 82 | } 83 | ((useSlotAlias && useSlotAlias().slot) || useSlotAlias().slot.default) ?? 84 | f.slot.default(null); // Must transform -------------------------------------------------------------------------------- /docs/pages/caveats.mdx: -------------------------------------------------------------------------------- 1 | # Caveats 2 | 3 | ## Slot Content Keys 4 | 5 | When `useSlot` parses `children`, it flattens the arrays. This allows it to 6 | correctly identify content for slots. For this reason, keys must be unique 7 | within the slot content, even if they are part of different arrays. 8 | 9 | ```jsx 10 | function MyComponent({ children }) { 11 | const { slot } = useSlot(children); 12 | return ; 13 | } 14 | 15 | // ❌ Incorrect: Duplicate keys 1 and 2 16 | 17 | {[ 18 |
    First node
    , 19 | Second second 20 | ]} 21 | {[ 22 | Third node 23 | ]} 24 | Fourth node 25 | Fifth node 26 | Sixth node 27 |
    28 | 29 | // ✅ Correct: Keys are different. 30 | 31 | {[ 32 |
    First node
    , 33 | Second second 34 | ]} 35 | {[ 36 | Third node 37 | ]} 38 | Fourth node 39 | Fifth node 40 | Sixth node 41 |
    42 | ``` 43 | 44 | ## Template as Custom Component Footgun 45 | 46 | When you specify a custom component with the template element's `as` prop, you 47 | must exercise caution. This is because the `children` you specify for the 48 | template element won't be the same `children` that will be passed to the `as` 49 | element. `react-slots` is likely to modify the `children` in a way that's 50 | significantly different from what you might expect. This is especially true when 51 | `OverrideNode` is used within the slot. For this reason, the `as` element's 52 | `children` must be of type `ReactNode` and should only be used within the `as` 53 | element for the purpose of rendering. 54 | 55 | ```tsx 56 | function MyComponent({ children }: { children: string }) { 57 | performSideEffect(children); 58 | return
    {children}
    ; 59 | } 60 | // ❌ Not Allowed: MyComponent expects a string to perform a side effect with. 61 | // The provided node will likely not be a string. 62 | Content; 63 | 64 | function MySecondComponent({ children }: { children: React.ReactNode }) { 65 | return shouldRenderChildren ? children : "default children"; 66 | } 67 | // ⚠️ Caution: MySecondComponent may or may not render children. 68 | Content; 69 | 70 | function MyThirdComponent({ children }: { children: React.ReactNode }) { 71 | return
    {children}
    ; 72 | } 73 | // ✅ Allowed: MyThirdComponent always returns unmodified children. 74 | Content; 75 | ``` 76 | 77 | If you are using typescript, compiler won't allow you to specify a component 78 | whose `children` is not assignable to `ReactNode` 79 | -------------------------------------------------------------------------------- /packages/unplugin-transform-react-slots/src/index.ts: -------------------------------------------------------------------------------- 1 | import { UnpluginInstance, createUnplugin } from "unplugin"; 2 | import { createFilter } from "@rollup/pluginutils"; 3 | import { type Options, resolveOption, defaultInclude } from "./core/options"; 4 | import { transformAsync } from "@babel/core"; 5 | import transformReactSlots from "@beqa/babel-plugin-transform-react-slots"; 6 | import SyntaxTypescript from "@babel/plugin-syntax-typescript"; 7 | 8 | // Matches @disable-transform-react-slots at the very start. Only line comments or block comments can precede it 9 | const isDisabledRegex = 10 | /^(?:\s*(?:\/\/[^\n\r]*|\/\*(?:.|[\n\r])*?\*\/))*\s*\/\/\s*@disable-transform-react-slots\W/; 11 | 12 | function appendTestComment(code: string, isDisabled: boolean): string { 13 | if (isDisabled) { 14 | code += 15 | "/* slot transformation skipped by unplugin-transform-react-slots */"; 16 | } else { 17 | code += "/* slot transformation done by unplugin-transform-react-slots */"; 18 | } 19 | return code; 20 | } 21 | 22 | export default createUnplugin((rawOptions) => { 23 | const options = resolveOption(rawOptions); 24 | const filter = createFilter(options.include, options.exclude); 25 | 26 | return { 27 | name: "unplugin-transform-react-slots", 28 | 29 | enforce: "pre", 30 | 31 | transformInclude(id) { 32 | return filter(id); 33 | }, 34 | 35 | async transform(code, id) { 36 | if (id.endsWith(".ts")) { 37 | return appendTestComment(code, true); 38 | } 39 | 40 | if (isDisabledRegex.test(code)) { 41 | return appendTestComment(code, true); 42 | } 43 | 44 | if (!(code.includes("@beqa/react-slots") && code.includes("useSlot"))) { 45 | return appendTestComment(code, true); 46 | } 47 | 48 | const transformed = await transformAsync(code, { 49 | plugins: [ 50 | id.endsWith(".tsx") ? [SyntaxTypescript, { isTSX: true }] : "", 51 | transformReactSlots, 52 | ].filter(Boolean), 53 | filename: id, 54 | }); 55 | 56 | if (!transformed) { 57 | throw new Error( 58 | "unplugin-transform-react-slots failed to transform file: " + id, 59 | ); 60 | } 61 | 62 | // TODO: test if returning babel sourcemap makes it better or worse 63 | return transformed.code && appendTestComment(transformed.code, false); 64 | }, 65 | 66 | esbuild: { 67 | onLoadFilter: 68 | !!options.include && !Array.isArray(options.include) 69 | ? options.include 70 | : defaultInclude, 71 | loader(code, id) { 72 | if (id.endsWith("tsx")) { 73 | return "tsx"; 74 | } 75 | 76 | if (id.endsWith("ts")) { 77 | return "ts"; 78 | } 79 | 80 | return "jsx"; 81 | }, 82 | }, 83 | }; 84 | }) as Pick< 85 | UnpluginInstance, 86 | "esbuild" | "rollup" | "vite" | "webpack" 87 | >; 88 | -------------------------------------------------------------------------------- /docs/pages/tutorial/enforcing-node-type.mdx: -------------------------------------------------------------------------------- 1 | # Enforcing the Node Type 2 | 3 | One of the simplest and most common use cases for `OverrideNode` is enforcing 4 | that parents provide a specific type of node for a slot. This practice is common 5 | in regular HTML elements. For example, a `
      ` element can only have `
    • ` as 6 | a child, a `