├── .nvmrc ├── www ├── static │ ├── .nojekyll │ └── img │ │ ├── logo.svg │ │ ├── favicon.svg │ │ └── logo_dark.svg ├── sidebars.js ├── babel.config.js ├── src │ ├── theme │ │ ├── PropsList │ │ │ ├── styles.module.css │ │ │ └── index.js │ │ ├── CodeLiveScope.js │ │ ├── CodeBlock.js │ │ └── Playground │ │ │ ├── index.js │ │ │ └── styles.module.css │ └── css │ │ └── custom.css ├── .gitignore ├── plugins │ ├── resolve-react.js │ └── docgen │ │ ├── jsDocHandler.js │ │ ├── doclets.js │ │ └── index.js ├── README.md ├── package.json ├── docusaurus.config.js └── docs │ └── index.mdx ├── .yarnrc.yml ├── test ├── tsconfig.json ├── setup.ts ├── transpile.test.ts ├── Provider.test.tsx └── sucrase.test.ts ├── src ├── highlight.ts ├── Error.tsx ├── InfoMessage.tsx ├── transpile.ts ├── LineNumber.tsx ├── transform │ ├── parser.d.ts │ ├── wrapLastExpression.ts │ ├── index.ts │ └── ImportTransformer.ts ├── index.tsx ├── Preview.tsx ├── useTest.ts ├── CodeBlock.tsx ├── prism.ts ├── ErrorBoundary.tsx ├── Editor.tsx ├── Provider.tsx └── SimpleEditor.tsx ├── vite.config.ts ├── rollup.config.js ├── vendor └── sucrase.js ├── tsconfig.json ├── .babelrc.cjs ├── patches ├── sucrase+3.35.0+001+add-exports.patch └── sucrase+3.35.0+002+top-level-scope-tracking.patch ├── .gitignore ├── README.md ├── package.json └── CHANGELOG.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 22.12.0 -------------------------------------------------------------------------------- /www/static/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /www/sidebars.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "..", 5 | "noImplicitAny": false 6 | }, 7 | "include": [".", "../src"] 8 | } 9 | -------------------------------------------------------------------------------- /www/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | require.resolve('@docusaurus/core/lib/babel/preset'), 4 | 5 | ['@babel/preset-typescript', { allowDeclareFields: true }], 6 | ], 7 | }; 8 | -------------------------------------------------------------------------------- /src/highlight.ts: -------------------------------------------------------------------------------- 1 | import { Prism } from 'prism-react-renderer' 2 | 3 | export default (code: string, language?: string) => { 4 | const grammar = language && Prism.languages[language] 5 | 6 | return grammar ? Prism.highlight(code, grammar, language as any) : code 7 | } 8 | -------------------------------------------------------------------------------- /www/src/theme/PropsList/styles.module.css: -------------------------------------------------------------------------------- 1 | .required { 2 | margin-left: 10px; 3 | font-size: 60%; 4 | background-color: var(--ifm-color-info-lightest); 5 | border-radius: var(--ifm-code-border-radius); 6 | padding: var(--ifm-code-padding-vertical) var(--ifm-code-padding-horizontal); 7 | } 8 | -------------------------------------------------------------------------------- /test/setup.ts: -------------------------------------------------------------------------------- 1 | import { cleanup } from '@testing-library/react'; 2 | import { vi } from 'vitest'; 3 | import { afterEach } from 'vitest'; 4 | 5 | // @ts-ignore 6 | window.__IMPORT__ = (s) => import(/* webpackIgnore: true */ s); 7 | 8 | afterEach(() => { 9 | cleanup(); 10 | vi.useRealTimers(); 11 | }); 12 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | environment: 'jsdom', 6 | setupFiles: './test/setup.ts', 7 | // TODO: remove include prop after complete Vitest migration 8 | include: ['test/**/*.{test,spec}.?(c|m)[jt]s?(x)'], 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /www/src/theme/CodeLiveScope.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import * as Jarle from '../../../src'; 4 | 5 | // Add react-live imports you need here 6 | const ReactLiveScope = { 7 | React, 8 | ...React, 9 | ReactDOM, 10 | Jarle, 11 | }; 12 | 13 | export default ReactLiveScope; 14 | -------------------------------------------------------------------------------- /www/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /src/Error.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { useError } from './Provider.js'; 4 | 5 | /** 6 | * Displays an syntax or runtime error that occurred when rendering the code 7 | */ 8 | export default function Error(props: React.HTMLProps) { 9 | const error = useError(); 10 | 11 | return error ?
{error.toString()}
: null; 12 | } 13 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | 4 | export default [ 5 | { 6 | input: 'vendor/sucrase.js', 7 | output: { 8 | file: 'src/transform/parser.js', 9 | format: 'es', 10 | }, 11 | plugins: [ 12 | resolve(), // so Rollup can find `ms` 13 | commonjs(), // so Rollup can convert `ms` to an ES module 14 | ], 15 | }, 16 | ]; 17 | -------------------------------------------------------------------------------- /vendor/sucrase.js: -------------------------------------------------------------------------------- 1 | import { transform, getSucraseContext, RootTransformer } from 'sucrase/'; 2 | import { TokenType as tt } from 'sucrase/dist/parser/tokenizer/types'; 3 | import CJSImportProcessor from 'sucrase/dist/CJSImportProcessor'; 4 | import computeSourceMap from 'sucrase/dist/computeSourceMap'; 5 | import { parse } from 'sucrase/dist/esm/parser'; 6 | 7 | export { 8 | RootTransformer, 9 | tt, 10 | parse, 11 | getSucraseContext, 12 | CJSImportProcessor, 13 | transform, 14 | computeSourceMap, 15 | }; 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "src", 4 | "target": "esnext", 5 | "useDefineForClassFields": true, 6 | "lib": ["esnext", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | "esModuleInterop": true, 9 | "allowSyntheticDefaultImports": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "jsx": "react-jsx", 13 | 14 | /* Linting */ 15 | "strict": true, 16 | "noImplicitAny": false, 17 | "module": "NodeNext", 18 | "moduleResolution": "NodeNext" 19 | }, 20 | "include": ["src"] 21 | } 22 | -------------------------------------------------------------------------------- /.babelrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = (api) => { 2 | return { 3 | presets: [ 4 | [ 5 | '@babel/preset-env', 6 | { 7 | bugfixes: true, 8 | shippedProposals: true, 9 | // exclude: ['dynamic-import'], 10 | modules: api.env() !== 'esm' ? 'commonjs' : false, 11 | targets: { esmodules: true }, 12 | }, 13 | ], 14 | ['@babel/preset-react', { runtime: 'automatic' }], 15 | ['@babel/preset-typescript', { allowDeclareFields: true }], 16 | ], 17 | plugins: ['@babel/plugin-syntax-dynamic-import'].filter(Boolean), 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /www/static/img/logo.svg: -------------------------------------------------------------------------------- 1 | 11 | 14 | 17 | 24 | 25 | -------------------------------------------------------------------------------- /www/static/img/favicon.svg: -------------------------------------------------------------------------------- 1 | 11 | 14 | 17 | 24 | 25 | -------------------------------------------------------------------------------- /www/static/img/logo_dark.svg: -------------------------------------------------------------------------------- 1 | 11 | 14 | 17 | 24 | 25 | -------------------------------------------------------------------------------- /src/InfoMessage.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const srOnlyStyle = { 4 | clip: 'rect(1px, 1px, 1px, 1px)', 5 | clipPath: 'inset(50%)', 6 | height: 1, 7 | width: 1, 8 | margin: -1, 9 | overflow: 'hidden', 10 | padding: 0, 11 | }; 12 | export default function InfoMessage({ 13 | srOnly = false, 14 | ...props 15 | }: React.HTMLProps & { srOnly?: boolean }) { 16 | return ( 17 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/transpile.ts: -------------------------------------------------------------------------------- 1 | import { Import, transform } from './transform/index.js'; 2 | 3 | export type Options = { 4 | inline?: boolean; 5 | isTypeScript?: boolean; 6 | showImports?: boolean; 7 | wrapper?: (code: string) => string; 8 | }; 9 | 10 | export default ( 11 | input: string, 12 | { inline = false, isTypeScript, showImports }: Options = {} 13 | ) => { 14 | let { code, imports } = transform(input, { 15 | removeImports: !showImports, 16 | wrapLastExpression: inline, 17 | transforms: isTypeScript ? ['typescript', 'jsx'] : ['jsx'], 18 | compiledFilename: 'compiled.js', 19 | filename: 'example.js', 20 | }); 21 | 22 | return { code, imports }; 23 | }; 24 | -------------------------------------------------------------------------------- /src/LineNumber.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const lineNumberStyle = { 4 | textAlign: 'right', 5 | userSelect: 'none', 6 | pointerEvents: 'none', 7 | paddingRight: 12, 8 | } as const; 9 | 10 | function LineNumber({ children, className, theme = true, style }: any) { 11 | return ( 12 | 25 | {children} 26 | 27 | ); 28 | } 29 | 30 | export default LineNumber; 31 | -------------------------------------------------------------------------------- /src/transform/parser.d.ts: -------------------------------------------------------------------------------- 1 | import { Options, SucraseContext } from 'sucrase/'; 2 | 3 | export { default as TokenProcessor } from 'sucrase/dist/types/TokenProcessor'; 4 | export { default as RootTransformer } from 'sucrase/dist/types/transformers/RootTransformer'; 5 | export { parse } from 'sucrase/dist/types/parser'; 6 | export { transform } from 'sucrase/'; 7 | export { TokenType as tt } from 'sucrase/dist/types/parser/tokenizer/types'; 8 | export { default as CJSImportProcessor } from 'sucrase/dist/types/CJSImportProcessor'; 9 | export { default as computeSourceMap } from 'sucrase/dist/types/computeSourceMap'; 10 | 11 | export function getSucraseContext( 12 | code: string, 13 | options: Options 14 | ): SucraseContext; 15 | -------------------------------------------------------------------------------- /www/plugins/resolve-react.js: -------------------------------------------------------------------------------- 1 | module.exports = () => ({ 2 | name: 'resolve-react', 3 | configureWebpack() { 4 | return { 5 | devtool: 'inline-source-map', 6 | devServer: { 7 | client: { 8 | overlay: { 9 | runtimeErrors: false, 10 | }, 11 | }, 12 | }, 13 | resolve: { 14 | extensionAlias: { 15 | '.js': ['.js', '.ts', '.tsx'], 16 | }, 17 | alias: { 18 | react$: require.resolve('react'), 19 | 'react-dom$': require.resolve('react-dom'), 20 | 'react-dom/server': require.resolve('react-dom/server'), 21 | 'react/jsx-runtime': require.resolve('react/jsx-runtime'), 22 | }, 23 | }, 24 | }; 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /www/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://v2.docusaurus.io/), a modern static website generator. 4 | 5 | ## Installation 6 | 7 | ```console 8 | yarn install 9 | ``` 10 | 11 | ## Local Development 12 | 13 | ```console 14 | yarn start 15 | ``` 16 | 17 | This command starts a local development server and open up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ## Build 20 | 21 | ```console 22 | yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ## Deployment 28 | 29 | ```console 30 | GIT_USER= USE_SSH=true yarn deploy 31 | ``` 32 | 33 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 34 | -------------------------------------------------------------------------------- /patches/sucrase+3.35.0+001+add-exports.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/sucrase/dist/esm/index.js b/node_modules/sucrase/dist/esm/index.js 2 | index 41a7aa0..61b1d3e 100644 3 | --- a/node_modules/sucrase/dist/esm/index.js 4 | +++ b/node_modules/sucrase/dist/esm/index.js 5 | @@ -131,3 +131,5 @@ function getSucraseContext(code, options) { 6 | } 7 | return {tokenProcessor, scopes, nameManager, importProcessor, helperManager}; 8 | } 9 | + 10 | +export { RootTransformer, getSucraseContext } 11 | diff --git a/node_modules/sucrase/dist/index.js b/node_modules/sucrase/dist/index.js 12 | index 6395245..8f12513 100644 13 | --- a/node_modules/sucrase/dist/index.js 14 | +++ b/node_modules/sucrase/dist/index.js 15 | @@ -131,3 +131,6 @@ function getSucraseContext(code, options) { 16 | } 17 | return {tokenProcessor, scopes, nameManager, importProcessor, helperManager}; 18 | } 19 | + 20 | +exports.getSucraseContext = getSucraseContext; 21 | +exports.RootTransformer = _RootTransformer2.default 22 | -------------------------------------------------------------------------------- /www/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "www", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "serve": "docusaurus serve", 12 | "clear": "docusaurus clear" 13 | }, 14 | "dependencies": { 15 | "@docpocalypse/props-table": "^0.3.11", 16 | "@docusaurus/core": "^3.7.0", 17 | "@docusaurus/preset-classic": "^3.7.0", 18 | "@mdx-js/react": "^3.0.0", 19 | "clsx": "^1.2.1", 20 | "globby": "^11.0.1", 21 | "react": "^19.1.0", 22 | "react-docgen": "^5.4.3", 23 | "react-dom": "^19.1.0", 24 | "tiny-case": "^1.0.3" 25 | }, 26 | "browserslist": { 27 | "production": [ 28 | ">0.5%", 29 | "not dead", 30 | "not op_mini all" 31 | ], 32 | "development": [ 33 | "last 1 chrome version", 34 | "last 1 firefox version", 35 | "last 1 safari version" 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Prism, 3 | themes, 4 | Highlight as PrismHighlight, 5 | } from 'prism-react-renderer'; 6 | 7 | import CodeBlock, { mapTokens } from './CodeBlock.js'; 8 | import Editor, { ControlledEditor } from './Editor.js'; 9 | import Error from './Error.js'; 10 | import InfoMessage from './InfoMessage.js'; 11 | import Preview from './Preview.js'; 12 | import Provider, { 13 | ImportResolver as _ImportResolver, 14 | useElement, 15 | useError, 16 | useLiveContext, 17 | useEditorConfig, 18 | useActions, 19 | useCode, 20 | } from './Provider.js'; 21 | import highlight from './highlight.js'; 22 | 23 | export type ImportResolver = _ImportResolver; 24 | 25 | export { 26 | Prism, 27 | PrismHighlight, 28 | CodeBlock, 29 | mapTokens, 30 | Error, 31 | Editor, 32 | ControlledEditor, 33 | Preview, 34 | Provider, 35 | InfoMessage, 36 | highlight, 37 | themes, 38 | useActions, 39 | useElement, 40 | useError, 41 | useCode, 42 | useEditorConfig, 43 | useLiveContext, 44 | }; 45 | 46 | export type { PrismTheme, Language } from 'prism-react-renderer'; 47 | -------------------------------------------------------------------------------- /www/src/theme/PropsList/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderProps from '@docpocalypse/props-table'; 3 | import Heading from '@theme/Heading'; 4 | 5 | import styles from './styles.module.css'; 6 | 7 | function PropsTable({ metadata }) { 8 | const props = renderProps(metadata.props || []); 9 | 10 | return ( 11 | <> 12 | {props.map((prop) => ( 13 | 14 | 15 | {prop.name} 16 | {prop.propData.required && ( 17 | required 18 | )} 19 | 20 | 21 | {React.createElement(prop.propData.description)} 22 |
23 |
24 | type: {prop.type} 25 |
26 | {prop.defaultValue && ( 27 |
28 | default: 29 | {prop.defaultValue} 30 |
31 | )} 32 |
33 |
34 | ))} 35 | 36 | ); 37 | } 38 | 39 | export default PropsTable; 40 | -------------------------------------------------------------------------------- /www/src/theme/CodeBlock.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-unresolved */ 2 | 3 | import React from 'react'; 4 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 5 | import { usePrismTheme } from '@docusaurus/theme-common'; 6 | import Playground from '@theme/Playground'; 7 | import CodeLiveScope from '@theme/CodeLiveScope'; 8 | import CodeBlock from '@theme-original/CodeBlock'; 9 | 10 | const getLanguage = (className = '') => { 11 | const [, mode] = className.match(/language-(\w+)/) || []; 12 | return mode; 13 | }; 14 | 15 | const withLiveEditor = (Component) => { 16 | const WrappedComponent = (props) => { 17 | const { isClient } = useDocusaurusContext(); 18 | const prismTheme = usePrismTheme(); 19 | 20 | if (props.live) { 21 | const language = props.language || getLanguage(props.className); 22 | 23 | return ( 24 | 31 | ); 32 | } 33 | 34 | return ; 35 | }; 36 | 37 | return WrappedComponent; 38 | }; 39 | 40 | export default withLiveEditor(CodeBlock); 41 | -------------------------------------------------------------------------------- /www/plugins/docgen/jsDocHandler.js: -------------------------------------------------------------------------------- 1 | const Doclets = require('./doclets'); 2 | 3 | module.exports = function jsDocHandler(documentation) { 4 | const description = documentation.get('description') || ''; 5 | 6 | const displayName = documentation.get('displayName'); 7 | const tags = Doclets.parseTags(description); 8 | const displayNameTag = tags.find((t) => t.name === 'displayName'); 9 | 10 | documentation.set('name', displayName); 11 | 12 | if (displayNameTag) { 13 | documentation.set('displayName', displayNameTag.value || displayName); 14 | } 15 | 16 | documentation.set('docblock', description); 17 | documentation.set('description', Doclets.cleanTags(description)); 18 | 19 | documentation.set('tags', tags || []); 20 | 21 | // eslint-disable-next-line no-underscore-dangle 22 | documentation._props.forEach((_, name) => { 23 | const propDoc = documentation.getPropDescriptor(name); 24 | 25 | const propDescription = propDoc.description || ''; 26 | const propTags = Doclets.parseTags(propDescription); 27 | 28 | propDoc.docblock = propDescription; 29 | propDoc.description = Doclets.cleanTags(propDescription); 30 | propDoc.tags = propTags || []; 31 | 32 | Doclets.applyPropTags(propDoc); 33 | }); 34 | }; 35 | -------------------------------------------------------------------------------- /src/Preview.tsx: -------------------------------------------------------------------------------- 1 | import ErrorBoundary from './ErrorBoundary.js'; 2 | import { useElement, useError } from './Provider.js'; 3 | 4 | /** 5 | * The component that renders the user's code. 6 | */ 7 | const Preview = ({ 8 | className, 9 | showLastValid = true, 10 | preventLinks = true, 11 | ...props 12 | }: { 13 | className?: string; 14 | /** 15 | * Whether an error should reset the preview to an empty state or keep showing the last valid code result. 16 | */ 17 | showLastValid?: boolean; 18 | /** 19 | * Prevent links from navigating when clicked. 20 | */ 21 | preventLinks?: boolean; 22 | }) => { 23 | const element = useElement(); 24 | 25 | // prevent links in examples from navigating 26 | const handleClick = (e: any) => { 27 | if (preventLinks && (e.target.tagName === 'A' || e.target.closest('a'))) 28 | e.preventDefault(); 29 | }; 30 | 31 | const previewProps = { 32 | role: 'region', 33 | 'aria-label': 'Code Example', 34 | ...props, 35 | }; 36 | 37 | const children = ( 38 |
39 | 40 |
41 | ); 42 | 43 | if (!showLastValid) { 44 | return {children}; 45 | } 46 | 47 | return children; 48 | }; 49 | 50 | function HideOnError({ children }: { children: React.ReactNode }) { 51 | const error = useError(); 52 | 53 | return error ? null : <>{children}; 54 | } 55 | 56 | export default Preview; 57 | -------------------------------------------------------------------------------- /www/docusaurus.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | title: 'JARLE', 3 | tagline: 'Just Another React Live Editor', 4 | url: 'https://jquense.github.io.', 5 | baseUrl: '/jarle/', 6 | onBrokenLinks: 'throw', 7 | onBrokenMarkdownLinks: 'warn', 8 | favicon: 'img/favicon.svg', 9 | organizationName: 'jquense', // Usually your GitHub org/user name. 10 | projectName: 'jarle', // Usually your repo name. 11 | themeConfig: { 12 | colorMode: { 13 | disableSwitch: true, 14 | }, 15 | navbar: { 16 | title: 'JARLE', 17 | logo: { 18 | alt: 'Jarle Logo', 19 | src: 'img/logo.svg', 20 | srcDark: 'img/logo_dark.svg', 21 | }, 22 | items: [ 23 | { 24 | href: 'https://github.com/jquense/jarle', 25 | position: 'right', 26 | className: 'header-github-link', 27 | 'aria-label': 'GitHub repository', 28 | }, 29 | ], 30 | }, 31 | footer: { 32 | style: 'dark', 33 | links: [], 34 | copyright: `Copyright © ${new Date().getFullYear()} Jason Quense. Built with Docusaurus.`, 35 | }, 36 | }, 37 | presets: [ 38 | [ 39 | '@docusaurus/preset-classic', 40 | { 41 | docs: { 42 | routeBasePath: '/', 43 | sidebarPath: require.resolve('./sidebars.js'), 44 | }, 45 | blog: false, 46 | theme: { 47 | customCss: require.resolve('./src/css/custom.css'), 48 | }, 49 | }, 50 | ], 51 | ], 52 | plugins: [ 53 | // [ 54 | // require.resolve('./plugins/docgen'), 55 | // { 56 | // src: ['../src/**/*.tsx'], 57 | // }, 58 | // ], 59 | require.resolve('./plugins/resolve-react'), 60 | ], 61 | }; 62 | -------------------------------------------------------------------------------- /test/transpile.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import transpile from '../src/transpile.js'; 3 | 4 | describe('parseImports', () => { 5 | it('removes imports', () => { 6 | const result = transpile( 7 | ` 8 | import Foo from './foo.js' 9 | 10 | 11 | `, 12 | { showImports: false } 13 | ); 14 | 15 | expect(result.code).toMatchInlineSnapshot(` 16 | "const _jsxFileName = ""; 17 | 18 | 19 | React.createElement(Foo, {__self: this, __source: {fileName: _jsxFileName, lineNumber: 4}} ) 20 | " 21 | `); 22 | 23 | expect(result.imports).toMatchInlineSnapshot(` 24 | [ 25 | { 26 | "base": "Foo", 27 | "code": "var foo_js$0 = require('./foo.js'); var Foo = foo_js$0.default;", 28 | "keys": [], 29 | "source": "./foo.js", 30 | }, 31 | ] 32 | `); 33 | }); 34 | 35 | it('removes imports with Typescript', () => { 36 | const result = transpile( 37 | ` 38 | import Foo, { Bar } from './foo.js'; 39 | 40 | /> 41 | `, 42 | { showImports: false, isTypeScript: true } 43 | ); 44 | 45 | expect(result.code).toMatchInlineSnapshot(` 46 | "const _jsxFileName = ""; 47 | 48 | 49 | React.createElement(Foo, {__self: this, __source: {fileName: _jsxFileName, lineNumber: 4}} ) 50 | " 51 | `); 52 | 53 | expect(result.imports).toMatchInlineSnapshot(` 54 | [ 55 | { 56 | "base": "Foo", 57 | "code": "var foo_js$0 = require('./foo.js'); var Foo = foo_js$0.default;", 58 | "keys": [], 59 | "source": "./foo.js", 60 | }, 61 | ] 62 | `); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | themes/ 2 | 3 | # Created by https://www.gitignore.io/api/node 4 | # Edit at https://www.gitignore.io/?templates=node 5 | lib/ 6 | cjs/ 7 | public/ 8 | .cache/ 9 | ### Node ### 10 | # Logs 11 | logs 12 | *.log 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | lerna-debug.log* 17 | *.tsbuildinfo 18 | .yarn/ 19 | 20 | # Diagnostic reports (https://nodejs.org/api/report.html) 21 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 22 | 23 | # Runtime data 24 | pids 25 | *.pid 26 | *.seed 27 | *.pid.lock 28 | 29 | # Directory for instrumented libs generated by jscoverage/JSCover 30 | lib-cov 31 | 32 | # Coverage directory used by tools like istanbul 33 | coverage 34 | 35 | # nyc test coverage 36 | .nyc_output 37 | 38 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 39 | .grunt 40 | 41 | # Bower dependency directory (https://bower.io/) 42 | bower_components 43 | 44 | # node-waf configuration 45 | .lock-wscript 46 | 47 | # Compiled binary addons (https://nodejs.org/api/addons.html) 48 | build/Release 49 | 50 | # Dependency directories 51 | node_modules/ 52 | jspm_packages/ 53 | 54 | # TypeScript v1 declaration files 55 | typings/ 56 | 57 | # Optional npm cache directory 58 | .npm 59 | 60 | # Optional eslint cache 61 | .eslintcache 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | .env.test 75 | 76 | # parcel-bundler cache (https://parceljs.org/) 77 | .cache 78 | 79 | # next.js build output 80 | .next 81 | 82 | # nuxt.js build output 83 | .nuxt 84 | 85 | # vuepress build output 86 | .vuepress/dist 87 | 88 | # Serverless directories 89 | .serverless/ 90 | 91 | # FuseBox cache 92 | .fusebox/ 93 | 94 | # DynamoDB Local files 95 | .dynamodb/ 96 | 97 | # End of https://www.gitignore.io/api/node 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

J.A.R.L.E

2 |

3 | Just Another React Live Editor 4 |

5 | 6 | JARLE is a as lightweight but feature-rich React component editor with live 7 | preview. JARLE uses [sucrase](https://github.com/alangpierce/sucrase) for fast, minimal 8 | compilation of JSX and/or Typescript. 9 | 10 | ## Usage 11 | 12 | ```js 13 | import { Provider, Editor, Error, Preview } from 'jarle'; 14 | 15 | function LiveEditor() { 16 | return ( 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | } 24 | ``` 25 | 26 | See **https://jquense.github.io/jarle/** for docs. 27 | 28 | ### Rendering Code 29 | 30 | Jarle removes boilerplate code in your examples, by rendering the **last** expression in your code block. 31 | Define variables, components, whatever, as long as the last line is a JSX expression. 32 | 33 | ```js 34 | Hello {subject}
38 | } 39 | 40 | 41 | `} 42 | /> 43 | ``` 44 | 45 | If you do need more control over what get's rendered, or need to render asynchronously, a 46 | `render` function is always in scope: 47 | 48 | ```js 49 | setTimeout(() => { 50 | render(
I'm late!
); 51 | }, 1000); 52 | ``` 53 | 54 | Jarle also supports rendering your code _as a component_, helpful for illustrating 55 | hooks or render behavior with minimal extra code. When using `renderAsComponent` 56 | the code text is used as the body of React function component, so you can use 57 | state and other hooks. 58 | 59 | ```js 60 | { 66 | let interval = setInterval(() => { 67 | setSeconds(prev => prev + 1) 68 | }, 1000) 69 | 70 | return () => clearInterval(interval) 71 | }, []) 72 | 73 |
Seconds past: {secondsPast}
74 | `} 75 | /> 76 | ``` 77 | -------------------------------------------------------------------------------- /src/transform/wrapLastExpression.ts: -------------------------------------------------------------------------------- 1 | import { SucraseContext } from 'sucrase'; 2 | import { 3 | TokenProcessor, 4 | type CJSImportProcessor, 5 | type RootTransformer, 6 | tt, 7 | } from './parser.js'; 8 | 9 | function findLastExpression(tokens: TokenProcessor) { 10 | let lastExprIdx: number | null = null; 11 | 12 | for (let i = 0; i < tokens.tokens.length; i++) { 13 | if (tokens.matches2AtIndex(i, tt._export, tt._default)) { 14 | return null; 15 | } 16 | 17 | // @ts-ignore 18 | if (tokens.tokens[i].isTopLevel) { 19 | const code = tokens.code.slice( 20 | tokens.tokens[i].start, 21 | tokens.tokens[i].end 22 | ); 23 | 24 | if (code.startsWith('exports')) { 25 | return null; 26 | } 27 | if (tokens.matches1AtIndex(i, tt._return)) { 28 | return null; 29 | } 30 | 31 | lastExprIdx = i; 32 | } 33 | } 34 | 35 | return lastExprIdx; 36 | } 37 | 38 | function process(tokens: TokenProcessor, lastIndex: number) { 39 | if (tokens.currentIndex() !== lastIndex) { 40 | return false; 41 | } 42 | 43 | let prev = tokens.currentIndex() - 1; 44 | 45 | let lastWasSemi = prev >= 0 && !tokens.matches1AtIndex(prev, tt.semi); 46 | 47 | if (tokens.matches2(tt._export, tt._default)) { 48 | tokens.removeInitialToken(); 49 | tokens.replaceTokenTrimmingLeftWhitespace( 50 | lastWasSemi ? 'return' : '; return' 51 | ); 52 | } else { 53 | let code = `return ${tokens.currentTokenCode()}`; 54 | if (lastWasSemi) { 55 | code = `;${code}`; 56 | } 57 | tokens.replaceTokenTrimmingLeftWhitespace(code); 58 | } 59 | return true; 60 | } 61 | 62 | export default function wrapLastExpression({ tokenProcessor }: SucraseContext) { 63 | let lastExprIdx = findLastExpression(tokenProcessor); 64 | 65 | if (lastExprIdx == null) { 66 | return 67 | } 68 | 69 | while (!tokenProcessor.isAtEnd()) { 70 | let wasProcessed = process(tokenProcessor, lastExprIdx); 71 | 72 | if (!wasProcessed) { 73 | tokenProcessor.copyToken(); 74 | } 75 | } 76 | 77 | return tokenProcessor.finish(); 78 | } 79 | -------------------------------------------------------------------------------- /test/Provider.test.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable require-await */ 2 | 3 | import { fireEvent, render, act } from '@testing-library/react'; 4 | import { describe, it, expect } from 'vitest'; 5 | import Provider, { Props, useElement, useError } from '../src/Provider.js'; 6 | 7 | describe('Provider', () => { 8 | async function mountProvider(props: Props<{}>) { 9 | let wrapper: ReturnType; 10 | 11 | function Child() { 12 | const el = useElement(); 13 | const err = useError(); 14 | if (err) console.error(err); 15 | return el; 16 | } 17 | 18 | wrapper = render(} {...props} />); 19 | 20 | await act(async () => { 21 | wrapper.rerender(} {...props} />); 22 | }); 23 | 24 | return wrapper!; 25 | } 26 | 27 | it('should render', async () => { 28 | const wrapper = await mountProvider({ 29 | code: ` 30 |
31 | `, 32 | }); 33 | 34 | expect(wrapper.getByTestId('test')).toBeDefined(); 35 | }); 36 | 37 | it('should render function component', async () => { 38 | const wrapper = await mountProvider({ 39 | code: ` 40 | function Example() { 41 | return
42 | } 43 | `, 44 | }); 45 | 46 | expect(wrapper.getByTestId('test')).toBeDefined(); 47 | }); 48 | 49 | it('should render class component', async () => { 50 | const wrapper = await mountProvider({ 51 | code: ` 52 | class Example extends React.Component { 53 | render() { 54 | return
55 | } 56 | } 57 | `, 58 | }); 59 | 60 | expect(wrapper.getByTestId('test')).toBeDefined(); 61 | }); 62 | 63 | it('should renderAsComponent', async () => { 64 | const wrapper = await mountProvider({ 65 | renderAsComponent: true, 66 | code: ` 67 | const [count, setCount] = useState(1); 68 | 69 |
setCount(2)}>{count}
70 | `, 71 | }); 72 | 73 | const div = wrapper.getByTestId('test'); 74 | 75 | expect(div).toBeDefined(); 76 | expect(div.textContent).toEqual('1'); 77 | 78 | fireEvent.click(div); 79 | expect(div.textContent).toEqual('2'); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /src/useTest.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import useCommittedRef from '@restart/hooks/useCommittedRef'; 3 | 4 | /** 5 | * Creates a `setInterval` that is properly cleaned up when a component unmounted 6 | * 7 | * @public 8 | * @param fn an function run on each interval 9 | * @param ms The milliseconds duration of the interval 10 | */ 11 | function useInterval(fn: () => void, ms: number): void; 12 | 13 | /** 14 | * Creates a pausable `setInterval` that is properly cleaned up when a component unmounted 15 | * 16 | * @public 17 | * @param fn an function run on each interval 18 | * @param ms The milliseconds duration of the interval 19 | * @param paused Whether or not the interval is currently running 20 | */ 21 | function useInterval(fn: () => void, ms: number, paused: boolean): void; 22 | 23 | /** 24 | * Creates a pausable `setInterval` that is properly cleaned up when a component unmounted 25 | * 26 | * @public 27 | * @param fn an function run on each interval 28 | * @param ms The milliseconds duration of the interval 29 | * @param paused Whether or not the interval is currently running 30 | * @param runImmediately Whether to run the function immediately on mount or unpause 31 | * rather than waiting for the first interval to elapse 32 | */ 33 | function useInterval( 34 | fn: () => void, 35 | ms: number, 36 | paused: boolean, 37 | runImmediately: boolean 38 | ): void; 39 | 40 | function useInterval( 41 | fn: () => void, 42 | ms: number, 43 | paused = false, 44 | runImmediately = false 45 | ): void { 46 | let handle: number; 47 | const fnRef = useCommittedRef(fn); 48 | // this ref is necessary b/c useEffect will sometimes miss a paused toggle 49 | // orphaning a setTimeout chain in the aether, so relying on it's refresh logic is not reliable. 50 | const pausedRef = useCommittedRef(paused); 51 | const tick = () => { 52 | if (pausedRef.current) return; 53 | fnRef.current(); 54 | schedule(); // eslint-disable-line no-use-before-define 55 | }; 56 | 57 | const schedule = () => { 58 | clearTimeout(handle); 59 | handle = setTimeout(tick, ms) as any; 60 | }; 61 | 62 | useEffect(() => { 63 | if (runImmediately) { 64 | tick(); 65 | } else { 66 | schedule(); 67 | } 68 | return () => clearTimeout(handle); 69 | }, [paused, runImmediately]); 70 | } 71 | 72 | export default useInterval; 73 | -------------------------------------------------------------------------------- /www/src/theme/Playground/index.js: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { 3 | Editor, 4 | Preview, 5 | Provider, 6 | InfoMessage, 7 | useError, 8 | } from '../../../../src'; 9 | 10 | import * as Jarle from '../../../../src'; 11 | 12 | import clsx from 'clsx'; 13 | 14 | import styles from './styles.module.css'; 15 | 16 | // needed for importing and demostrating JARLE in itself 17 | // this is because we are using `src` directly 18 | window.__IMPORT__ = (s) => import(/* webpackIgnore: true */ s); 19 | 20 | const Info = (props) => ( 21 | 25 | ); 26 | 27 | function resolveImports(sources) { 28 | return Promise.all( 29 | sources.map((s) => { 30 | if (s === 'jarle') { 31 | return Jarle; 32 | } 33 | 34 | return import(/* webpackIgnore: true */ s); 35 | }) 36 | ); 37 | } 38 | 39 | function Playground({ 40 | children, 41 | theme, 42 | className, 43 | lineNumbers, 44 | inline, 45 | ...props 46 | }) { 47 | const error = useError(); 48 | const ref = React.useRef(null); 49 | 50 | React.useEffect(() => { 51 | let observer = new ResizeObserver(([entry]) => { 52 | const box = entry.contentRect; 53 | const hasSpace = inline == null ? box.width > 700 : !inline; 54 | 55 | ref.current.classList.toggle(styles.inline, hasSpace); 56 | }); 57 | 58 | observer.observe(ref.current); 59 | 60 | return () => { 61 | observer.disconnect(); 62 | }; 63 | }, []); 64 | 65 | return ( 66 |
67 | 73 | 79 | 80 |
81 | 82 | 83 |
84 |
85 |
86 | ); 87 | } 88 | 89 | function Error(props) { 90 | const error = useError(); 91 | 92 | return error ? ( 93 |
94 |
{error.toString()}
95 |
96 | ) : null; 97 | } 98 | 99 | export default Playground; 100 | -------------------------------------------------------------------------------- /src/CodeBlock.tsx: -------------------------------------------------------------------------------- 1 | import { Highlight, Language, Prism, PrismTheme } from 'prism-react-renderer'; 2 | import React from 'react'; 3 | import LineNumber from './LineNumber.js'; 4 | import { LineOutputProps, RenderProps } from './prism.js'; 5 | 6 | type MapTokens = RenderProps & { 7 | getLineNumbers?: (line: number) => React.ReactNode; 8 | errorLocation?: { line: number; col: number }; 9 | }; 10 | 11 | function addErrorHighlight( 12 | props: LineOutputProps, 13 | index: number, 14 | errorLocation?: MapTokens['errorLocation'] 15 | ) { 16 | if (index + 1 === errorLocation?.line) { 17 | props.className = `${props.className || ''} token-line-error`; 18 | } 19 | return props; 20 | } 21 | 22 | export const mapTokens = ({ 23 | tokens, 24 | getLineProps, 25 | getTokenProps, 26 | errorLocation, 27 | getLineNumbers, 28 | }: MapTokens) => ( 29 | <> 30 | {tokens.map((line, i) => { 31 | const { key = i, ...lineProps } = getLineProps({ line, key: String(i) }); 32 | 33 | return ( 34 |
35 | {getLineNumbers?.(i + 1)} 36 | {line.map((token, ii) => { 37 | const { key = ii, ...props } = getTokenProps({ 38 | token, 39 | key: String(ii), 40 | }); 41 | 42 | return ; 43 | })} 44 |
45 | ); 46 | })} 47 | 48 | ); 49 | 50 | interface CodeBlockProps { 51 | className?: string; 52 | style?: any; 53 | theme?: PrismTheme; 54 | code: string; 55 | language: Language; 56 | lineNumbers?: boolean; 57 | mapTokens?: (props: MapTokens) => React.ReactNode; 58 | } 59 | 60 | function CodeBlock({ 61 | code, 62 | theme, 63 | language, 64 | lineNumbers, 65 | mapTokens: consumerMapTokens = mapTokens, 66 | ...props 67 | }: CodeBlockProps) { 68 | const style = typeof theme?.plain === 'object' ? theme.plain : {}; 69 | 70 | const getLineNumbers = lineNumbers 71 | ? (num: number) => {num} 72 | : undefined; 73 | 74 | return ( 75 | 76 | {(hl) => ( 77 |
81 |           {consumerMapTokens({ ...hl, getLineNumbers })}
82 |         
83 | )} 84 |
85 | ); 86 | } 87 | 88 | export default CodeBlock; 89 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jarle", 3 | "version": "3.4.0", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/jquense/jarle.git" 7 | }, 8 | "author": "Jason Quense ", 9 | "license": "MIT", 10 | "scripts": { 11 | "build": "./build.sh", 12 | "prepublishOnly": "yarn run build", 13 | "release": "4c release", 14 | "tdd": "vitest", 15 | "test": "vitest run" 16 | }, 17 | "files": [ 18 | "cjs", 19 | "lib" 20 | ], 21 | "type": "module", 22 | "exports": { 23 | ".": { 24 | "require": { 25 | "types": "./cjs/index.d.ts", 26 | "default": "./cjs/index.js" 27 | }, 28 | "import": { 29 | "types": "./lib/index.d.ts", 30 | "default": "./lib/index.js" 31 | } 32 | }, 33 | "./*": { 34 | "require": { 35 | "types": "./cjs/*.d.ts", 36 | "default": "./cjs/*.js" 37 | }, 38 | "import": { 39 | "types": "./lib/*.d.ts", 40 | "default": "./lib/*.js" 41 | } 42 | } 43 | }, 44 | "publishConfig": { 45 | "access": "public" 46 | }, 47 | "prettier": { 48 | "singleQuote": true 49 | }, 50 | "devDependencies": { 51 | "@4c/cli": "^3.0.1", 52 | "@babel/cli": "^7.24.7", 53 | "@babel/core": "^7.24.7", 54 | "@babel/preset-env": "^7.24.7", 55 | "@babel/preset-react": "^7.24.7", 56 | "@babel/preset-typescript": "^7.24.7", 57 | "@rollup/plugin-commonjs": "^21.0.1", 58 | "@rollup/plugin-node-resolve": "^13.0.6", 59 | "@testing-library/dom": "^10.4.0", 60 | "@testing-library/react": "^16.2.0", 61 | "@types/react": "^19.0.12", 62 | "@types/react-dom": "^19.0.4", 63 | "@types/react-is": "^19.0.0", 64 | "cpy": "^8.1.2", 65 | "glob": "^7.2.0", 66 | "jsdom": "^24.1.0", 67 | "patch-package": "^8.0.0", 68 | "react": "^19.1.0", 69 | "react-dom": "^19.1.0", 70 | "react-is": "^19.1.0", 71 | "rollup": "^2.59.0", 72 | "rollup-plugin-dts": "^4.0.1", 73 | "sucrase": "^3.35.0", 74 | "typescript": "^5.4.5", 75 | "vitest": "^1.6.0" 76 | }, 77 | "dependencies": { 78 | "@restart/hooks": "^0.6.2", 79 | "prism-react-renderer": "^2.4.1", 80 | "sourcemap-codec": "^1.4.8", 81 | "uncontrollable": "^9.0.0" 82 | }, 83 | "peerDependencies": { 84 | "react": ">=18.3.1", 85 | "react-dom": ">=18.3.1", 86 | "react-is": ">=18.3.1" 87 | }, 88 | "packageManager": "yarn@4.8.1+sha512.bc946f2a022d7a1a38adfc15b36a66a3807a67629789496c3714dd1703d2e6c6b1c69ff9ec3b43141ac7a1dd853b7685638eb0074300386a59c18df351ef8ff6", 89 | "resolutions": { 90 | "@4c/rollout@npm:^3.0.1": "patch:@4c/rollout@npm%3A3.0.1#~/.yarn/patches/@4c-rollout-npm-3.0.1-04c0733bf4.patch" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /www/src/theme/Playground/styles.module.css: -------------------------------------------------------------------------------- 1 | .playgroundHeader { 2 | letter-spacing: 0.08rem; 3 | padding: 0.75rem; 4 | text-transform: uppercase; 5 | font-weight: bold; 6 | } 7 | 8 | .playgroundEditorHeader { 9 | background: var(--ifm-color-emphasis-600); 10 | color: var(--ifm-color-content-inverse); 11 | } 12 | 13 | .playgroundEditor { 14 | padding: 1rem; 15 | outline: none; 16 | padding-top: 2rem; 17 | font-family: var(--ifm-font-family-monospace) !important; 18 | border-top-left-radius: var(--ifm-global-radius); 19 | border-top-right-radius: var(--ifm-global-radius); 20 | } 21 | 22 | .playgroundEditor::after { 23 | content: 'editable'; 24 | 25 | top: 0.2rem; 26 | left: 0; 27 | position: absolute; 28 | pointer-events: none; 29 | letter-spacing: 0.08rem; 30 | font-size: 90%; 31 | padding: 0 0.75rem; 32 | text-transform: uppercase; 33 | font-weight: bold; 34 | } 35 | 36 | .playgroundPreview { 37 | position: relative; 38 | display: grid; 39 | grid-template: 1fr / minmax(auto, 100%); 40 | border: 1px solid var(--ifm-color-emphasis-200); 41 | border-bottom-left-radius: var(--ifm-global-radius); 42 | border-bottom-right-radius: var(--ifm-global-radius); 43 | position: relative; 44 | padding: 1rem; 45 | padding-top: 2rem; 46 | } 47 | 48 | .playgroundPreview > * { 49 | grid-area: 1 / 1; 50 | } 51 | 52 | .playgroundPreview::after { 53 | content: 'result'; 54 | 55 | top: 0; 56 | left: 0; 57 | position: absolute; 58 | pointer-events: none; 59 | font-size: 90%; 60 | letter-spacing: 0.08rem; 61 | padding: 0 0.75rem; 62 | text-transform: uppercase; 63 | font-weight: bold; 64 | } 65 | 66 | .infoMessage { 67 | font-size: 70%; 68 | padding: 0.4rem; 69 | } 70 | 71 | .errorOverlay { 72 | backdrop-filter: blur(4px); 73 | display: flex; 74 | margin-inline: -1rem; 75 | flex-direction: column; 76 | justify-content: safe center; 77 | } 78 | 79 | .error { 80 | all: unset; 81 | text-align: center; 82 | vertical-align: middle; 83 | font-size: 85% !important; 84 | font-family: var(--ifm-font-family-monospace); 85 | color: var(--ifm-color-danger-dark); 86 | } 87 | 88 | .inline { 89 | display: grid; 90 | grid-template-columns: 2fr 1.2fr; 91 | } 92 | 93 | .inline .playgroundEditor { 94 | height: 100%; 95 | border-top-right-radius: 0; 96 | border-bottom-left-radius: var(--ifm-global-radius); 97 | } 98 | 99 | .inline .playgroundPreview { 100 | height: 100%; 101 | 102 | border-bottom-left-radius: 0; 103 | border-bottom-right-radius: var(--ifm-global-radius); 104 | } 105 | 106 | .playgroundPreview > * > :global(.rw-widget) { 107 | max-width: 300px; 108 | } 109 | 110 | .playgroundPreview > * > * { 111 | margin: 0 auto; 112 | } 113 | -------------------------------------------------------------------------------- /src/prism.ts: -------------------------------------------------------------------------------- 1 | import { CSSProperties } from 'react'; 2 | 3 | export type Language = 4 | | 'markup' 5 | | 'bash' 6 | | 'clike' 7 | | 'c' 8 | | 'cpp' 9 | | 'css' 10 | | 'javascript' 11 | | 'jsx' 12 | | 'coffeescript' 13 | | 'actionscript' 14 | | 'css-extr' 15 | | 'diff' 16 | | 'git' 17 | | 'go' 18 | | 'graphql' 19 | | 'handlebars' 20 | | 'json' 21 | | 'less' 22 | | 'makefile' 23 | | 'markdown' 24 | | 'objectivec' 25 | | 'ocaml' 26 | | 'python' 27 | | 'reason' 28 | | 'sass' 29 | | 'scss' 30 | | 'sql' 31 | | 'stylus' 32 | | 'tsx' 33 | | 'typescript' 34 | | 'wasm' 35 | | 'yaml'; 36 | 37 | export type PrismThemeEntry = { 38 | color?: string; 39 | backgroundColor?: string; 40 | fontStyle?: 'normal' | 'italic'; 41 | fontWeight?: 42 | | 'normal' 43 | | 'bold' 44 | | '100' 45 | | '200' 46 | | '300' 47 | | '400' 48 | | '500' 49 | | '600' 50 | | '700' 51 | | '800' 52 | | '900'; 53 | textDecorationLine?: 54 | | 'none' 55 | | 'underline' 56 | | 'line-through' 57 | | 'underline line-through'; 58 | opacity?: number; 59 | [styleKey: string]: string | number | void; 60 | }; 61 | 62 | export type PrismTheme = { 63 | plain: PrismThemeEntry; 64 | styles: Array<{ 65 | types: string[]; 66 | style: PrismThemeEntry; 67 | languages?: Language[]; 68 | }>; 69 | }; 70 | 71 | export type ThemeDict = { 72 | root: StyleObj; 73 | plain: StyleObj; 74 | [type: string]: StyleObj; 75 | }; 76 | 77 | export type Token = { 78 | types: string[]; 79 | content: string; 80 | empty?: boolean; 81 | }; 82 | 83 | export type PrismToken = { 84 | type: string; 85 | content: Array | string; 86 | }; 87 | 88 | export type StyleObj = CSSProperties; 89 | 90 | export type LineInputProps = { 91 | key?: string; 92 | style?: StyleObj; 93 | className?: string; 94 | line: Token[]; 95 | [otherProp: string]: any; 96 | }; 97 | 98 | export type LineOutputProps = { 99 | key?: string; 100 | style?: StyleObj; 101 | className: string; 102 | [otherProps: string]: any; 103 | }; 104 | 105 | export type TokenInputProps = { 106 | key?: string; 107 | style?: StyleObj; 108 | className?: string; 109 | token: Token; 110 | [otherProp: string]: any; 111 | }; 112 | 113 | export type TokenOutputProps = { 114 | key?: string; 115 | style?: StyleObj; 116 | className: string; 117 | children: string; 118 | [otherProp: string]: any; 119 | }; 120 | 121 | export type RenderProps = { 122 | tokens: Token[][]; 123 | className: string; 124 | style: StyleObj; 125 | getLineProps: (input: LineInputProps) => LineOutputProps; 126 | getTokenProps: (input: TokenInputProps) => TokenOutputProps; 127 | }; 128 | -------------------------------------------------------------------------------- /www/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /* stylelint-disable docusaurus/copyright-header */ 2 | /** 3 | * Any CSS included here will be global. The classic template 4 | * bundles Infima by default. Infima is a CSS framework designed to 5 | * work well for content-centric websites. 6 | */ 7 | 8 | /* You can override the default Infima variables here. */ 9 | :root { 10 | --ifm-color-primary: #6d4b3e; 11 | --ifm-color-primary-dark: #624338; 12 | --ifm-color-primary-darker: #5d4035; 13 | --ifm-color-primary-darkest: #4c342b; 14 | --ifm-color-primary-light: #785244; 15 | --ifm-color-primary-lighter: #7d5647; 16 | --ifm-color-primary-lightest: #8e6151; 17 | --ifm-code-font-size: 95%; 18 | } 19 | 20 | .docusaurus-highlight-code-line { 21 | background-color: rgb(72, 77, 91); 22 | display: block; 23 | margin: 0 calc(-1 * var(--ifm-pre-padding)); 24 | padding: 0 var(--ifm-pre-padding); 25 | } 26 | 27 | .header-github-link:hover { 28 | opacity: 0.6; 29 | } 30 | 31 | .header-github-link:before { 32 | content: ''; 33 | width: 24px; 34 | height: 24px; 35 | display: flex; 36 | background: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E") 37 | no-repeat; 38 | } 39 | 40 | html[data-theme='dark'] .header-github-link:before { 41 | background: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='white' d='M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12'/%3E%3C/svg%3E") 42 | no-repeat; 43 | } 44 | 45 | .token-line-error { 46 | border-bottom: 1px dotted var(--ifm-color-danger); 47 | } 48 | -------------------------------------------------------------------------------- /src/ErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react'; 2 | import { ActionContext } from './Provider.js'; 3 | 4 | interface Props { 5 | element: ReactElement | null; 6 | showLastValid: boolean; 7 | } 8 | 9 | type State = { 10 | hasError?: boolean; 11 | element?: ReactElement | null; 12 | revision: number; 13 | }; 14 | 15 | class CodeLiveErrorBoundary extends React.Component { 16 | declare context: React.ContextType; 17 | 18 | lastGoodResult: ReactElement | null = null; 19 | 20 | pendingLastGoodResult: ReactElement | null = null; 21 | errorRenderCount = 0; 22 | timeout: NodeJS.Timeout | null = null; 23 | 24 | constructor(props: Props) { 25 | super(props); 26 | 27 | this.state = { 28 | hasError: false, 29 | element: props.element, 30 | revision: 0, 31 | }; 32 | } 33 | 34 | static getDerivedStateFromError() { 35 | return { hasError: true }; 36 | } 37 | 38 | static getDerivedStateFromProps(props: Props, state: State) { 39 | if (props.element === state.element) { 40 | return state; 41 | } 42 | 43 | return { 44 | hasError: false, 45 | element: props.element, 46 | revision: state.revision + 1, 47 | }; 48 | } 49 | 50 | componentWillUnmount(): void { 51 | clearTimeout(this.timeout!); 52 | } 53 | 54 | componentDidCatch(error: Error) { 55 | clearTimeout(this.timeout!); 56 | this.pendingLastGoodResult = null; 57 | 58 | // if the lastGoodResult is crashing then don't keep reporting it. 59 | if ( 60 | this.state.hasError && 61 | this.lastGoodResult && 62 | this.errorRenderCount > 1 63 | ) { 64 | this.lastGoodResult = null; 65 | return; 66 | } 67 | 68 | this.context.onError(error); 69 | } 70 | 71 | componentDidMount() { 72 | if (!this.state.hasError) { 73 | this.pendingLastGoodResult = this.props.element; 74 | } 75 | } 76 | 77 | componentDidUpdate(prevProps: Props, prevState: State) { 78 | if (prevState.revision !== this.state.revision) { 79 | this.errorRenderCount = 0; 80 | } 81 | 82 | if (!this.state.hasError) { 83 | if (this.pendingLastGoodResult) { 84 | this.lastGoodResult = this.pendingLastGoodResult; 85 | } 86 | 87 | this.pendingLastGoodResult = this.props.element; 88 | 89 | // sometimes cDU is called before the error is caught... 90 | this.timeout = setTimeout(() => { 91 | if (this.state.hasError || !this.pendingLastGoodResult) { 92 | return; 93 | } 94 | 95 | this.lastGoodResult = this.pendingLastGoodResult; 96 | }, 100); 97 | } else { 98 | this.errorRenderCount++; 99 | } 100 | } 101 | 102 | render() { 103 | if (this.state.hasError) { 104 | return this.props.showLastValid ? this.lastGoodResult : null; 105 | } 106 | 107 | return this.props.element; 108 | } 109 | } 110 | 111 | CodeLiveErrorBoundary.contextType = ActionContext; 112 | 113 | export default CodeLiveErrorBoundary; 114 | -------------------------------------------------------------------------------- /src/transform/index.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Transform, 3 | SucraseContext, 4 | Options as SucraseOptions, 5 | } from 'sucrase'; 6 | 7 | import ImportRemoverTransformer, { Import } from './ImportTransformer.js'; 8 | import { getSucraseContext, RootTransformer } from './parser.js'; 9 | import wrapLastExpression from './wrapLastExpression.js'; 10 | 11 | export type { Import }; 12 | 13 | type TransformerResult = ReturnType; 14 | 15 | class JarleRootTransformer extends RootTransformer { 16 | private importTransformer: ImportRemoverTransformer; 17 | 18 | private wrapLastExpression: (result: TransformerResult) => TransformerResult; 19 | 20 | constructor( 21 | context: SucraseContext, 22 | options: SucraseOptions & Options, 23 | parsingTransforms: Transform[] 24 | ) { 25 | super(context, options.transforms ?? [], false, { 26 | ...options, 27 | }); 28 | 29 | this.importTransformer = new ImportRemoverTransformer(context, options); 30 | 31 | // @ts-ignore 32 | this.transformers.unshift(this.importTransformer); 33 | 34 | this.wrapLastExpression = options.wrapLastExpression 35 | ? (result: TransformerResult) => { 36 | return ( 37 | wrapLastExpression( 38 | getSucraseContext(result.code, { 39 | ...options, 40 | transforms: parsingTransforms, 41 | }) 42 | ) ?? result 43 | ); 44 | } 45 | : (result: TransformerResult) => result; 46 | } 47 | 48 | get imports() { 49 | return this.importTransformer.imports; 50 | } 51 | 52 | transform() { 53 | let result = super.transform(); 54 | 55 | result.code = result.code 56 | .replace('"use strict";', '') 57 | .replace('exports. default =', 'exports.default =') 58 | .replace( 59 | 'Object.defineProperty(exports, "__esModule", {value: true});', 60 | '' 61 | ); 62 | 63 | return this.wrapLastExpression(result); 64 | } 65 | } 66 | 67 | export interface Options { 68 | removeImports?: boolean; 69 | wrapLastExpression?: boolean; 70 | syntax?: 'js' | 'typescript'; 71 | transforms?: Transform[]; 72 | filename?: string; 73 | compiledFilename?: string; 74 | } 75 | 76 | export function transform(code: string, options: Options = {}) { 77 | const transforms = options.transforms || []; 78 | const parsingTransforms = ['imports', 'jsx'] as Transform[]; 79 | const isTypeScriptEnabled = 80 | options.syntax === 'typescript' || transforms.includes('typescript'); 81 | 82 | if (isTypeScriptEnabled) { 83 | parsingTransforms.push('typescript'); 84 | } 85 | 86 | const sucraseOptions: SucraseOptions & Options = { 87 | ...options, 88 | transforms, 89 | preserveDynamicImport: true, 90 | enableLegacyBabel5ModuleInterop: false, 91 | enableLegacyTypeScriptModuleInterop: true, 92 | jsxRuntime: 'classic', 93 | }; 94 | 95 | const ctx = getSucraseContext(code, { 96 | ...sucraseOptions, 97 | transforms: parsingTransforms, 98 | }); 99 | 100 | const transformer = new JarleRootTransformer( 101 | ctx, 102 | sucraseOptions, 103 | parsingTransforms 104 | ); 105 | 106 | return { 107 | code: transformer.transform().code, 108 | imports: transformer.imports, 109 | }; 110 | } 111 | -------------------------------------------------------------------------------- /www/plugins/docgen/doclets.js: -------------------------------------------------------------------------------- 1 | const DOCLET_PATTERN = /^@(\w+)(?:$|\s((?:[^](?!^@\w))*))/gim; 2 | 3 | const cleanDocletValue = (str) => { 4 | str = str.trim(); 5 | if (str.endsWith(`}`) && str.startsWith(`{`)) str = str.slice(1, -1); 6 | return str; 7 | }; 8 | 9 | const isLiteral = (str) => /^('|"|true|false|\d+)/.test(str.trim()); 10 | 11 | /** 12 | * Remove doclets from string 13 | */ 14 | const cleanTags = (desc) => { 15 | desc = desc || ``; 16 | const idx = desc.search(DOCLET_PATTERN); 17 | return (idx === -1 ? desc : desc.substr(0, idx)).trim(); 18 | }; 19 | 20 | /** 21 | * Given a string, this function returns an array of doclet tags 22 | * 23 | * Adapted from https://github.com/reactjs/react-docgen/blob/ee8a5359c478b33a6954f4546637312764798d6b/src/utils/docblock.js#L62 24 | * Updated to strip \r from the end of doclets 25 | */ 26 | const getTags = (str) => { 27 | const doclets = []; 28 | let match = DOCLET_PATTERN.exec(str); 29 | let val; 30 | 31 | for (; match; match = DOCLET_PATTERN.exec(str)) { 32 | val = match[2] ? match[2].replace(/\r$/, ``) : true; 33 | const key = match[1]; 34 | doclets.push({ name: key, value: val }); 35 | } 36 | return doclets; 37 | }; 38 | 39 | const parseTags = (description = '') => { 40 | return getTags(description) || []; 41 | }; 42 | 43 | function parseType(type) { 44 | if (!type) { 45 | return undefined; 46 | } 47 | 48 | const { name, raw } = type; 49 | 50 | if (name === `union`) { 51 | return { 52 | name, 53 | value: raw.split(`|`).map((v) => { 54 | return { name: v.trim() }; 55 | }), 56 | }; 57 | } 58 | 59 | if (name === `enum`) { 60 | return { ...type }; 61 | } 62 | 63 | if (raw) { 64 | return { 65 | name: raw, 66 | }; 67 | } 68 | 69 | return { ...type }; 70 | } 71 | 72 | /** 73 | * Reads the JSDoc "doclets" and applies certain ones to the prop type data 74 | * This allows us to "fix" parsing errors, or unparsable data with JSDoc 75 | * style comments 76 | */ 77 | const applyPropTags = (prop) => { 78 | prop.tags.forEach(({ name, value }) => { 79 | // the @type doclet to provide a prop type 80 | // Also allows enums (oneOf) if string literals are provided 81 | // ex: @type {("optionA"|"optionB")} 82 | if (name === `type`) { 83 | value = cleanDocletValue(value); 84 | 85 | if (prop.type === undefined) { 86 | prop.type = {}; 87 | } 88 | 89 | prop.type.name = value; 90 | 91 | if (value[0] === `(`) { 92 | value = value 93 | .substring(1, value.length - 1) 94 | .split(`|`) 95 | .map((v) => v.trim()); 96 | const typeName = value.every(isLiteral) ? `enum` : `union`; 97 | prop.type.name = typeName; 98 | prop.type.value = value.map((v) => 99 | typeName === `enum` ? { value: v, computed: false } : { name: value } 100 | ); 101 | } 102 | return; 103 | } 104 | 105 | // Use @required to mark a prop as required 106 | // useful for custom propTypes where there isn't a `.isRequired` addon 107 | if (name === `required` && value) { 108 | prop.required = true; 109 | return; 110 | } 111 | 112 | // Use @defaultValue to provide a prop's default value 113 | if ((name === `default` || name === `defaultValue`) && value != null) { 114 | prop.defaultValue = { value, computed: false }; 115 | } 116 | }); 117 | 118 | // lookup for tsTypes or flowTypes 119 | if (prop.type === undefined) { 120 | prop.type = parseType(prop.tsType) || parseType(prop.flowType); 121 | } 122 | 123 | return prop; 124 | }; 125 | 126 | module.exports = { cleanTags, parseTags, applyPropTags }; 127 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [2.1.1](https://github.com/jquense/jarle/compare/v2.1.0...v2.1.1) (2023-01-18) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * don't render invalid code ([1a2cfe1](https://github.com/jquense/jarle/commit/1a2cfe165a1943ddf71d19a87eac66dadccee6aa)) 7 | 8 | 9 | 10 | 11 | 12 | # [2.1.0](https://github.com/jquense/jarle/compare/v2.0.0...v2.1.0) (2023-01-18) 13 | 14 | 15 | ### Features 16 | 17 | * support named exports, and upgrade sucrase ([3031638](https://github.com/jquense/jarle/commit/3031638c51f67ede9cfe43c351ea4871ca76c473)) 18 | 19 | 20 | 21 | 22 | 23 | # [2.0.0](https://github.com/jquense/jarle/compare/v2.0.0-beta.1...v2.0.0) (2022-01-18) 24 | 25 | 26 | 27 | 28 | 29 | # [1.3.0](https://github.com/jquense/jarle/compare/v1.2.2...v1.3.0) (2021-10-27) 30 | 31 | 32 | ### Features 33 | 34 | * use latest for acorn ecma target ([562f36e](https://github.com/jquense/jarle/commit/562f36e053accbcfa354217e6f9be7b12bb922b8)) 35 | 36 | 37 | 38 | 39 | 40 | ## [1.2.2](https://github.com/jquense/jarle/compare/v1.2.1...v1.2.2) (2021-08-06) 41 | 42 | 43 | ### Bug Fixes 44 | 45 | * setState after unmount ([9063cf5](https://github.com/jquense/jarle/commit/9063cf5959ab40c8ccb0af3351f4a087cf48e6aa)) 46 | 47 | 48 | 49 | 50 | 51 | ## [1.2.1](https://github.com/jquense/jarle/compare/v1.2.0...v1.2.1) (2021-08-06) 52 | 53 | 54 | ### Bug Fixes 55 | 56 | * peer dep ([474e084](https://github.com/jquense/jarle/commit/474e0842a8b67cf028bcedcb74956c116a9d28ec)) 57 | 58 | 59 | 60 | 61 | 62 | # [1.2.0](https://github.com/jquense/jarle/compare/v1.1.2...v1.2.0) (2021-03-31) 63 | 64 | 65 | ### Features 66 | 67 | * bump deps ([98c525c](https://github.com/jquense/jarle/commit/98c525cb91744547a9956e086988c70d386a8915)) 68 | 69 | 70 | 71 | 72 | 73 | ## [1.1.2](https://github.com/jquense/jarle/compare/v1.1.0...v1.1.2) (2021-02-02) 74 | 75 | 76 | ### Bug Fixes 77 | 78 | * @4c/tsconfig to dev dependencies ([#8](https://github.com/jquense/jarle/issues/8)) ([55582c8](https://github.com/jquense/jarle/commit/55582c842a8a77c9caa9c5526ff9345f87181916)) 79 | * default import ([a63fad9](https://github.com/jquense/jarle/commit/a63fad971261c40b09bde999cd0c917056e63e54)) 80 | * **Preview:** allow props to override defaults ([#5](https://github.com/jquense/jarle/issues/5)) ([3e3e3a7](https://github.com/jquense/jarle/commit/3e3e3a77b6611d0b7c759199833bc9a26c939f51)), closes [#4](https://github.com/jquense/jarle/issues/4) 81 | 82 | 83 | 84 | 85 | 86 | ## [1.1.1](https://github.com/jquense/jarle/compare/v1.1.0...v1.1.1) (2021-02-01) 87 | 88 | 89 | ### Bug Fixes 90 | 91 | * @4c/tsconfig to dev dependencies ([#8](https://github.com/jquense/jarle/issues/8)) ([55582c8](https://github.com/jquense/jarle/commit/55582c842a8a77c9caa9c5526ff9345f87181916)) 92 | * **Preview:** allow props to override defaults ([#5](https://github.com/jquense/jarle/issues/5)) ([3e3e3a7](https://github.com/jquense/jarle/commit/3e3e3a77b6611d0b7c759199833bc9a26c939f51)), closes [#4](https://github.com/jquense/jarle/issues/4) 93 | 94 | 95 | 96 | 97 | 98 | # [1.1.0](https://github.com/jquense/jarle/compare/v1.0.3...v1.1.0) (2021-01-22) 99 | 100 | 101 | ### Features 102 | 103 | * allow component returns and export default ([1000f48](https://github.com/jquense/jarle/commit/1000f48271f0a9424c9e00b27323b5a89e99de42)) 104 | 105 | 106 | 107 | 108 | 109 | ## [1.0.3](https://github.com/jquense/jarle/compare/v1.0.2...v1.0.3) (2021-01-12) 110 | 111 | 112 | ### Bug Fixes 113 | 114 | * removing imports ([b1d6473](https://github.com/jquense/jarle/commit/b1d647376b118204c9e37852f140ebc5674db9a0)) 115 | 116 | 117 | 118 | 119 | 120 | ## [1.0.2](https://github.com/jquense/jarle/compare/v1.0.1...v1.0.2) (2021-01-12) 121 | 122 | 123 | ### Bug Fixes 124 | 125 | * remove unused deps ([81f20ab](https://github.com/jquense/jarle/commit/81f20ab0abecd477dac76a53acfd3013af13d23c)) 126 | 127 | 128 | 129 | 130 | 131 | -------------------------------------------------------------------------------- /src/transform/ImportTransformer.ts: -------------------------------------------------------------------------------- 1 | import { SucraseContext } from 'sucrase'; 2 | import { 3 | TokenProcessor, 4 | type CJSImportProcessor, 5 | type RootTransformer, 6 | tt, 7 | } from './parser.js'; 8 | 9 | export type Import = { 10 | code: string; 11 | source: string; 12 | base: null | string; 13 | keys: Array<{ local: string; imported: string }>; 14 | }; 15 | 16 | export default class ImportRemoverTransformer { 17 | tokens: TokenProcessor; 18 | importProcessor: CJSImportProcessor; 19 | 20 | imports: Import[] = []; 21 | private readonly removeImports: boolean; 22 | 23 | constructor( 24 | context: SucraseContext, 25 | { removeImports }: { removeImports?: boolean } 26 | ) { 27 | this.tokens = context.tokenProcessor; 28 | this.importProcessor = context.importProcessor!; 29 | this.removeImports = removeImports || false; 30 | 31 | // clear the replacements b/c we are handling imports 32 | // @ts-ignore private 33 | this.importProcessor.identifierReplacements.clear(); 34 | } 35 | 36 | getPrefixCode() { 37 | return ''; 38 | } 39 | 40 | getHoistedCode() { 41 | return ''; 42 | } 43 | 44 | getSuffixCode() { 45 | return ''; 46 | } 47 | 48 | process(): boolean { 49 | if (!this.tokens.matches1(tt._import)) return false; 50 | 51 | // dynamic import 52 | if (this.tokens.matches2(tt._import, tt.parenL)) { 53 | return true; 54 | } 55 | 56 | this.tokens.removeInitialToken(); 57 | while (!this.tokens.matches1(tt.string)) { 58 | this.tokens.removeToken(); 59 | } 60 | 61 | const path = this.tokens.stringValue(); 62 | 63 | const detail = this.buildImport(path); 64 | this.importProcessor.claimImportCode(path); 65 | if (detail?.code) { 66 | this.imports.push(detail); 67 | 68 | this.tokens.replaceTokenTrimmingLeftWhitespace( 69 | this.removeImports ? '' : detail.code 70 | ); 71 | } else { 72 | this.tokens.removeToken(); 73 | } 74 | 75 | if (this.tokens.matches1(tt.semi)) { 76 | this.tokens.removeToken(); 77 | } 78 | 79 | return true; 80 | } 81 | 82 | num = 0; 83 | getIdentifier(src: string) { 84 | return `${src.split('/').pop()!.replace(/\W/g, '_')}$${this.num++}`; 85 | } 86 | 87 | private buildImport(path: string) { 88 | // @ts-ignore 89 | if (!this.importProcessor.importInfoByPath.has(path)) { 90 | return null; 91 | } 92 | 93 | let FN = 'require'; 94 | 95 | const { defaultNames, wildcardNames, namedImports, hasBareImport } = 96 | // @ts-ignore 97 | this.importProcessor.importInfoByPath.get(path); 98 | 99 | const req = `${FN}('${path}');`; 100 | const tmp = this.getIdentifier(path); 101 | 102 | const named = [] as string[]; 103 | const details: Import = { 104 | base: null, 105 | source: path, 106 | keys: [], 107 | code: '', 108 | }; 109 | 110 | namedImports.forEach((s) => { 111 | if ( 112 | this.importProcessor.shouldAutomaticallyElideImportedName(s.localName) 113 | ) { 114 | return; 115 | } 116 | 117 | named.push( 118 | s.localName === s.importedName 119 | ? s.localName 120 | : `${s.importedName}: ${s.localName}` 121 | ); 122 | details.keys.push({ local: s.localName, imported: s.importedName }); 123 | }); 124 | 125 | if (defaultNames.length || wildcardNames.length) { 126 | const name = defaultNames[0] || wildcardNames[0]; 127 | 128 | if (!this.importProcessor.shouldAutomaticallyElideImportedName(name)) { 129 | details.base = name; 130 | // intentionally use `var` so that conflcits with Jarle provider scope get resolved naturally 131 | // with the import overriding the scoped identifier 132 | if (wildcardNames.length) { 133 | details.code = `var ${name} = ${req}`; 134 | } else { 135 | details.code = `var ${tmp} = ${req} var ${name} = ${tmp}.default;`; 136 | } 137 | } 138 | } 139 | 140 | if (named.length) { 141 | details.code += ` var { ${named.join(', ')} } = ${ 142 | details.code ? `${tmp};` : req 143 | }`; 144 | } 145 | 146 | if (hasBareImport) { 147 | details.code = req; 148 | } 149 | 150 | details.code = details.code.trim(); 151 | return details; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /www/plugins/docgen/index.js: -------------------------------------------------------------------------------- 1 | const reactDocgen = require('react-docgen'); 2 | const globby = require('globby'); 3 | const fs = require('fs/promises'); 4 | const path = require('path'); 5 | const mdx = require('@mdx-js/mdx'); 6 | const { pascalCase } = require('tiny-case'); 7 | 8 | const jsDocHandler = require('./jsDocHandler'); 9 | 10 | function compile(value) {} 11 | 12 | const noop = `() => null`; 13 | 14 | async function stringify(value, write, parent) { 15 | let str = ''; 16 | if (Array.isArray(value)) { 17 | const values = await Promise.all( 18 | value.map((v) => stringify(v, write, v?.name || parent)) 19 | ); 20 | 21 | str = `[${values.join(',\n')}]`; 22 | } else if (value && typeof value === 'object') { 23 | str += '{\n'; 24 | for (const [key, keyValue] of Object.entries(value)) { 25 | if (key === 'description') { 26 | if (keyValue) { 27 | const file = await write(parent, await mdx(keyValue)); 28 | str += `"${key}": require('./${file}').default,\n`; 29 | } else { 30 | str += `"${key}": ${noop},\n`; 31 | } 32 | } else { 33 | str += `"${key}": ${await stringify(keyValue, write, key)},\n`; 34 | } 35 | } 36 | str += '}'; 37 | } else { 38 | str += JSON.stringify(value); 39 | } 40 | return str; 41 | } 42 | 43 | function descriptions(component) { 44 | let process = async (c) => { 45 | if (c.description) c.description = await mdx(c.description); 46 | }; 47 | return Promise.all([ 48 | process(component), 49 | ...Object.values(component.props || {}).map(process), 50 | ]); 51 | } 52 | 53 | module.exports = (ctx, options = {}) => { 54 | const { src, docgen = {}, babel = {}, parserOptions } = options; 55 | 56 | const defaultHandlers = [...reactDocgen.defaultHandlers, jsDocHandler]; 57 | 58 | return { 59 | name: 'docgen', 60 | getPathsToWatch() { 61 | return src; 62 | }, 63 | async loadContent() { 64 | const files = await globby(options.src); 65 | 66 | const content = await Promise.all( 67 | files.map(async (file) => { 68 | try { 69 | return { 70 | docgen: reactDocgen.parse( 71 | await fs.readFile(file), 72 | docgen.resolver || 73 | reactDocgen.resolver.findAllComponentDefinitions, 74 | defaultHandlers.concat(docgen.handlers || []), 75 | { 76 | filename: file, 77 | parserOptions, 78 | ...babel, 79 | } 80 | ), 81 | file, 82 | }; 83 | } catch (e) { 84 | if (e.message.includes('No suitable component definition found')) 85 | return; 86 | 87 | console.error(e.message, file); 88 | } 89 | }) 90 | ); 91 | 92 | return content.filter(Boolean); 93 | }, 94 | 95 | async contentLoaded({ content, actions }) { 96 | const re = /\.[jt]sx?/gi; 97 | const { createData, saveData, addRoute } = actions; 98 | 99 | await Promise.all( 100 | content.flatMap(({ file, docgen }) => { 101 | return docgen.map(async (component) => { 102 | component.displayName = 103 | component.displayName || 104 | pascalCase(path.basename(file, path.extname(file))); 105 | 106 | const name = component.displayName; 107 | 108 | component.props = Object.entries(component.props || {}).map( 109 | ([name, value]) => ({ 110 | name, 111 | ...value, 112 | }) 113 | ); 114 | 115 | const write = async (p = '', data) => { 116 | const filename = `${name}_${p ? `${p}_` : ''}description.js`; 117 | await createData( 118 | filename, 119 | `import { mdx } from '@mdx-js/react';\n\n${data}` 120 | ); 121 | return filename; 122 | }; 123 | 124 | return createData( 125 | `${name}.js`, 126 | `module.exports = ${await stringify(component, write)}` 127 | ); 128 | }); 129 | }) 130 | ); 131 | }, 132 | 133 | configureWebpack() { 134 | const scope = options.id === 'default' ? '' : `/${options.id}`; 135 | 136 | const dataPath = `${ctx.generatedFilesDir}/docgen/${options.id}/`; 137 | 138 | return { 139 | resolve: { 140 | alias: { 141 | [`@metadata${scope}`]: dataPath, 142 | }, 143 | }, 144 | }; 145 | }, 146 | }; 147 | }; 148 | -------------------------------------------------------------------------------- /test/sucrase.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, test, expect } from 'vitest'; 2 | import { transform } from '../src/transform/index.js'; 3 | 4 | describe('general parsing smoketest', () => { 5 | it('parses', () => { 6 | transform( 7 | ` 8 | const obj = { a: { b: 1, c: true }} 9 | 10 | let f = obj?.a?.b?.() 11 | ` 12 | ); 13 | 14 | transform( 15 | ` 16 | const obj = { a: { b: 1, c: true }} 17 | 18 | function foo(a: T): T { 19 | return a; 20 | } 21 | 22 | let f: number = obj?.a?.b?.() satisfies number 23 | `, 24 | { transforms: ['typescript'] } 25 | ); 26 | }); 27 | }); 28 | 29 | describe('import rewriting', () => { 30 | // it('parses', () => { 31 | // console.log( 32 | // transform(`
`, { 33 | // syntax: 'typescript', 34 | // wrapLastExpression: true, 35 | // }).code 36 | // ); 37 | // }); 38 | 39 | test.each([ 40 | ['no import', 'import "./foo";', "require('./foo');", undefined], 41 | 42 | [ 43 | 'default import', 44 | 'import Foo from "./foo";', 45 | "var foo$0 = require('./foo'); var Foo = foo$0.default;", 46 | undefined, 47 | ], 48 | [ 49 | 'named imports', 50 | 'import { Bar, Baz } from "./foo";', 51 | "var { Bar, Baz } = require('./foo');", 52 | undefined, 53 | ], 54 | [ 55 | 'namespace', 56 | 'import * as Foo from "./foo";', 57 | "var Foo = require('./foo');", 58 | undefined, 59 | ], 60 | ['side effect', 'import "./foo";', "require('./foo');", undefined], 61 | [ 62 | 'mixed', 63 | 'import Foo, { Bar, Baz } from "./foo";', 64 | "var foo$0 = require('./foo'); var Foo = foo$0.default; var { Bar, Baz } = foo$0;", 65 | undefined, 66 | ], 67 | [ 68 | 'type imports', 69 | 'import type Foo from "./foo";', 70 | '', 71 | { syntax: 'typescript' }, 72 | ], 73 | [ 74 | 'type only imports', 75 | 'import Bar from "./bar";\nimport Foo from "./foo";\nconst foo: Foo = Bar', 76 | "var bar$0 = require('./bar'); var Bar = bar$0.default;\n\nconst foo = Bar", 77 | { transforms: ['typescript'] }, 78 | ], 79 | [ 80 | 'preserves new lines', 81 | 'import { \nBar,\nBaz\n} from "./foo";', 82 | "\n\n\nvar { Bar, Baz } = require('./foo');", 83 | undefined, 84 | ], 85 | ])('compiles %s', (_, input, expected, options: any) => { 86 | expect(transform(input, options).code).toEqual(expected); 87 | }); 88 | 89 | it('removes imports', () => { 90 | expect( 91 | transform(`import Foo from './foo';\nimport Bar from './bar';\n
`, { 92 | removeImports: true, 93 | }).code 94 | ).toEqual('\n\n
'); 95 | }); 96 | 97 | it('fills imports', () => { 98 | const { imports } = transform( 99 | ` 100 | import Foo from './foo'; 101 | import * as D from './foo2' 102 | import A, { B, c as C } from './foo3' 103 | ` 104 | ); 105 | 106 | expect(imports).toEqual([ 107 | { 108 | base: 'Foo', 109 | source: './foo', 110 | keys: [], 111 | code: expect.anything(), 112 | }, 113 | { 114 | base: 'D', 115 | source: './foo2', 116 | keys: [], 117 | code: expect.anything(), 118 | }, 119 | { 120 | base: 'A', 121 | source: './foo3', 122 | keys: [ 123 | { local: 'B', imported: 'B' }, 124 | { local: 'C', imported: 'c' }, 125 | ], 126 | code: expect.anything(), 127 | }, 128 | ]); 129 | }); 130 | 131 | it('excludes type imports', () => { 132 | const { imports } = transform( 133 | ` 134 | import type Foo from './foo'; 135 | import * as D from './foo2' 136 | import type { B, c as C } from './foo3' 137 | 138 | const foo: Foo = D; 139 | `, 140 | { transforms: ['typescript'] } 141 | ); 142 | 143 | expect(imports).toEqual([ 144 | { 145 | base: 'D', 146 | source: './foo2', 147 | keys: [], 148 | code: expect.anything(), 149 | }, 150 | ]); 151 | }); 152 | 153 | it('elides unused imports and types', () => { 154 | const { imports } = transform( 155 | ` 156 | import Foo from './foo'; 157 | import * as D from './foo2' 158 | import { B, c as C } from './foo3' 159 | 160 | const foo: Foo = D; 161 | `, 162 | { transforms: ['typescript'] } 163 | ); 164 | 165 | expect(imports).toEqual([ 166 | { 167 | base: 'D', 168 | source: './foo2', 169 | keys: [], 170 | code: expect.anything(), 171 | }, 172 | ]); 173 | }); 174 | }); 175 | 176 | describe('wrap last expression', () => { 177 | test.each([ 178 | [ 179 | 'basic', 180 | "let a = 1;\nReact.createElement('i', null, a);", 181 | "let a = 1;\nreturn React.createElement('i', null, a);", 182 | ], 183 | ['single expression', '
', 'return
'], 184 | ['with semi', '
;', 'return
;'], 185 | [ 186 | 'does nothing if already a return', 187 | '
;\nreturn ', 188 | '
;\nreturn ', 189 | ], 190 | [ 191 | 'does nothing if already a return earlier', 192 | 'return ; \n
;', 193 | 'return ; \n
;', 194 | ], 195 | [ 196 | 'multiline expression', 197 | ` 198 | function Wrapper(ref) { 199 | let children = ref.children; 200 | return React.createElement('div', {id: 'foo'}, children); 201 | }; 202 | 203 | React.createElement(Wrapper, null, 204 | React.createElement(Wrapper, null, React.createElement(Icon, {name: "plus"})), 205 | React.createElement(Wrapper, null, React.createElement(Icon, {name: "clip"})) 206 | ); 207 | `, 208 | ` 209 | function Wrapper(ref) { 210 | let children = ref.children; 211 | return React.createElement('div', {id: 'foo'}, children); 212 | }; 213 | 214 | return React.createElement(Wrapper, null, 215 | React.createElement(Wrapper, null, React.createElement(Icon, {name: "plus"})), 216 | React.createElement(Wrapper, null, React.createElement(Icon, {name: "clip"})) 217 | ); 218 | `, 219 | ], 220 | [ 221 | 'replaces export default', 222 | `export default
`, 223 | `exports.default =
`, 224 | ], 225 | [ 226 | 'prefers export default', 227 | `export default
;\n`, 228 | `exports.default =
;\n`, 229 | ], 230 | [ 231 | 'return class', 232 | `const bar = true;\nclass foo {}`, 233 | `const bar = true;\nreturn class foo {}`, 234 | ], 235 | [ 236 | 'export class', 237 | `export default class foo {}`, 238 | ` class foo {} exports.default = foo;`, 239 | ], 240 | [ 241 | 'return function', 242 | `const bar = true\nfunction foo() {}`, 243 | `const bar = true\n;return function foo() {}`, 244 | ], 245 | [ 246 | 'export function', 247 | `export default function foo() {}`, 248 | ` function foo() {} exports.default = foo;`, 249 | ], 250 | [ 251 | 'export function 2', 252 | `function foo() {}\nfunction bar(baz= function() {}) {}`, 253 | `function foo() {}\n;return function bar(baz= function() {}) {}`, 254 | ], 255 | 256 | ['confusing expressions 1', `foo, bar\nbaz`, `foo, bar\n;return baz`], 257 | [ 258 | 'confusing expressions 2', 259 | `foo, (() => {});\nbaz`, 260 | `foo, (() => {});\nreturn baz`, 261 | ], 262 | [ 263 | 'confusing expressions 3', 264 | `foo, (() => {});baz\nquz`, 265 | `foo, (() => {});baz\n;return quz`, 266 | ], 267 | ['confusing expressions 4', `let foo = {};baz`, `let foo = {};return baz`], 268 | [ 269 | 'confusing expressions 4', 270 | `function foo(){\nlet bar = 1; return baz;};baz`, 271 | `function foo(){\nlet bar = 1; return baz;};return baz`, 272 | ], 273 | ])('compiles %s', (_, input, expected) => { 274 | expect( 275 | transform(input, { transforms: ['imports'], wrapLastExpression: true }) 276 | .code 277 | ).toEqual(expected); 278 | }); 279 | }); 280 | -------------------------------------------------------------------------------- /src/Editor.tsx: -------------------------------------------------------------------------------- 1 | import { Highlight, Prism, type PrismTheme } from 'prism-react-renderer'; 2 | import React, { 3 | useCallback, 4 | useEffect, 5 | useLayoutEffect, 6 | useMemo, 7 | useRef, 8 | useState, 9 | } from 'react'; 10 | import SimpleCodeEditor from './SimpleEditor.js'; 11 | 12 | import { mapTokens } from './CodeBlock.js'; 13 | import InfoMessage from './InfoMessage.js'; 14 | import { useActions, useCode, useEditorConfig, useError, useLiveContext } from './Provider.js'; 15 | import LineNumber from './LineNumber.js'; 16 | 17 | let uid = 0; 18 | 19 | function useStateFromProp(prop: TProp) { 20 | const state = useState(prop); 21 | const firstRef = useRef(true); 22 | 23 | useMemo(() => { 24 | if (firstRef.current) { 25 | firstRef.current = false; 26 | return; 27 | } 28 | state[1](prop); 29 | }, [prop]); 30 | 31 | return state; 32 | } 33 | 34 | function useInlineStyle() { 35 | useEffect(() => { 36 | if (document.getElementById('__jarle-style-tag')) { 37 | return; 38 | } 39 | 40 | const style = document.createElement('style'); 41 | document.head.append(style); 42 | style.setAttribute('id', '__jarle-style-tag'); 43 | style.sheet!.insertRule(` 44 | .__jarle { 45 | display: grid; 46 | position: relative; 47 | grid-template-columns: auto 1fr; 48 | } 49 | `); 50 | style.sheet!.insertRule(` 51 | .__jarle pre, 52 | .__jarle textarea { 53 | overflow: visible; 54 | } 55 | `); 56 | }, []); 57 | } 58 | 59 | export interface ControlledEditorProps extends Props { 60 | language?: string; 61 | 62 | code: string; 63 | 64 | errorLocation?: 65 | | { 66 | line: number; 67 | col: number; 68 | } 69 | | undefined; 70 | 71 | onCodeChange: (code: string) => void; 72 | 73 | } 74 | 75 | /** 76 | * The Editor is the code text editor component, some props can be supplied directly 77 | * or take from the Provider context if available. 78 | */ 79 | export const ControlledEditor = React.forwardRef( 80 | ( 81 | { 82 | code, 83 | style, 84 | className, 85 | theme, 86 | language, 87 | onCodeChange, 88 | errorLocation, 89 | infoComponent: Info = InfoMessage, 90 | lineNumbers, 91 | infoSrOnly = false, 92 | }: ControlledEditorProps, 93 | ref: any 94 | ) => { 95 | const mouseDown = useRef(false); 96 | 97 | useInlineStyle(); 98 | 99 | const [{ visible, ignoreTab, keyboardFocused }, setState] = useState({ 100 | visible: false, 101 | ignoreTab: false, 102 | keyboardFocused: false, 103 | }); 104 | 105 | const id = useMemo(() => `described-by-${++uid}`, []); 106 | 107 | const handleKeyDown = (event: React.KeyboardEvent) => { 108 | const { key } = event; 109 | 110 | if (ignoreTab && key !== 'Tab' && key !== 'Shift') { 111 | if (key === 'Enter') event.preventDefault(); 112 | setState((prev) => ({ ...prev, ignoreTab: false })); 113 | } 114 | if (!ignoreTab && key === 'Escape') { 115 | setState((prev) => ({ ...prev, ignoreTab: true })); 116 | } 117 | }; 118 | 119 | const handleFocus = (e: React.FocusEvent) => { 120 | if (e.target !== e.currentTarget) return; 121 | setState({ 122 | visible: true, 123 | ignoreTab: !mouseDown.current, 124 | keyboardFocused: !mouseDown.current, 125 | }); 126 | }; 127 | 128 | const handleBlur = (e: React.FocusEvent) => { 129 | if (e.target !== e.currentTarget) return; 130 | setState((prev) => ({ 131 | ...prev, 132 | visible: false, 133 | })); 134 | }; 135 | 136 | const handleMouseDown = () => { 137 | mouseDown.current = true; 138 | setTimeout(() => { 139 | mouseDown.current = false; 140 | }); 141 | }; 142 | 143 | const highlight = useCallback( 144 | (value: string) => ( 145 | 151 | {(hl) => 152 | mapTokens({ 153 | ...hl, 154 | errorLocation, 155 | getLineNumbers: (line: number) => 156 | lineNumbers ? ( 157 | 165 | {line} 166 | 167 | ) : null, 168 | }) 169 | } 170 | 171 | ), 172 | [theme, lineNumbers, language, errorLocation] 173 | ); 174 | 175 | const baseTheme = { 176 | whiteSpace: 'pre', 177 | fontFamily: 'monospace', 178 | ...(theme?.plain || {}), 179 | ...style, 180 | }; 181 | 182 | return ( 183 |
188 |
189 | {/* 190 | These line numbers are visually hidden in order to dynamically create enough space for the numbers. 191 | The visible numbers are added to the actual lines, and absolutely positioned to the left into the same space. 192 | this allows for soft wrapping lines as well as not changing the dimensions of the `pre` tag to keep 193 | the syntax highlighting synced with the textarea. 194 | */} 195 | {lineNumbers && 196 | (code || '').split(/\n/g).map((_, i) => ( 197 | 203 | {i + 1} 204 | 205 | ))} 206 |
207 | 220 | {visible && (keyboardFocused || !ignoreTab) && ( 221 | 222 | {ignoreTab ? ( 223 | <> 224 | Press enter or type a key to enable tab-to-indent 225 | 226 | ) : ( 227 | <> 228 | Press esc to disable tab-to-indent 229 | 230 | )} 231 | 232 | )} 233 |
234 | ); 235 | } 236 | ); 237 | 238 | export interface Props { 239 | className?: string; 240 | 241 | style?: any; 242 | 243 | /** A Prism theme object, can also be specified on the Provider */ 244 | theme?: PrismTheme; 245 | 246 | /** Render line numbers */ 247 | lineNumbers?: boolean; 248 | 249 | /** Styles the info component so that it is not visible but still accessible by screen readers. */ 250 | infoSrOnly?: boolean; 251 | 252 | /** The component used to render A11y messages about keyboard navigation, override to customize the styling */ 253 | infoComponent?: React.ComponentType; 254 | } 255 | 256 | export default function Editor({ theme, ...props }: Props) { 257 | const { 258 | theme: contextTheme, 259 | language, 260 | } = useEditorConfig(); 261 | const code = useCode() 262 | const error = useError(); 263 | const { onChange} = useActions() 264 | 265 | const errorLocation = error?.location || error?.loc; 266 | 267 | return ( 268 | 276 | ); 277 | } 278 | -------------------------------------------------------------------------------- /patches/sucrase+3.35.0+002+top-level-scope-tracking.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/sucrase/dist/esm/parser/traverser/statement.js b/node_modules/sucrase/dist/esm/parser/traverser/statement.js 2 | index 34a6511..4f19dcc 100644 3 | --- a/node_modules/sucrase/dist/esm/parser/traverser/statement.js 4 | +++ b/node_modules/sucrase/dist/esm/parser/traverser/statement.js 5 | @@ -89,7 +89,7 @@ import { 6 | } from "./util"; 7 | 8 | export function parseTopLevel() { 9 | - parseBlockBody(tt.eof); 10 | + parseBlockBody(tt.eof, true); 11 | state.scopes.push(new Scope(0, state.tokens.length, true)); 12 | if (state.scopeDepth !== 0) { 13 | throw new Error(`Invalid scope depth at end of file: ${state.scopeDepth}`); 14 | @@ -104,7 +104,7 @@ export function parseTopLevel() { 15 | // `if (foo) /blah/.exec(foo)`, where looking at the previous token 16 | // does not help. 17 | 18 | -export function parseStatement(declaration) { 19 | +export function parseStatement(declaration, isTopLevel) { 20 | if (isFlowEnabled) { 21 | if (flowTryParseStatement()) { 22 | return; 23 | @@ -113,10 +113,10 @@ export function parseStatement(declaration) { 24 | if (match(tt.at)) { 25 | parseDecorators(); 26 | } 27 | - parseStatementContent(declaration); 28 | + parseStatementContent(declaration, isTopLevel); 29 | } 30 | 31 | -function parseStatementContent(declaration) { 32 | +function parseStatementContent(declaration, isTopLevel) { 33 | if (isTypeScriptEnabled) { 34 | if (tsTryParseStatementContent()) { 35 | return; 36 | @@ -124,6 +124,7 @@ function parseStatementContent(declaration) { 37 | } 38 | 39 | const starttype = state.type; 40 | + const startTokenIndex = state.tokens.length 41 | 42 | // Most types of statements are recognized by the keyword they 43 | // start with. Many are trivial to parse, some require a bit of 44 | @@ -147,11 +148,13 @@ function parseStatementContent(declaration) { 45 | if (lookaheadType() === tt.dot) break; 46 | if (!declaration) unexpected(); 47 | parseFunctionStatement(); 48 | + state.tokens[startTokenIndex].isTopLevel = isTopLevel; 49 | return; 50 | 51 | case tt._class: 52 | if (!declaration) unexpected(); 53 | parseClass(true); 54 | + state.tokens[startTokenIndex].isTopLevel = isTopLevel; 55 | return; 56 | 57 | case tt._if: 58 | @@ -159,9 +162,11 @@ function parseStatementContent(declaration) { 59 | return; 60 | case tt._return: 61 | parseReturnStatement(); 62 | + state.tokens[startTokenIndex].isTopLevel = isTopLevel; 63 | return; 64 | case tt._switch: 65 | parseSwitchStatement(); 66 | + state.tokens[startTokenIndex].isTopLevel = isTopLevel; 67 | return; 68 | case tt._throw: 69 | parseThrowStatement(); 70 | @@ -247,11 +252,15 @@ function parseStatementContent(declaration) { 71 | simpleName = token.contextualKeyword; 72 | } 73 | } 74 | + 75 | + state.tokens[startTokenIndex].isTopLevel = isTopLevel; 76 | + 77 | if (simpleName == null) { 78 | semicolon(); 79 | return; 80 | } 81 | if (eat(tt.colon)) { 82 | + state.tokens[startTokenIndex].isTopLevel = isTopLevel; 83 | parseLabeledStatement(); 84 | } else { 85 | // This was an identifier, so we might want to handle flow/typescript-specific cases. 86 | @@ -581,9 +590,9 @@ export function parseBlock(isFunctionScope = false, contextId = 0) { 87 | state.scopeDepth--; 88 | } 89 | 90 | -export function parseBlockBody(end) { 91 | +export function parseBlockBody(end, isTopLevel) { 92 | while (!eat(end) && !state.error) { 93 | - parseStatement(true); 94 | + parseStatement(true, isTopLevel); 95 | } 96 | } 97 | 98 | diff --git a/node_modules/sucrase/dist/parser/traverser/statement.js b/node_modules/sucrase/dist/parser/traverser/statement.js 99 | index 6be3391..255fbb5 100644 100 | --- a/node_modules/sucrase/dist/parser/traverser/statement.js 101 | +++ b/node_modules/sucrase/dist/parser/traverser/statement.js 102 | @@ -89,7 +89,7 @@ var _lval = require('./lval'); 103 | var _util = require('./util'); 104 | 105 | function parseTopLevel() { 106 | - parseBlockBody(_types.TokenType.eof); 107 | + parseBlockBody(_types.TokenType.eof, true); 108 | _base.state.scopes.push(new (0, _state.Scope)(0, _base.state.tokens.length, true)); 109 | if (_base.state.scopeDepth !== 0) { 110 | throw new Error(`Invalid scope depth at end of file: ${_base.state.scopeDepth}`); 111 | @@ -104,7 +104,7 @@ var _util = require('./util'); 112 | // `if (foo) /blah/.exec(foo)`, where looking at the previous token 113 | // does not help. 114 | 115 | - function parseStatement(declaration) { 116 | + function parseStatement(declaration, isTopLevel) { 117 | if (_base.isFlowEnabled) { 118 | if (_flow.flowTryParseStatement.call(void 0, )) { 119 | return; 120 | @@ -113,10 +113,10 @@ var _util = require('./util'); 121 | if (_tokenizer.match.call(void 0, _types.TokenType.at)) { 122 | parseDecorators(); 123 | } 124 | - parseStatementContent(declaration); 125 | + parseStatementContent(declaration, isTopLevel); 126 | } exports.parseStatement = parseStatement; 127 | 128 | -function parseStatementContent(declaration) { 129 | +function parseStatementContent(declaration, isTopLevel) { 130 | if (_base.isTypeScriptEnabled) { 131 | if (_typescript.tsTryParseStatementContent.call(void 0, )) { 132 | return; 133 | @@ -124,6 +124,7 @@ function parseStatementContent(declaration) { 134 | } 135 | 136 | const starttype = _base.state.type; 137 | + const startTokenIndex = _base.state.tokens.length 138 | 139 | // Most types of statements are recognized by the keyword they 140 | // start with. Many are trivial to parse, some require a bit of 141 | @@ -147,11 +148,13 @@ function parseStatementContent(declaration) { 142 | if (_tokenizer.lookaheadType.call(void 0, ) === _types.TokenType.dot) break; 143 | if (!declaration) _util.unexpected.call(void 0, ); 144 | parseFunctionStatement(); 145 | + _base.state.tokens[startTokenIndex].isTopLevel = isTopLevel; 146 | return; 147 | 148 | case _types.TokenType._class: 149 | if (!declaration) _util.unexpected.call(void 0, ); 150 | parseClass(true); 151 | + _base.state.tokens[startTokenIndex].isTopLevel = isTopLevel; 152 | return; 153 | 154 | case _types.TokenType._if: 155 | @@ -159,6 +162,7 @@ function parseStatementContent(declaration) { 156 | return; 157 | case _types.TokenType._return: 158 | parseReturnStatement(); 159 | + _base.state.tokens[startTokenIndex].isTopLevel = isTopLevel; 160 | return; 161 | case _types.TokenType._switch: 162 | parseSwitchStatement(); 163 | @@ -210,6 +214,7 @@ function parseStatementContent(declaration) { 164 | if (_tokenizer.match.call(void 0, _types.TokenType._function) && !_util.canInsertSemicolon.call(void 0, )) { 165 | _util.expect.call(void 0, _types.TokenType._function); 166 | parseFunction(functionStart, true); 167 | + _base.state.tokens[startTokenIndex].isTopLevel = isTopLevel; 168 | return; 169 | } else { 170 | _base.state.restoreFromSnapshot(snapshot); 171 | @@ -247,11 +252,15 @@ function parseStatementContent(declaration) { 172 | simpleName = token.contextualKeyword; 173 | } 174 | } 175 | + 176 | + _base.state.tokens[startTokenIndex].isTopLevel = isTopLevel; 177 | + 178 | if (simpleName == null) { 179 | _util.semicolon.call(void 0, ); 180 | return; 181 | } 182 | if (_tokenizer.eat.call(void 0, _types.TokenType.colon)) { 183 | + _base.state.tokens[startTokenIndex].isTopLevel = false; 184 | parseLabeledStatement(); 185 | } else { 186 | // This was an identifier, so we might want to handle flow/typescript-specific cases. 187 | @@ -581,9 +590,9 @@ function parseIdentifierStatement(contextualKeyword) { 188 | _base.state.scopeDepth--; 189 | } exports.parseBlock = parseBlock; 190 | 191 | - function parseBlockBody(end) { 192 | + function parseBlockBody(end, isTopLevel) { 193 | while (!_tokenizer.eat.call(void 0, end) && !_base.state.error) { 194 | - parseStatement(true); 195 | + parseStatement(true, isTopLevel); 196 | } 197 | } exports.parseBlockBody = parseBlockBody; 198 | 199 | diff --git a/node_modules/sucrase/dist/types/parser/tokenizer/index.d.ts b/node_modules/sucrase/dist/types/parser/tokenizer/index.d.ts 200 | index 45cd799..66b2480 100644 201 | --- a/node_modules/sucrase/dist/types/parser/tokenizer/index.d.ts 202 | +++ b/node_modules/sucrase/dist/types/parser/tokenizer/index.d.ts 203 | @@ -51,6 +51,7 @@ export declare class Token { 204 | isOptionalChainEnd: boolean; 205 | subscriptStartIndex: number | null; 206 | nullishStartIndex: number | null; 207 | + isTopLevel?: boolean; 208 | } 209 | export declare function next(): void; 210 | export declare function nextTemplateToken(): void; 211 | -------------------------------------------------------------------------------- /www/docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | id: docs 3 | title: JARLE 4 | slug: / 5 | --- 6 | {/* 7 | import providerMeta from '@metadata/Provider'; 8 | import editor from '@metadata/Editor'; 9 | import error from '@metadata/Error'; 10 | import preview from '@metadata/Preview'; */} 11 | import PropsList from '@theme/PropsList'; 12 | 13 | Write code and see the result as you type. 14 | 15 | ## Overview 16 | 17 | ```jsx live 18 |

Hello World!

19 | ``` 20 | 21 | JARLE only looks at the last thing you return, so write whatever you need in front 22 | of it. 23 | 24 | ```jsx live 25 | const DEFAULT = 'World'; 26 | 27 | function Greet({ subject = DEFAULT }) { 28 | return
Hello {subject}
; 29 | } 30 | 31 | class ClassyGreet extends React.Component { 32 | render() { 33 | const { subject } = this.props; 34 | return Hello {subject}; 35 | } 36 | } 37 | 38 | <> 39 | 40 | 41 | 42 | ; 43 | ``` 44 | 45 | If the last expression is a valid React element type it'll render that as well: 46 | 47 | ```jsx live 48 | const DEFAULT = 'World'; 49 | 50 | function Greet() { 51 | return
Hello world
; 52 | } 53 | ``` 54 | 55 | Or with class components 56 | 57 | ```jsx live 58 | class ClassyGreet extends React.Component { 59 | render() { 60 | return Hello world; 61 | } 62 | } 63 | ``` 64 | 65 | If you want to be explicit you can also `export` a value directly (only the default export is used). 66 | 67 | ```jsx live 68 | export default React.forwardRef(() => { 69 | return I'm unique!; 70 | }); 71 | ``` 72 | 73 | For punchy terse demostrations of component render logic, use `renderAsComponent` 74 | to have JARLE use your code as the body of a React function component. 75 | 76 | ```jsx live renderAsComponent 77 | const [seconds, setSeconds] = useState(0); 78 | 79 | useEffect(() => { 80 | let interval = setInterval(() => { 81 | setSeconds((prev) => prev + 1); 82 | }, 1000); 83 | 84 | return () => clearInterval(interval); 85 | }, []); 86 | 87 | return
Seconds past: {seconds}
; 88 | ``` 89 | 90 | If you do need more control over what get's rendered, or need to render asynchronously, a 91 | `render` function is always in scope: 92 | 93 | ```jsx live 94 | setTimeout(() => { 95 | render(
I'm late!
); 96 | }, 1000); 97 | ``` 98 | 99 | ### TypeScript 100 | 101 | JARLE supports compiling TypeScript if you specify the language as such. 102 | 103 | ```tsx live 104 | import type { Names } from './types'; 105 | 106 | interface Props { 107 | name: string; 108 | } 109 | 110 | function Greeting({ name }: Props) { 111 | return Hello {name}; 112 | } 113 | 114 | ; 115 | ``` 116 | 117 | ## Scope 118 | 119 | You can control which values, clases, components, etc are provided automatically 120 | to the example code when it's run by adjusting the `scope`. The `scope` is an object 121 | map of identifiers and their values. The `React` namespace is provided as well as 122 | all of the built-in hooks (useState, useRef, etc) automatically along with a `render()` 123 | function for finely tuning the returned element. 124 | 125 | You can also add our own values: 126 | 127 | ```jsx 128 | import lodash from 'lodash'; 129 | 130 | ; 131 | ``` 132 | 133 | ## Importing modules in examples 134 | 135 | Import can be used in code blocks (as long as the browser supports [dynamic imports](https://caniuse.com/es6-module-dynamic-import)). 136 | 137 | ```jsx live 138 | import confetti from 'https://cdn.skypack.dev/canvas-confetti'; 139 | 140 | ; 141 | ``` 142 | 143 | Import statements are extracted from the example code and the requests 144 | are passed to a function responsible for resolving them into modules. 145 | 146 | The default behavior uses `import()`, relying on the browsers module support. 147 | You can however, customize the importer to suit your needs. The resolver 148 | may return an object map of possible requests to their module, useful 149 | when you only want to expose a few local values to examples (see also `scope`). 150 | 151 | ```jsx 152 | ({ 154 | lodash: lodash, 155 | 'my-component': { default: () => Hey }, 156 | })} 157 | /> 158 | ``` 159 | 160 | For dynamic resolution the resolve is also passed a list of requests to be mapped 161 | to their modules. Here is the default implementation 162 | 163 | ```jsx 164 | 166 | promise.all(requests.map((request) => import(request))) 167 | } 168 | /> 169 | ``` 170 | 171 | You can also mix together some of your own static analysis and tooling to 172 | build really neat integrations where imports are resolved a head of time, using webpack 173 | or other bundlers. 174 | 175 | > Note: Typescript type only imports are not passed to the import resolver, since they 176 | > are compile-time only fixtures. 177 | 178 | ## Usage 179 | 180 | A full example of how to use JARLE via another JARLE editor! 181 | 182 | ```jsx live inline=false 183 | import { Provider, Editor, Error, Preview, themes } from 'jarle'; 184 | 185 | const code = ` 186 | Here's an editor inside an editor 187 | `; 188 | 189 |
190 |

Yo Dawg I heard you liked editors

191 | 192 | 193 | 194 | 195 | 196 | 197 |
; 198 | ``` 199 | 200 | ### Render into an iframe 201 | 202 | Previews are all rendered into the same document as the editor so they share stuff 203 | like global CSS. If want to sandbox the result from the parent document, render 204 | your preview into an `