├── .gitignore ├── src ├── shared.ts ├── utils.ts ├── index.ts ├── main-entry.import-declaration-visitor.ts ├── handle-import-specifier.ts └── handle-if-reference.ts ├── .vscode └── settings.json ├── tsconfig.json ├── index.d.ts ├── package.json ├── tests └── index.test.ts └── readme.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | exp.ts -------------------------------------------------------------------------------- /src/shared.ts: -------------------------------------------------------------------------------- 1 | 2 | /** The npm handle for the plugin */ 3 | export const PLUGIN_HANDLE = 'babel-plugin-solid-if-component' 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "Reactivars", 4 | "sthir" 5 | ] 6 | } 7 | es6-string-javascript -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "moduleResolution": "node", 5 | "noImplicitAny": true, 6 | "strict": false, 7 | "outDir": "dist" 8 | }, 9 | "include": ["src/**/*"], 10 | "exclude": ["node_modules", "**/*.test.js"] 11 | } -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { JSX, Show } from 'solid-js' 2 | 3 | 4 | export declare const If: (props: { 5 | cond: T | undefined | null | false; 6 | fallback?: JSX.Element; 7 | children: JSX.Element | ((item: NonNullable) => JSX.Element); 8 | }) => (() => JSX.Element) 9 | 10 | export declare const Else:( 11 | props: { children: JSX.Element; } 12 | ) => (() => JSX.Element) 13 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { identifier, numericLiteral, memberExpression } from '@babel/types' 2 | 3 | 4 | export const getGetterMemberExpression = 5 | (bindingName: string) => 6 | memberExpression( 7 | identifier(bindingName), 8 | numericLiteral(0), 9 | true 10 | ) 11 | 12 | 13 | // Take a string and remove all new lines and whitespaces longer than 1 character 14 | const removeUnnecessaryWhitespace = (message: string) => 15 | message 16 | .replace(/\n/g, "") 17 | .replace(/\s+/g, " ") 18 | 19 | export const error = (message: string) => { 20 | throw new Error(removeUnnecessaryWhitespace(message)) 21 | } 22 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import type { Program } from '@babel/types' 2 | import { NodePath } from '@babel/traverse' 3 | import { mainEntryImportDeclarationVisitor } 4 | from './main-entry.import-declaration-visitor' 5 | import { PLUGIN_HANDLE } from './shared' 6 | 7 | 8 | /** Cleans up unused macro imports */ 9 | const cleanUnusuedImportsProgramVisitor = { 10 | exit: (programPath: NodePath) => 11 | programPath.traverse({ 12 | ImportDeclaration: importDeclarationPath => 13 | importDeclarationPath.node.source.value === PLUGIN_HANDLE 14 | && importDeclarationPath.remove() 15 | }) 16 | } 17 | 18 | 19 | export default () => ({ 20 | name: PLUGIN_HANDLE, 21 | visitor: { 22 | ImportDeclaration: mainEntryImportDeclarationVisitor, 23 | Program: cleanUnusuedImportsProgramVisitor 24 | } 25 | }) 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babel-plugin-solid-if-component", 3 | "version": "0.0.2", 4 | "description": "A babel for SolidJS that gives you an and components which provide an alternative syntax to the component.", 5 | "main": "dist/index", 6 | "types": "index.d.ts", 7 | "scripts": { 8 | "test": "uvu -r tsm tests", 9 | "build": "tsc" 10 | }, 11 | "devDependencies": { 12 | "@babel/core": "^7.16.7", 13 | "@babel/generator": "^7.16.7", 14 | "@babel/parser": "^7.16.7", 15 | "@babel/plugin-syntax-jsx": "^7.16.7", 16 | "@babel/traverse": "^7.16.7", 17 | "@types/babel__generator": "^7.6.4", 18 | "@types/babel__traverse": "^7.14.2", 19 | "ts-node": "^10.4.0", 20 | "tsm": "^2.2.1", 21 | "typescript": "^4.5.4", 22 | "uvu": "^0.5.3", 23 | "@babel/types": "^7.16.7" 24 | }, 25 | "peerDependencies": { 26 | "@babel/core": "^7.16", 27 | "solid-js": "^1.3" 28 | }, 29 | "license": "MIT", 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/orenelbaum/babel-plugin-solid-if-component" 33 | }, 34 | "author": "Oren Elbaum", 35 | "keywords": [ 36 | "babel", 37 | "babel-plugin", 38 | "solid", 39 | "solid-js", 40 | "react", 41 | "if", 42 | "else", 43 | "show", 44 | "conditionals" 45 | ] 46 | } -------------------------------------------------------------------------------- /src/main-entry.import-declaration-visitor.ts: -------------------------------------------------------------------------------- 1 | import { 2 | importSpecifier as createImportSpecifier, identifier as createIdentifier, 3 | stringLiteral as createStringLiteral, 4 | importDeclaration as createImportDeclaration, 5 | } from '@babel/types' 6 | import type { ImportDeclaration } from '@babel/types' 7 | import { NodePath } from '@babel/traverse' 8 | import { PLUGIN_HANDLE } from './shared' 9 | import { handleImportSpecifier } from './handle-import-specifier' 10 | 11 | 12 | export const mainEntryImportDeclarationVisitor = ( 13 | importDeclarationPath: NodePath 14 | ) => { 15 | const importDeclaration = importDeclarationPath.node; 16 | 17 | if (importDeclaration.source.value !== PLUGIN_HANDLE) return 18 | 19 | // Create a unique identifier for 'Show' 20 | const showIdentifier = 21 | importDeclarationPath.scope.generateUidIdentifier('Show') 22 | 23 | for (const importSpecifier of importDeclaration.specifiers) 24 | handleImportSpecifier( 25 | importDeclarationPath, showIdentifier, importSpecifier 26 | ) 27 | 28 | // Add import of `Show` from 'solid-js' as the showIdentifier 29 | importDeclarationPath.insertAfter( 30 | createImportDeclaration( 31 | [ 32 | createImportSpecifier( 33 | showIdentifier, 34 | createIdentifier('Show') 35 | ) 36 | ], 37 | createStringLiteral('solid-js') 38 | ) 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import { test } from 'uvu' 2 | import * as assert from 'uvu/assert' 3 | import { traverse } from '@babel/core' 4 | import generate from '@babel/generator' 5 | import { parse } from '@babel/parser' 6 | import main from '../src/index' 7 | 8 | 9 | test('index', async () => { 10 | await testOnlyIf() 11 | await testIfAndElse() 12 | }) 13 | 14 | test.run() 15 | 16 | 17 | async function assertTransform(src, expectedOutput, message, shouldLog = false) { 18 | const ast = parse( 19 | src, 20 | { 21 | sourceType: "module", 22 | plugins: [ "jsx" ] 23 | } 24 | ) 25 | 26 | traverse( 27 | ast, 28 | main().visitor 29 | ) 30 | 31 | const res = generate(ast) 32 | 33 | if (shouldLog) console.log(res.code) 34 | 35 | assert.snapshot(res.code, expectedOutput, message) 36 | } 37 | 38 | 39 | // Only without 40 | async function testOnlyIf() { 41 | const src = 42 | /*javascript*/`import { If } from 'babel-plugin-solid-if-component'; 43 | <> 44 | 45 |
Hello
46 |
47 | ` 48 | 49 | const expectedOutput = 50 | /*javascript*/`import { Show as _Show } from "solid-js"; 51 | <> 52 | <_Show when={hello}> 53 |
Hello
54 | 55 | ;` 56 | 57 | await assertTransform(src, expectedOutput, 'Only without ') 58 | } 59 | 60 | // and 61 | async function testIfAndElse() { 62 | const src = 63 | /*javascript*/`import { If, Else } from 'babel-plugin-solid-if-component'; 64 | <> 65 | 66 |
Hello
67 |
68 | 69 |
Goodbye
70 |
71 | ` 72 | 73 | const expectedOutput = 74 | `import { Show as _Show } from "solid-js"; 75 | <> 76 | <_Show when={hello} fallback=<> 77 |
Goodbye
78 | > 79 |
Hello
80 | 81 | 82 | ;` 83 | 84 | await assertTransform(src, expectedOutput, ' and ') 85 | } 86 | -------------------------------------------------------------------------------- /src/handle-import-specifier.ts: -------------------------------------------------------------------------------- 1 | 2 | import type { ImportDeclaration, Identifier } from '@babel/types' 3 | import { NodePath } from '@babel/traverse' 4 | import { PLUGIN_HANDLE } from './shared' 5 | import { error } from './utils' 6 | import { handleIfReference } from './handle-if-reference' 7 | 8 | 9 | type AnyImportSpecifier = ImportDeclaration['specifiers'][0] 10 | 11 | const validateImportSpecifier = ( 12 | importSpecifier: AnyImportSpecifier 13 | ) => { 14 | // Error on import namespace specifier (import * as) 15 | if (importSpecifier.type === 'ImportNamespaceSpecifier') 16 | return void error(` 17 | ${PLUGIN_HANDLE} error: you tried to import using namespace 18 | specifier (import * as ... from '${PLUGIN_HANDLE}'). This syntax is 19 | unsupported. Use nomral import specifier instead (import { If, Else 20 | } from '${PLUGIN_HANDLE}'). 21 | `) 22 | 23 | // Error on import default specifier (import x from) 24 | if (importSpecifier.type === 'ImportDefaultSpecifier') 25 | return void error(` 26 | ${PLUGIN_HANDLE} error: you tried to import using default specifier 27 | (import x from '${PLUGIN_HANDLE}'). This syntax is unsupported. Use 28 | nomral import specifier instead (import { If, Else } from 29 | '${PLUGIN_HANDLE}'). 30 | `) 31 | 32 | // Error on string literal import specifier (import { 'x' as y } from) 33 | const imported = importSpecifier.imported 34 | if (imported.type === 'StringLiteral') 35 | return void error(` 36 | ${PLUGIN_HANDLE} error: you tried to import using a string literal 37 | in your specifier (import { 'x' as y } from z). This syntax is 38 | unsupported. Use nomral import specifier instead (import { If, Else 39 | } from '${PLUGIN_HANDLE}'). 40 | `) 41 | 42 | const importedName = imported.name 43 | 44 | if (importedName !== 'If' && importedName !== 'Else') 45 | return void error(` 46 | ${PLUGIN_HANDLE} error: you tried to import from '${PLUGIN_HANDLE}' 47 | using a specifier that is not 'If' or 'Else'. Those are the only 48 | two valid specifiers. 49 | `) 50 | 51 | return { importedName: importedName as 'If' | 'Else' } 52 | } 53 | 54 | export const handleImportSpecifier = ( 55 | importDeclarationPath: NodePath, 56 | showIdentifier: Identifier, 57 | importSpecifier: AnyImportSpecifier 58 | ) => { 59 | const { importedName } = validateImportSpecifier(importSpecifier) 60 | 61 | const bindingName = importSpecifier.local.name 62 | 63 | if (importedName === 'If') 64 | for ( 65 | const referencePath of 66 | importDeclarationPath.scope.bindings[bindingName].referencePaths 67 | ) 68 | handleIfReference(referencePath, showIdentifier) 69 | 70 | // Remove the import specifier 71 | importDeclarationPath.node.specifiers.splice( 72 | importDeclarationPath.node.specifiers.indexOf(importSpecifier), 73 | 1 74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # babel-plugin-solid-if-component 2 | 3 | `babel-plugin-solid-if-component` is a Babel for SolidJS plugin that gives you an `` and `` component macros. It compiles to Solid's `` component (`` goes to the fallback prop) and it gives you an altrnative syntax to the `` component that achieve the same conditional rendering behavior. 4 | 5 | **[Try in Stackblitz](https://stackblitz.com/github/orenelbaum/babel-plugin-solid-if-component-example)** 6 | 7 | **[Open example repo in Github](https://github.com/orenelbaum/babel-plugin-solid-if-component-example)** 8 | 9 | > **Note** 10 | This plugin is WIP. 11 | 12 | ```jsx 13 | import { If, Else } from 'babel-plugin-solid-if-component'; 14 | 15 | const MyComp = () => { 16 | return ( 17 | <> 18 | 19 |
Hello
20 |
21 | 22 |
Goodbye
23 |
24 | 25 | ) 26 | } 27 | 28 | // ↓ ↓ ↓ ↓ Compiles to ↓ ↓ ↓ ↓ 29 | 30 | import { Show as _Show } from "solid-js"; 31 | 32 | const MyComp = () => { 33 | return ( 34 | <> 35 | <_Show 36 | when={hello} 37 | fallback={
Goodbye
} 38 | > 39 |
Hello
40 | 41 | 42 | ) 43 | } 44 | ``` 45 | 46 | - The `` component can be used by itself. 47 | - The `` component has to always follow an `` component. 48 | - An else-if syntax is not supported yet but is on the roadmap. 49 | - Error handling is not fully implemented yet. 50 | - Errors can also be prvented by an ESLint rule which is also on the roadmap. 51 | 52 | ## Getting Started 53 | 54 | ```sh 55 | npm i -D babel-plugin-solid-if-component 56 | ``` 57 | 58 | In your Vite config, find the your vite-plugin-solid initialization (in the default Solid template it will be imported as solidPlugin). 59 | 60 | The first argument this initialization function takes, is the options object. 61 | 62 | Add this field to the initializer options: 63 | 64 | ```js 65 | { 66 | babel: { 67 | plugins: ['babel-plugin-solid-if-component'] 68 | } 69 | } 70 | ``` 71 | 72 | 73 | ## Roadmap / Missing Features 74 | - `` / `` component 75 | - Error handling 76 | - More tests 77 | - ESLint rule 78 | - Alternative auto import syntax: `` and `` (under considaration) 79 | 80 | 81 | 82 | ## Other cool plugins for Solid 83 | - https://github.com/orenelbaum/babel-plugin-reactivars-solid - A Svelte-like "reactive variables" plugin for Solid that lets you pass reactive variables (getter + setter) around in a concise way (made by me). 84 | - https://github.com/orenelbaum/babel-plugin-solid-undestructure - This plugin lets you destructure your props without losing reactivity (made by me). 85 | - https://github.com/LXSMNSYC/babel-plugin-solid-labels - Solid labels is more of an all in one plugin. It has Svelte-like reactive variables (like this plugin), prop destructuring and more. 86 | - https://github.com/LXSMNSYC/solid-sfc - An experimental SFC compiler for SolidJS. 87 | -------------------------------------------------------------------------------- /src/handle-if-reference.ts: -------------------------------------------------------------------------------- 1 | import { 2 | jSXIdentifier as createJsxIdentifier, jSXAttribute as createJsxAttribute, 3 | jSXFragment as creteJsxFragment, 4 | jSXOpeningFragment as createJsxOpeningFragment, 5 | jSXClosingFragment as createJsxClosingFragment, 6 | } from '@babel/types' 7 | import type { 8 | Identifier, Node, JSXAttribute, JSXElement, JSXIdentifier 9 | } from '@babel/types' 10 | import { NodePath } from '@babel/traverse' 11 | import { PLUGIN_HANDLE } from './shared' 12 | import { error } from './utils' 13 | 14 | 15 | export const handleIfReference = ( 16 | referencePath: NodePath, 17 | showIdentifier: Identifier, 18 | ) => { 19 | // Check that the reference is used as a JSX component 20 | if (!referencePath.isJSXIdentifier()) return error(` 21 | ${PLUGIN_HANDLE} error: the 'If' import can only be used as a JSX 22 | component. 23 | `) 24 | 25 | const reference = referencePath.node 26 | const referenceParent = referencePath.parent 27 | 28 | // Check if the reference is used in a closing component 29 | if (referenceParent.type === 'JSXClosingElement') return 30 | if (referenceParent.type !== 'JSXOpeningElement') return error(` 31 | ${PLUGIN_HANDLE} error: you used 'if' as a normal variable. You can 32 | only use it as a component. 33 | `) 34 | 35 | const ifOpeningElementPath = referencePath.parentPath 36 | const ifOpeningElement = referenceParent 37 | const ifElementPath = 38 | ifOpeningElementPath.parentPath as NodePath 39 | const ifElement = ifElementPath.node 40 | const ifElementParentChildren = (ifElementPath.parent as any).children 41 | 42 | // Check if the next sibling is an component 43 | 44 | // Find the index of the If element in the parent's children 45 | const ifElementChildIndex = 46 | ifElementParentChildren.indexOf(ifElement) as number 47 | 48 | let nextSibling = ifElementParentChildren[ifElementChildIndex + 1] 49 | 50 | const nextSiblingIsWhiteSpace = 51 | nextSibling.type === 'JSXText' && nextSibling.value.trim() === '' 52 | if (nextSiblingIsWhiteSpace) 53 | nextSibling = ifElementParentChildren[ifElementChildIndex + 2] 54 | 55 | const siblingElseElement = 56 | nextSibling 57 | && nextSibling.type === 'JSXElement' 58 | && nextSibling.openingElement.name.type === 'JSXIdentifier' 59 | && nextSibling.openingElement.name.name === 'Else' 60 | && nextSibling as JSXElement 61 | 62 | 63 | // Replace the If element with Show: 64 | 65 | // Change the opening element's name to 'Show' 66 | reference.name = showIdentifier.name 67 | // Change the closing element's name to 'Show' 68 | ;(ifElement.closingElement.name as JSXIdentifier).name = 69 | showIdentifier.name 70 | 71 | // Change the `cond` attribute to `when` 72 | const condAttribute = ifOpeningElement.attributes.find( 73 | attribute => { 74 | if (attribute.type !== 'JSXAttribute') return error(` 75 | Babel plugin solid-if-component error: the 'If' component 76 | can only have a 'cond' normal JSX attribute attribute. Spread 77 | attributes are not supported. 78 | `) 79 | return attribute.name.name === 'cond' 80 | } 81 | ) as JSXAttribute 82 | if (!condAttribute) return error(` 83 | Babel plugin solid-if-component error: the 'If' component must have a 84 | 'cond' attribute. 85 | `) 86 | if (condAttribute.name.type === 'JSXNamespacedName') return error(` 87 | Babel plugin solid-if-component error: the 'If' component cannot have a 88 | namespaced 'cond' attribute. 89 | `) 90 | condAttribute.name.name = 'when' 91 | 92 | 93 | if (siblingElseElement) { 94 | // Add the Else element's children to the If element's fallback attribute 95 | const elseChildren = siblingElseElement.children 96 | ifOpeningElement.attributes.push( 97 | createJsxAttribute( 98 | createJsxIdentifier('fallback'), 99 | creteJsxFragment( 100 | createJsxOpeningFragment(), 101 | createJsxClosingFragment(), 102 | elseChildren 103 | ) 104 | ) 105 | ) 106 | 107 | // Remove the Else element 108 | ifElementParentChildren.splice( 109 | ifElementChildIndex + (nextSiblingIsWhiteSpace ? 2 : 1), 110 | 1 111 | ) 112 | } 113 | } 114 | --------------------------------------------------------------------------------