├── .commitlintrc ├── .eslintignore ├── .eslintrc ├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── babel.config.js ├── examples └── next-app │ ├── README.md │ ├── jsnx.config.js │ ├── next.config.js │ ├── package.json │ └── src │ ├── components │ └── Header │ │ ├── header.jsnx │ │ └── index.js │ └── pages │ └── index.jsnx ├── jest.config.js ├── jsconfig.json ├── lerna.json ├── package.json ├── packages ├── jsnx │ ├── .babelrc │ ├── __tests__ │ │ └── index.spec.js │ ├── package.json │ └── src │ │ ├── estree-to-js.js │ │ ├── index.js │ │ └── json-to-estree.js ├── loader │ ├── .babelrc │ ├── __fixtures__ │ │ └── greeting.fixture.jsnx │ ├── __tests__ │ │ └── index.spec.js │ ├── jsnx.config.js │ ├── package.json │ └── src │ │ └── index.js └── nextjs │ ├── .babelrc │ ├── README.md │ ├── index.js │ └── package.json └── yarn.lock /.commitlintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .log 2 | .DS_Store 3 | .jest-* 4 | lib 5 | node_modules 6 | build 7 | examples/*/.next 8 | lerna-debug.log 9 | 10 | **/yarn-error.log 11 | **/.env 12 | **/.out 13 | **/.storybook-static 14 | **/.next 15 | **/mock/data 16 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true, 6 | "jest": true 7 | }, 8 | "extends": [ 9 | "airbnb", 10 | "plugin:import/recommended", 11 | "plugin:react/recommended", 12 | "plugin:prettier/recommended", 13 | "plugin:sonarjs/recommended" 14 | ], 15 | "parser": "babel-eslint", 16 | "parserOptions": { 17 | "allowImportExportEverywhere": true, 18 | "ecmaFeatures": { 19 | "jsx": true 20 | }, 21 | "ecmaVersion": 2018, 22 | "sourceType": "module" 23 | }, 24 | "settings": { 25 | "import/resolver": { 26 | "alias": [ 27 | ["@jsnx-js/jsnx", ["./packages/jsnx"]], 28 | ["@jsnx-js/loader", ["./packages/loader"]], 29 | ["@jsnx-js/nextjs", ["./packages/nextjs"]] 30 | ], 31 | "node": { 32 | "extensions": [".js", ".jsx", ".jsnx"] 33 | }, 34 | "json": { 35 | "extensions": [".jsnx"] 36 | } 37 | } 38 | }, 39 | "plugins": [ 40 | "react", "sonarjs" 41 | ], 42 | "rules": { 43 | "import/prefer-default-export": "off", 44 | "strict": 0, 45 | "react/no-children-prop": "off", 46 | "react/react-in-jsx-scope": "off", 47 | "react/jsx-props-no-spreading": "off", 48 | "switch-colon-spacing": [ 49 | 2, 50 | { 51 | "after": false, 52 | "before": false 53 | } 54 | ], 55 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], 56 | "import/no-unresolved": [ 57 | 2, 58 | { 59 | "commonjs": true, 60 | "amd": true 61 | } 62 | ], 63 | "import/extensions": [ 64 | "error", 65 | { 66 | "jsnx": "never" 67 | } 68 | ], 69 | "import/no-extraneous-dependencies": [ 70 | "warn", 71 | { 72 | "devDependencies": true, 73 | "optionalDependencies": true, 74 | "peerDependencies": true 75 | } 76 | ], 77 | "react/jsx-indent": ["error", 2], 78 | "react/prop-types": ["error"], 79 | "prettier/prettier": ["error"], 80 | "no-console": [ 81 | "error", 82 | { 83 | "allow": ["error"] 84 | } 85 | ] 86 | }, 87 | "overrides": [ 88 | { 89 | "files": [ 90 | "*.test.js" 91 | ], 92 | "rules": { 93 | "sonarjs/no-duplicate-string": "off", 94 | "sonarjs/no-identical-functions": "off" 95 | } 96 | } 97 | ] 98 | } 99 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | **/node_modules 5 | **/.pnp 6 | **/.pnp.js 7 | 8 | # testing 9 | **/.jest-* 10 | 11 | # next.js 12 | **/.next/ 13 | **/out/ 14 | 15 | # production 16 | **/build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | # lock filess 37 | package-lock.json 38 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at flavioribeiro.tech@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Flavio Ribeiro 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JNSX JS 2 | 3 | Parse json structure file to react component using [Abstract Syntax API](https://en.wikipedia.org/wiki/Abstract_syntax_tree). 4 | 5 | [![lerna](https://img.shields.io/badge/maintained%20with-lerna-cc00ff.svg)](https://lerna.js.org/) 6 | [![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/jsnx-js/jsnx/blob/main/LICENSE) 7 | 8 | ## Example Usage 9 | 10 | - [Next.js](packages/nextjs/README.md) 11 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | babelrcRoots: ['./packages/*', './examples/*'], 3 | plugins: [ 4 | [ 5 | 'module-resolver', 6 | { 7 | alias: { 8 | '@jsnx-js/jsnx': './packages/jsnx', 9 | '@jsnx-js/loader': './packages/loader', 10 | '@jsnx-js/nextjs': './packages/nextjs', 11 | }, 12 | }, 13 | ], 14 | ], 15 | }; 16 | -------------------------------------------------------------------------------- /examples/next-app/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/import?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /examples/next-app/jsnx.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | componentsPath: '@material-ui/core', 3 | }; 4 | -------------------------------------------------------------------------------- /examples/next-app/next.config.js: -------------------------------------------------------------------------------- 1 | const withJSNX = require('@jsnx-js/nextjs'); 2 | 3 | module.exports = withJSNX({ 4 | pageExtensions: ['js', 'jsx', 'jsnx'], 5 | }); 6 | -------------------------------------------------------------------------------- /examples/next-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-app", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start" 9 | }, 10 | "dependencies": { 11 | "@jsnx-js/nextjs": "*", 12 | "@material-ui/core": "^4.11.2", 13 | "next": "10.0.5", 14 | "react": "17.0.1", 15 | "react-dom": "17.0.1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/next-app/src/components/Header/header.jsnx: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Header", 3 | "components": [ 4 | { 5 | "name": "LinearProgress", 6 | "type": "module" 7 | }, 8 | { 9 | "name": "Breadcrumbs", 10 | "custom": { 11 | "source": "@material-ui/core/Breadcrumbs" 12 | }, 13 | "props": { 14 | "aria-label": "breadcrumb", 15 | "children": [ 16 | { 17 | "name": "Link", 18 | "custom": { 19 | "source": "next/link", 20 | "name": "NextLink" 21 | }, 22 | "props": { 23 | "href": "/", 24 | "children": "Material-UI" 25 | } 26 | }, 27 | { 28 | "name": "Link", 29 | "type": "module", 30 | "props": { 31 | "href": "/getting-started/installation/", 32 | "children": "Core" 33 | } 34 | }, 35 | { 36 | "name": "Typography", 37 | "type": "module", 38 | "props": { 39 | "color": "textPrimary", 40 | "children": "Breadcrumb" 41 | } 42 | } 43 | ] 44 | } 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /examples/next-app/src/components/Header/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './header'; 2 | -------------------------------------------------------------------------------- /examples/next-app/src/pages/index.jsnx: -------------------------------------------------------------------------------- 1 | { 2 | "name": "HomePage", 3 | "components": [ 4 | { 5 | "name": "Header", 6 | "custom": { 7 | "source": "../components/Header", 8 | "name": "HeaderComponent" 9 | } 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collectCoverage: true, 3 | projects: ['./packages/*', './examples/*'], 4 | cacheDirectory: '.jest-cache', 5 | coverageDirectory: '.jest-coverage', 6 | testPathIgnorePatterns: ['/node_modules/', '/.next/', '/build/'], 7 | coverageReporters: ['html', 'lcov', 'text-summary'], 8 | coveragePathIgnorePatterns: ['./node_modules/', './build/'], 9 | moduleDirectories: ['node_modules', ''], 10 | moduleNameMapper: { 11 | '^@jsnx-js/(.*)$': '/pacakges/$1/src', 12 | }, 13 | coverageThreshold: { 14 | global: { 15 | lines: 95, 16 | branches: 95, 17 | functions: 95, 18 | statements: 95, 19 | }, 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6" 5 | }, 6 | "exclude": ["**/build/", "node_modules"] 7 | } 8 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "!packages/nextjs", 4 | "packages/*", 5 | "examples/*" 6 | ], 7 | "npmClient": "yarn", 8 | "useWorkspaces": true, 9 | "version": "0.0.1", 10 | "command": { 11 | "publish": { 12 | "conventionalCommits": true, 13 | "yes": true 14 | }, 15 | "version": { 16 | "message": "chore(release): publish %s", 17 | "allowBranch": "main", 18 | "ignoreChanges": ["**/__fixtures__/**", "**/__tests__/**", "**/*.md"] 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "version": "0.0.1", 4 | "private": true, 5 | "workspaces": [ 6 | "packages/*", 7 | "examples/*" 8 | ], 9 | "scripts": { 10 | "test": "jest", 11 | "lint": "eslint ./packages", 12 | "build": "lerna run --ignore @jsnx-js/nextjs --parallel --no-bail build", 13 | "clean": "lerna exec --parallel -- rimraf build .next node_modules", 14 | "postinstall": "lerna bootstrap", 15 | "dev:next-app": "cd examples/next-app && yarn dev", 16 | "start:next-app": "cd examples/next-app && yarn start -p $PORT" 17 | }, 18 | "dependencies": {}, 19 | "devDependencies": { 20 | "@babel/core": "^7.12.10", 21 | "@babel/plugin-syntax-jsx": "^7.12.1", 22 | "@babel/plugin-transform-react-jsx": "^7.12.12", 23 | "@babel/plugin-transform-runtime": "^7.12.10", 24 | "@babel/preset-env": "^7.12.11", 25 | "@babel/preset-react": "^7.12.10", 26 | "@commitlint/cli": "^11.0.0", 27 | "@commitlint/config-conventional": "^11.0.0", 28 | "@testing-library/jest-dom": "^5.11.9", 29 | "@testing-library/react": "^11.2.3", 30 | "@testing-library/react-hooks": "^5.0.0", 31 | "babel-eslint": "^10.1.0", 32 | "babel-loader": "^8.2.2", 33 | "babel-plugin-module-resolver": "^4.1.0", 34 | "eslint": "^7.18.0", 35 | "eslint-config-airbnb": "^18.2.1", 36 | "eslint-config-prettier": "^7.2.0", 37 | "eslint-import-resolver-alias": "^1.1.2", 38 | "eslint-plugin-babel": "^5.3.1", 39 | "eslint-plugin-import": "^2.22.1", 40 | "eslint-plugin-jsx-a11y": "^6.4.1", 41 | "eslint-plugin-prettier": "^3.3.1", 42 | "eslint-plugin-react": "^7.22.0", 43 | "eslint-plugin-sonarjs": "^0.5.0", 44 | "husky": "^4.3.8", 45 | "jest": "^26.6.3", 46 | "json-loader": "^0.5.7", 47 | "lerna": "^3.22.1", 48 | "prettier": "^2.2.1", 49 | "react-test-renderer": "^17.0.1", 50 | "rimraf": "^3.0.2" 51 | }, 52 | "husky": { 53 | "hooks": { 54 | "pre-commit": "yarn lint", 55 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" 56 | } 57 | }, 58 | "prettier": { 59 | "trailingComma": "es5", 60 | "tabWidth": 2, 61 | "semi": true, 62 | "singleQuote": true, 63 | "arrowParens": "avoid", 64 | "printWidth": 90, 65 | "bracketSpacing": true 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/jsnx/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/env", 5 | { 6 | "targets": { 7 | "node": "current" 8 | } 9 | } 10 | ], 11 | "@babel/react" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/jsnx/__tests__/index.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from '@testing-library/react'; 3 | import { transformAsync } from '@babel/core'; 4 | 5 | import jsnx, { sync } from '../src'; 6 | 7 | async function run(value, options = {}) { 8 | const doc = await jsnx(value, { ...options, skipExport: true }); 9 | 10 | // …and that into serialized JS. 11 | const { code } = await transformAsync(doc, { 12 | plugins: ['@babel/plugin-transform-react-jsx'], 13 | }); 14 | 15 | // …and finally run it, returning the component. 16 | // eslint-disable-next-line no-new-func 17 | return new Function('React', `${code}; return JSNXContent`)(React); 18 | } 19 | 20 | const component = { 21 | name: 'p', 22 | props: { 23 | children: 'Hello, World!', 24 | }, 25 | }; 26 | 27 | describe('JSNX', () => { 28 | const mockComponent =

Hello, World!

; 29 | 30 | it('should contruct correct jsx', async () => { 31 | const result = await jsnx( 32 | JSON.stringify({ 33 | components: [component], 34 | }) 35 | ); 36 | 37 | expect(result).toEqual( 38 | `/* @jsxRuntime classic */\nfunction JSNXContent(props) {\n return

Hello, World!

;\n}\nexport default JSNXContent;\n` 39 | ); 40 | }); 41 | 42 | it('should contruct correct jsx when sync', () => { 43 | const result = sync( 44 | JSON.stringify({ 45 | components: [component], 46 | }) 47 | ); 48 | 49 | expect(result).toEqual( 50 | `/* @jsxRuntime classic */\nfunction JSNXContent(props) {\n return

Hello, World!

;\n}\nexport default JSNXContent;\n` 51 | ); 52 | }); 53 | 54 | it('should render correctly', async () => { 55 | const Greeting = await run( 56 | JSON.stringify({ 57 | components: [component], 58 | }) 59 | ); 60 | const { container } = render(); 61 | expect(container).toEqual(render(mockComponent).container); 62 | }); 63 | 64 | it('should render correctly with filepath', async () => { 65 | const Greeting = await run( 66 | JSON.stringify({ 67 | components: [component], 68 | }), 69 | { 70 | filepath: 'index.jsnx', 71 | } 72 | ); 73 | const { container } = render(); 74 | expect(container).toEqual(render(mockComponent).container); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /packages/jsnx/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jsnx-js/jsnx", 3 | "version": "0.0.1", 4 | "main": "build/index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "build": "NODE_ENV=production BABEL_ENV=esm babel ./src --out-dir ./build" 8 | }, 9 | "dependencies": { 10 | "astring": "^1.6.0", 11 | "unified": "^9.2.0" 12 | }, 13 | "devDependencies": { 14 | "@babel/core": "^7.12.10", 15 | "@testing-library/react": "^11.2.3", 16 | "react": "^17.0.1", 17 | "react-dom": "^17.0.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/jsnx/src/estree-to-js.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-plusplus */ 2 | import { baseGenerator, generate } from 'astring'; 3 | 4 | // `attr="something"` 5 | function JSXAttribute(node, state) { 6 | state.write(' '); 7 | this[node.name.type](node.name, state); 8 | 9 | if (node.value != null) { 10 | state.write('='); 11 | this[node.value.type](node.value, state); 12 | } 13 | } 14 | 15 | // `` 16 | function JSXClosingElement(node, state) { 17 | this[node.name.type](node.name, state); 18 | } 19 | 20 | // `` 21 | function JSXClosingFragment(node, state) { 22 | state.write(''); 23 | } 24 | 25 | // `
` 26 | function JSXElement(node, state) { 27 | state.write('<'); 28 | this[node.openingElement.type](node.openingElement, state); 29 | if (node.closingElement) { 30 | state.write('>'); 31 | let index = -1; 32 | 33 | while (++index < node.children.length) { 34 | this[node.children[index].type](node.children[index], state); 35 | } 36 | 37 | state.write(''); 40 | } else { 41 | state.write(' />'); 42 | } 43 | } 44 | 45 | // `<>` 46 | function JSXFragment(node, state) { 47 | this[node.openingFragment.type](node.openingElement, state); 48 | 49 | let index = -1; 50 | 51 | while (++index < node.children.length) { 52 | this[node.children[index].type](node.children[index], state); 53 | } 54 | 55 | /* istanbul ignore if - incorrect tree. */ 56 | if (!node.closingFragment) { 57 | throw new Error('Cannot handle fragment w/o closing tag'); 58 | } 59 | 60 | this[node.closingFragment.type](node.closingElement, state); 61 | } 62 | 63 | // `{}` 64 | function JSXEmptyExpression() {} 65 | 66 | // `{expression}` 67 | function JSXExpressionContainer(node, state) { 68 | state.write('{'); 69 | this[node.expression.type](node.expression, state); 70 | state.write('}'); 71 | } 72 | 73 | // `
` 74 | function JSXOpeningElement(node, state) { 75 | let index = -1; 76 | 77 | this[node.name.type](node.name, state); 78 | 79 | while (++index < node.attributes.length) { 80 | this[node.attributes[index].type](node.attributes[index], state); 81 | } 82 | } 83 | 84 | // `<>` 85 | function JSXOpeningFragment(node, state) { 86 | state.write('<>'); 87 | } 88 | 89 | // `div` 90 | function JSXIdentifier(node, state) { 91 | state.write(node.name); 92 | } 93 | 94 | // `member.expression` 95 | function JSXMemberExpression(node, state) { 96 | this[node.object.type](node.object, state); 97 | state.write('.'); 98 | this[node.property.type](node.property, state); 99 | } 100 | 101 | // `ns:attr="something"` 102 | /* istanbul ignore next - JSNX (and most JSX things) don’t support them. 103 | * But keep it here just in case we might in the future. 104 | */ 105 | function JSXNamespacedName(node, state) { 106 | this[node.namespace.type](node.namespace, state); 107 | state.write(':'); 108 | this[node.name.type](node.name, state); 109 | } 110 | 111 | // `{...argument}` 112 | function JSXSpreadAttribute(node, state) { 113 | state.write(' {'); 114 | /* eslint-disable-next-line new-cap */ 115 | this.SpreadElement(node, state); 116 | state.write('}'); 117 | } 118 | 119 | // `!` 120 | function JSXText(node, state) { 121 | /* istanbul ignore next - `raw` is currently always be set, but could be 122 | * missing if something injects a `JSXText` into the tree. 123 | * Preferring `raw` over `value` means character references are kept as-is. 124 | */ 125 | const value = node.raw || node.value; 126 | state.write(value); 127 | } 128 | 129 | const customGenerator = { 130 | ...baseGenerator, 131 | JSXAttribute, 132 | JSXClosingElement, 133 | JSXClosingFragment, 134 | JSXElement, 135 | JSXEmptyExpression, 136 | JSXExpressionContainer, 137 | JSXFragment, 138 | JSXIdentifier, 139 | JSXMemberExpression, 140 | JSXNamespacedName, 141 | JSXOpeningElement, 142 | JSXOpeningFragment, 143 | JSXSpreadAttribute, 144 | JSXText, 145 | }; 146 | 147 | function estreeToJs(estree) { 148 | return generate(estree, { generator: customGenerator }); 149 | } 150 | 151 | export default estreeToJs; 152 | -------------------------------------------------------------------------------- /packages/jsnx/src/index.js: -------------------------------------------------------------------------------- 1 | import unified from 'unified'; 2 | import jsonToEstree from './json-to-estree'; 3 | 4 | const pragma = `/* @jsxRuntime classic */`; 5 | 6 | export function createCompiler(options) { 7 | const { plugins, ...opts } = options; 8 | return unified().use(jsonToEstree, opts).use({ plugins }); 9 | } 10 | 11 | function createConfig(json, options) { 12 | const config = { contents: json }; 13 | 14 | if (options.filepath) { 15 | config.path = options.filepath; 16 | } 17 | 18 | return config; 19 | } 20 | 21 | export function sync(json, options = {}) { 22 | const file = createCompiler(options).processSync(createConfig(json, options)); 23 | return `${pragma}\n${String(file)}`; 24 | } 25 | 26 | async function compile(json, options = {}) { 27 | const file = await createCompiler(options).process(createConfig(json, options)); 28 | return `${pragma}\n${String(file)}`; 29 | } 30 | 31 | export default compile; 32 | -------------------------------------------------------------------------------- /packages/jsnx/src/json-to-estree.js: -------------------------------------------------------------------------------- 1 | import estreeToJs from './estree-to-js'; 2 | 3 | function createJSONImport(specifiers, path) { 4 | return { 5 | type: 'ImportDeclaration', 6 | specifiers, 7 | source: { 8 | type: 'Literal', 9 | value: path, 10 | raw: JSON.stringify(path), 11 | }, 12 | importKind: 'value', 13 | }; 14 | } 15 | 16 | function createJSONImports(imports, options) { 17 | if (imports.length) { 18 | return imports.map(({ name, custom }) => { 19 | const { source, name: rename } = custom || {}; 20 | 21 | const specifier = { 22 | type: 'ImportSpecifier', 23 | imported: { type: 'Identifier', optional: false, name }, 24 | local: { type: 'Identifier', optional: false, name: rename || name }, 25 | }; 26 | 27 | if (source) { 28 | specifier.type = 'ImportDefaultSpecifier'; 29 | } 30 | 31 | return createJSONImport([specifier], source || options.componentsPath); 32 | }); 33 | } 34 | 35 | return []; 36 | } 37 | 38 | function createJSONProgram() { 39 | return { 40 | type: 'Program', 41 | body: [], 42 | }; 43 | } 44 | 45 | function createJSONComponent(component, hasChildren) { 46 | if (!component) return null; 47 | 48 | if (typeof component === 'string') { 49 | return { 50 | type: 'JSXText', 51 | value: component, 52 | raw: component, 53 | }; 54 | } 55 | 56 | const { children, ...props } = component?.props || {}; 57 | const { name: rename } = component?.custom || {}; 58 | const attributes = Object.keys(props).map(name => ({ 59 | type: 'JSXAttribute', 60 | name: { type: 'JSXIdentifier', name }, 61 | value: { 62 | type: 'Literal', 63 | value: props[name], 64 | raw: JSON.stringify(props[name]), 65 | }, 66 | })); 67 | 68 | return { 69 | type: 'JSXElement', 70 | openingElement: { 71 | type: 'JSXOpeningElement', 72 | name: { 73 | type: 'JSXIdentifier', 74 | name: rename || component.name, 75 | }, 76 | attributes: [ 77 | { 78 | type: 'JSXSpreadAttribute', 79 | argument: { type: 'Identifier', name: 'props' }, 80 | }, 81 | ...attributes, 82 | ], 83 | selfClosing: !hasChildren, 84 | }, 85 | ...(hasChildren && { 86 | closingElement: { 87 | type: 'JSXClosingElement', 88 | name: { 89 | type: 'JSXIdentifier', 90 | name: rename || component.name, 91 | }, 92 | }, 93 | }), 94 | children: Array.isArray(children) 95 | ? children.map(child => createJSONComponent(child, child?.props?.children)) 96 | : [createJSONComponent(children)], 97 | }; 98 | } 99 | 100 | function createJSONContent(children, name) { 101 | return { 102 | type: 'FunctionDeclaration', 103 | id: { type: 'Identifier', name, optional: false }, 104 | expression: false, 105 | generator: false, 106 | async: false, 107 | params: [ 108 | { 109 | type: 'Identifier', 110 | name: 'props', 111 | optional: false, 112 | }, 113 | ], 114 | body: { 115 | type: 'BlockStatement', 116 | body: [ 117 | { 118 | type: 'ReturnStatement', 119 | argument: 120 | children.length > 1 121 | ? { 122 | type: 'JSXFragment', 123 | openingFragment: { type: 'JSXOpeningFragment' }, 124 | closingFragment: { type: 'JSXClosingFragment' }, 125 | children, 126 | } 127 | : children[0], 128 | }, 129 | ], 130 | }, 131 | }; 132 | } 133 | 134 | function serializeEstree(estree) { 135 | return estreeToJs(estree); 136 | } 137 | 138 | function getImports(components) { 139 | return components.reduce((memo, component) => { 140 | if ( 141 | (component?.type === 'module' || component?.custom?.source) && 142 | !memo.find( 143 | ({ name }) => name === component.name && name === component?.custom?.name 144 | ) 145 | ) { 146 | memo.push(component); 147 | } 148 | 149 | if (Array.isArray(component?.props?.children)) { 150 | memo.push(...getImports(component.props.children)); 151 | } 152 | 153 | return memo; 154 | }, []); 155 | } 156 | 157 | function compile(options = {}) { 158 | function parse(tree) { 159 | const { components, name = 'JSNXContent' } = JSON.parse(tree); 160 | const imports = getImports(components); 161 | 162 | const JSONComponents = components.map(component => { 163 | return createJSONComponent(component, !!component?.props?.children); 164 | }); 165 | 166 | const estree = createJSONProgram(); 167 | 168 | estree.body = [ 169 | ...estree.body, 170 | ...createJSONImports(imports, options), 171 | createJSONContent(JSONComponents, name), 172 | !options.skipExport 173 | ? { 174 | type: 'ExportDefaultDeclaration', 175 | declaration: { 176 | type: 'Identifier', 177 | name, 178 | }, 179 | exportKind: 'value', 180 | } 181 | : null, 182 | ]; 183 | 184 | estree.body = estree.body.filter(state => state !== null); 185 | 186 | return estree; 187 | } 188 | 189 | function compiler(tree) { 190 | return serializeEstree(tree); 191 | } 192 | 193 | this.Compiler = compiler; 194 | this.Parser = parse; 195 | } 196 | 197 | export default compile; 198 | -------------------------------------------------------------------------------- /packages/loader/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/env", 5 | { 6 | "targets": { 7 | "node": "current" 8 | } 9 | } 10 | ], 11 | "@babel/react" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/loader/__fixtures__/greeting.fixture.jsnx: -------------------------------------------------------------------------------- 1 | { 2 | "components": [ 3 | { 4 | "name": "h1", 5 | "props": { 6 | "children": "Hello, world!" 7 | } 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /packages/loader/__tests__/index.spec.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { renderToStaticMarkup } from 'react-dom/server'; 3 | import { createFsFromVolume, Volume } from 'memfs'; 4 | import webpack from 'webpack'; 5 | import path from 'path'; 6 | 7 | const transform = (fixture, options) => { 8 | const compiler = webpack({ 9 | context: __dirname, 10 | entry: `../__fixtures__/${fixture}`, 11 | mode: 'none', 12 | module: { 13 | rules: [ 14 | { 15 | test: /\.jsnx$/, 16 | use: [ 17 | { 18 | loader: 'babel-loader', 19 | options: { 20 | plugins: [ 21 | '@babel/plugin-transform-runtime', 22 | '@babel/plugin-syntax-jsx', 23 | '@babel/plugin-transform-react-jsx', 24 | ], 25 | }, 26 | }, 27 | { loader: path.resolve(__dirname, '..', 'src'), options }, 28 | ], 29 | }, 30 | ], 31 | }, 32 | }); 33 | 34 | compiler.outputFileSystem = createFsFromVolume(new Volume()); 35 | compiler.outputFileSystem.join = path.join.bind(path); 36 | 37 | return new Promise((resolve, reject) => { 38 | compiler.run((err, stats) => { 39 | if (err) reject(err); 40 | if (stats.hasErrors()) reject(stats.toJson().errors); 41 | 42 | resolve( 43 | stats 44 | .toJson({ source: true }) 45 | .modules.find(m => m.name === `../__fixtures__/${fixture}`) 46 | ); 47 | }); 48 | }); 49 | }; 50 | 51 | const run = value => { 52 | const val = value 53 | .replace(/import React from 'react';/, '') 54 | .replace(/export default/, 'return'); 55 | 56 | // eslint-disable-next-line no-new-func 57 | return new Function('React', val)(React); 58 | }; 59 | 60 | describe('@jsnx-js/loader', () => { 61 | it('should support a file', async () => { 62 | const file = await transform('greeting.fixture.jsnx'); 63 | const Content = run(file.source); 64 | 65 | expect(renderToStaticMarkup()).toEqual('

Hello, world!

'); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /packages/loader/jsnx.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | componentsPath: '@components', 3 | }; 4 | -------------------------------------------------------------------------------- /packages/loader/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jsnx-js/loader", 3 | "version": "0.0.1", 4 | "main": "build/index.js", 5 | "license": "MIT", 6 | "scripts": { 7 | "build": "NODE_ENV=production BABEL_ENV=esm babel ./src --out-dir ./build" 8 | }, 9 | "dependencies": { 10 | "@jsnx-js/jsnx": "*", 11 | "loader-utils": "^2.0.0" 12 | }, 13 | "devDependencies": { 14 | "memfs": "^3.2.0", 15 | "memory-fs": "^0.5.0", 16 | "react": "^17.0.1", 17 | "react-dom": "^17.0.1", 18 | "webpack": "^5.16.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/loader/src/index.js: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'fs'; 2 | import { getOptions } from 'loader-utils'; 3 | import jsnx from '@jsnx-js/jsnx'; 4 | 5 | const CONFIG_FILE_NAME = 'jsnx.config.js'; 6 | const DEFAULT_RENDERER = `import React from 'react';`; 7 | 8 | async function getConfig() { 9 | let path = `${process.cwd()}/${CONFIG_FILE_NAME}`; 10 | 11 | if (!existsSync(path)) { 12 | path = `${__dirname}/../${CONFIG_FILE_NAME}`; 13 | } 14 | 15 | return import(path); 16 | } 17 | 18 | async function loader(content) { 19 | if (this.cacheable) this.cacheable(); 20 | const callback = this.async(); 21 | const config = await getConfig(); 22 | const options = { ...getOptions(this), ...config.default, filepath: this.resourcePath }; 23 | 24 | let value = typeof content === 'string' ? JSON.parse(content) : content; 25 | 26 | value = JSON.stringify(value) 27 | .replace(/\u2028/g, '\\u2028') 28 | .replace(/\u2029/g, '\\u2029'); 29 | 30 | let result; 31 | 32 | try { 33 | result = await jsnx(value, options); 34 | } catch (err) { 35 | return callback(err); 36 | } 37 | 38 | const { renderer = DEFAULT_RENDERER } = options; 39 | const code = `${renderer}\n${result}`; 40 | return callback(null, code); 41 | } 42 | 43 | export default loader; 44 | -------------------------------------------------------------------------------- /packages/nextjs/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["@babel/env", { "bugfixes": true }]] 3 | } -------------------------------------------------------------------------------- /packages/nextjs/README.md: -------------------------------------------------------------------------------- 1 | # JNSX JS - Next.JS 2 | 3 | ## Usage 4 | 5 | Here is a quick example to get you started, **it's all you need**: 6 | 7 | ```javascript 8 | const withJSNX = require('@jsnx-js/with-jsnx'); 9 | 10 | module.exports = withJSNX({ 11 | pageExtensions: ['js', 'jsx', 'jsnx'], 12 | }); 13 | ``` 14 | Here is a quick example with **options**: 15 | 16 | ```javascript 17 | // jsnx.config.js 18 | module.exports = { 19 | componentsPath: '@material-ui/core', 20 | }; 21 | ``` 22 | 23 | **File**: will returns a [React](https://reactjs.org/) functional component: 24 | 25 | ```json 26 | { 27 | "components": [ 28 | { 29 | "name": "LinearProgress", 30 | "imported": true 31 | }, 32 | { 33 | "name": "Breadcrumbs", 34 | "imported": "default", 35 | "from": "@material-ui/core/Breadcrumbs", 36 | "props": { 37 | "aria-label": "breadcrumb", 38 | "children": [ 39 | { 40 | "name": "Link", 41 | "imported": "default", 42 | "from": "next/link", 43 | "props": { 44 | "href": "/", 45 | "children": "Material-UI" 46 | } 47 | }, 48 | { 49 | "name": "Link", 50 | "imported": true, 51 | "asName": "MaterialLink", 52 | "props": { 53 | "href": "/getting-started/installation/", 54 | "children": "Core" 55 | } 56 | }, 57 | { 58 | "name": "Typography", 59 | "imported": "default", 60 | "props": { 61 | "color": "textPrimary", 62 | "children": "Breadcrumb" 63 | } 64 | } 65 | ] 66 | } 67 | } 68 | ] 69 | } 70 | -------------------------------------------------------------------------------- /packages/nextjs/index.js: -------------------------------------------------------------------------------- 1 | module.exports = (nextConfig = {}) => { 2 | return { 3 | ...nextConfig, 4 | webpack(config, options) { 5 | if (!options.defaultLoaders) { 6 | throw new Error( 7 | 'This plugin is not compatible with Next.js versions below 5.0.0 https://err.sh/next-plugins/upgrade' 8 | ); 9 | } 10 | 11 | // eslint-disable-next-line no-param-reassign 12 | options.defaultLoaders.jsnx = { 13 | loader: '@jsnx-js/loader', 14 | options: { 15 | skipExport: false, 16 | }, 17 | }; 18 | 19 | config.module.rules.push({ 20 | test: /\.jsnx$/, 21 | use: [options.defaultLoaders.babel, options.defaultLoaders.jsnx], 22 | }); 23 | 24 | config.resolve.extensions.push('.jsnx'); 25 | 26 | if (typeof nextConfig.webpack === 'function') { 27 | return nextConfig.webpack(config, options); 28 | } 29 | 30 | return config; 31 | }, 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /packages/nextjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jsnx-js/nextjs", 3 | "version": "0.0.1", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "@babel/cli": "^7.12.16", 8 | "@jsnx-js/loader": "*" 9 | } 10 | } 11 | --------------------------------------------------------------------------------