├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── toc.yml ├── .gitignore ├── LICENSE ├── README.md ├── babel.config.js ├── bin └── cli.js ├── fixtures ├── README.md └── basic │ ├── react │ ├── component.react │ └── super │ │ └── nexted │ │ └── foo.react │ └── src │ ├── component.js │ └── super │ └── nexted │ └── foo.js ├── jest.config.js ├── package-lock.json ├── package.json ├── src └── compiler.ts ├── tests ├── README.md ├── __snapshots__ │ └── snapshots.test.ts.snap ├── binding-nested-obj │ └── src.react ├── options-displayName │ ├── options.json │ └── src.react ├── snapshots.test.ts ├── state-simple │ └── src.react ├── styles-static │ └── src.react └── temp │ └── src.react └── tsconfig.json /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/toc.yml: -------------------------------------------------------------------------------- 1 | on: push 2 | name: TOC Generator 3 | jobs: 4 | generateTOC: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: technote-space/toc-generator@v2 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | out 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and not Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | # Stores VSCode versions used for testing VSCode extensions 109 | .vscode-test 110 | 111 | # yarn v2 112 | .yarn/cache 113 | .yarn/unplugged 114 | .yarn/build-state.yml 115 | .yarn/install-state.gz 116 | .pnp.* 117 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License Copyright (c) 2020 sw-yx 2 | 3 | Permission is hereby granted, free of 4 | charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, copy, modify, merge, 7 | publish, distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to the 9 | following conditions: 10 | 11 | The above copyright notice and this permission notice 12 | (including the next paragraph) shall be included in all copies or substantial 13 | portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO 18 | EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 19 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Experimental React Single File Components 2 | 3 | Swyx's Experimental Proposal for bringing Single File Components to React. [Other proposals can be found here](https://github.com/react-sfc/react-sfc-proposal). The specific APIs are unstable for now and have already changed from what was shown at the React Rally talk! 4 | 5 | > :warning: This is an experiment/proof of concept, and is a solo endeavor not endorsed by the React team. There are legitimate design concerns raised (see Concerns section below). It may remain a toy unless other folks pick it up/help contribute/design/maintain it! [Let me know what your interest is and help spread the word](https://twitter.com/swyx). 6 | 7 | See philosophical discussion at React Rally 2020: https://www.youtube.com/watch?v=18F5v1diO_A 8 | 9 | ## Usage 10 | 11 | 2 ways use React SFCs in your app: 12 | 13 | ### As a CLI 14 | 15 | To gradually adopt this in **pre-existing** React projects - you can leave your project exactly as is and only write individual SFCs in a separate folder, without touching your bundler config at all. 16 | 17 | - `npm i react-sfc` 18 | - Create a `/react` *origin* folder in your project to watch and compile **from**. 19 | - We assume you have a destination `/src` *output* folder with the rest of your app, to compile **to**. 20 | - run `react-sfc watch` or `rsfc watch`. 21 | - Now you are free to create `/react/MyButton.react` files in that folder 22 | 23 | CLI Flags: 24 | - If you need to customize the names of the folders that you are compiling **from** and compiling **to**, you can pass CLI flags: `react-sfc watch -f MYORIGINFOLDER -t MYOUTPUTFOLDER` 25 | - By default, the CLI compiles `.react` files into `.js` files. If you need it to output `.tsx` files or other, you can pass the extension flag `--extension tsx` or `-e tsx`. *Note: the developer experience for this is not yet tested*. 26 | 27 | Other commands: 28 | 29 | - if you don't need a `watch` workflow, you can also do single runs with other commands (same CLI flags apply): 30 | - `react-sfc build` to build once 31 | - `react-sfc validate` to parse your origin folder without building, to check for errors 32 | 33 | ### As a Rollup plugin 34 | 35 | In a new or pre-existing React + Rollup project 36 | 37 | - Plugin: https://github.com/sw-yx/rollup-plugin-react-sfc 38 | - Demo: https://github.com/sw-yx/rollup-react-boilerplate 39 | 40 | ## Other ways 41 | 42 | TBD. need help to write a webpack plugin version of this. 43 | 44 | --- 45 | 46 | > Special note to readers: this package is deployed to `react-sfc` on npm right now - but i am not going to be selfish at all about this. if someone else comes along with a better impl i will give you the npm name and github org. Please come and take it. 47 | 48 | ## Table of Contents 49 | 50 | 51 | 52 | **Table of Contents** 53 | 54 | - [Design Goals](#design-goals) 55 | - [In 1 image](#in-1-image) 56 | - [Features implemented](#features-implemented) 57 | - [Basic Proposal](#basic-proposal) 58 | - [Advanced Opportunities](#advanced-opportunities) 59 | - [CSS in JS](#css-in-js) 60 | - [State](#state) 61 | - [Binding](#binding) 62 | - [GraphQL](#graphql) 63 | - [Dev Optimizations](#dev-optimizations) 64 | - [Why? I don't need this!](#why-i-dont-need-this) 65 | - [General principle: Loaders vs SFCs](#general-principle-loaders-vs-sfcs) 66 | - [Notable Concerns](#notable-concerns) 67 | - [Am I missing some obvious idea or some critical flaw?](#am-i-missing-some-obvious-idea-or-some-critical-flaw) 68 | 69 | 70 | 71 | 72 | ## Design Goals 73 | 74 | - Stay "Close to JavaScript" to benefit from existing tooling: syntax highlighting, autocomplete/autoimport, static exports, TypeScript 75 | - Have easy upgrade paths to go from a basic component to dynamic styles, or add state, or extract graphql dependencies 76 | - Reduce verbosity without sacrificing readability 77 | 78 | This probably means that a successful React SFC should be a superset of normal React: you should be able to rename any `.js` and `.jsx` file and it should "just work", before taking advantage of any new features. 79 | 80 | ## In 1 image 81 | 82 | ![image](https://user-images.githubusercontent.com/6764957/90271942-32ab5400-de8f-11ea-91a8-8a0cebebd6aa.png) 83 | 84 | 85 | 86 | ## Features implemented 87 | 88 | - [x] Automatic react import 89 | - [x] mutable useState `_` syntax 90 | - [x] useStateWithLabel hook replaces useState to label in prod 91 | - [x] Dynamic CSS transform to styled-JSX 92 | - [x] set displayName if passed as compiler option 93 | - [x] `$value={$text}` binding for onChange 94 | - this works for nested properties eg `$value={$text.foo}` 95 | 96 | TODO: 97 | 98 | - [ ] JS and CSS sourcemaps 99 | - [ ] it does not properly work with `styled-jsx` in rollup - need [SUPER hacky shit](https://twitter.com/swyx/status/1290055528068952064) to work (see boilerplate's index.html) 100 | - [ ] useEffect dependency tracking 101 | - [ ] automatically extract text for i18n 102 | - [ ] nothing graphql related yet 103 | - [ ] optional `css` no-op function for syntax highlighting in JS 104 | - [ ] $value shorthand eg `$value` 105 | - [ ] $value generalized eg `$style` 106 | - [ ] handle multiple bindings 107 | - [ ] test for TSX support? 108 | 109 | open questions 110 | 111 | - what binding syntax is best? 112 | - considered `bind:value` but typescript does not like that 113 | - `$` prefix works but doesnt look coherent with the rest of RSFC format. using this for now 114 | - `_` prefix looks ugly? <- went with this one 115 | 116 | ## Basic Proposal 117 | 118 | Here is how we might write a React Single File Component: 119 | 120 | ```js 121 | let _count = 1 122 | 123 | export const STYLE = ` 124 | div { /* scoped by default */ 125 | background-color: ${_count > 4 ? "papayawhip" : "palegoldenrod"}; 126 | } 127 | ` 128 | 129 | export default () => { 130 | useEffect(() => console.log('rerendered')) 131 | return ( 132 | 135 | ) 136 | } 137 | ``` 138 | 139 | The component name would be taken from the filename. Named exports would also be externally accessible. 140 | 141 | ## Advanced Opportunities 142 | 143 | These require more work done by the surrounding compiler/distribution, and offer a lot of room for innovation: 144 | 145 | ### CSS in JS 146 | 147 | We can switch nicely from no-runtime scoped styles to CSS-in-JS: 148 | 149 | ```js 150 | export const STYLE = props => ` 151 | div { 152 | background-color: ${props.bgColor || 'papayawhip'}; 153 | } 154 | ` 155 | // etc 156 | ``` 157 | 158 | In future we might offer a no-op `css` function that would make it easier for editor tooling to do CSS in JS syntax highlighting: 159 | 160 | ```js 161 | // NOT YET IMPLEMENTED 162 | export const STYLE = css` 163 | div { /* properly syntax highlighted */ 164 | background-color: blue; 165 | } 166 | ` 167 | ``` 168 | 169 | ### State 170 | 171 | We can declare mutable state: 172 | 173 | ```js 174 | let _count = 0 175 | 176 | export const STYLE = ` 177 | button { 178 | // scoped by default 179 | background-color: ${_count > 5 ? 'red' : 'papayawhip'}; 180 | } 181 | ` 182 | 183 | export default () => { 184 | return 185 | } 186 | ``` 187 | 188 |
189 | 190 | and this is transformed to the appropriate React APIs. 191 | 192 | 193 | 194 | ```js 195 | export default const FILENAME = () => { 196 | const [_count, set_Count] = useState(0); 197 | return ( 198 | <> 199 | 200 | 208 | 209 | ); 210 | }; 211 | ``` 212 | 213 |
214 | 215 | 216 | We can also do local two way binding to make forms a lot easier: 217 | 218 | 219 | ```js 220 | let data = { 221 | firstName: '', 222 | lastName: '', 223 | age: undefined, 224 | } 225 | 226 | function onSubmit(event) { 227 | event.preventDefault() 228 | fetch('/myendpoint, { 229 | method: 'POST', 230 | body: JSON.stringify(data) 231 | }) 232 | } 233 | 234 | export default () => { 235 | return ( 236 |
237 | 241 | 245 | 249 | 250 |
251 | ) 252 | } 253 | ``` 254 | 255 | ### Binding 256 | 257 | Local two way binding can be really nice. 258 | 259 | 260 | ```js 261 | let _text = 0 262 | 263 | export default () => { 264 | return 265 | } 266 | ``` 267 | 268 | And this transpiles to the appropriate `onChange` handler and `value` attribute. It would also have to handle object access. 269 | 270 | Another feature from Vue and Svelte that is handy is class binding. JSX only offers className as a string. We could do better: 271 | 272 | 273 | ```js 274 | // NOT YET IMPLEMENTED 275 | let _foo = 0 276 | let _bar = 0 277 | 278 | export default () => { 279 | return
280 | Test 284 | 285 | 286 | 287 | } 288 | ``` 289 | 290 | ### GraphQL 291 | 292 | The future of React is [Render-as-you-Fetch](https://reactjs.org/docs/concurrent-mode-suspense.html#approach-3-render-as-you-fetch-using-suspense) data, and being able to statically extract the data dependencies from the component (without rendering it) is important to avoid Data waterfalls: 293 | 294 | ```js 295 | // NOT YET IMPLEMENTED 296 | export const GRAPHQL = ` 297 | query MYPOSTS { 298 | posts { 299 | title 300 | author 301 | } 302 | } 303 | ` 304 | // NOT YET IMPLEMENTED 305 | export default function MYFILE (props, {data, status}) { 306 | if (typeof status === Error) return
Error {data.state.message}
307 | return ( 308 |
309 | Posts: 310 | {status.isLoading() ?
Loading...
311 | : ( 312 |
    313 | {data.map((item, i) =>
  • {item}
  • )} 314 |
315 | ) 316 | } 317 |
318 | ) 319 | } 320 | } 321 | ``` 322 | 323 | ### Dev Optimizations 324 | 325 | We can offer other compile time optimizations for React: 326 | 327 | - Named State Hooks 328 | 329 | Automatically insert `useDebugValue` for each `useState`: 330 | 331 | ```js 332 | function useStateWithLabel(initialValue, name) { 333 | const [value, setValue] = useState(initialValue); 334 | useDebugValue(`${name}: ${value}`); 335 | return [value, setValue]; 336 | } 337 | ``` 338 | 339 | - Auto optimized useEffect 340 | 341 | Automatically insert all dependencies when using `useAutoEffect`, exactly similar to https://github.com/yuchi/hooks.macro 342 | 343 | ## Why? I don't need this! 344 | 345 | That's right, you don't -need- it. SFCs are always sugar, just like JSX. You don't need it, but when it is enough of a community standard it makes things nicer for almost everyone. SFC's aren't a required part of Vue, but they are a welcome community norm. 346 | 347 | The goal isn't to evaluate this idea based on need. In my mind this will live or die based on how well it accomplishes two goals: 348 | 349 | - For beginners, provide a blessed structure in a chaotic world of anything-goes. 350 | - For experts, provide a nicer DX by encoding extremely common boilerplatey patterns in syntax. 351 | 352 | Any new file format starts with a handicap of not working with existing tooling e.g. syntax highlighting. So a successful React SFC effort will also need to have a plan for critical tooling. 353 | 354 | ## General principle: Loaders vs SFCs 355 | 356 | Stepping back from concrete examples to discuss how this might affect DX. In a sense, SFCs simply centralize what we already do with loaders. Instead of 357 | 358 | ``` 359 | Component.jsx 360 | Component.scss 361 | Component.graphql 362 | ``` 363 | 364 | we have 365 | 366 | ```js 367 | export const STYLE // etc 368 | export const GRAPHQL // etc 369 | export default () =>
// etc 370 | ``` 371 | 372 | in a file. Why would we exchange file separation for a super long file? Although there are ways to mitigate this, it is not very appealing on its own. 373 | 374 | However, to the extent that the React SFC loader is a single entry point to webpack for all these different filetypes, we have the opportunity to simplify config, skip small amounts of boilerplate, and enforce some consistency with the single file format. Having fewer files causes less pollution of IDE file namespace, and makes it easier to set up these peripheral concerns around jsx (styling, data, tests, documentation, etc) incrementally without messing with creating/deleting files. 375 | 376 | ## Notable Concerns 377 | 378 | - "This is a sugar that makes things more complicated/confusing, not less. Like people aren't going to understand the boundaries of what is allowed here. Like if you can mutate the binding, can you mutate the value? Either way will cause confusion" - [source](https://twitter.com/buildsghost/status/1294355186538799105?s=20) 379 | - i need to think about this but i might end up agreeing 380 | - it might be possible to design around this 381 | 382 | 383 | ## Am I missing some obvious idea or some critical flaw? 384 | 385 | File an issue or PR or [tweet at me](https://twitter.com/swyx), lets chat. 386 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@babel/preset-typescript' 4 | ] 5 | } -------------------------------------------------------------------------------- /bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // 👆 Used to tell Node.js that this is a CLI tool 3 | 4 | "use strict"; 5 | 6 | const sade = require("sade"); 7 | const prog = sade("react-sfc"); 8 | const chalk = require("chalk"); 9 | const CheapWatch = require("cheap-watch"); 10 | const { Compiler } = require("../dist/compiler"); 11 | 12 | const fs = require("fs"); 13 | const path = require("path"); 14 | const output = fs.readFileSync(path.join(__dirname, "../package.json"), "utf8"); 15 | 16 | prog 17 | .version(output.version) 18 | .command("watch") 19 | .describe( 20 | "Transpile and watch a directory of .react files to .js and .css files (default from react to src). Ctrl+C to kill." 21 | ) 22 | .example("watch") 23 | .option("--from, -f", "Source dir (default react)") 24 | .option("--to, -t", "Dist dir (default src)") 25 | .option("--extension, -e", "Filetype (default js)") 26 | .action(async (opts) => { 27 | const src = opts.from || "react"; 28 | const dst = opts.to || "src"; 29 | const outputExtension = opts.extension || "js"; // pass tsx for typescript? 30 | console.log( 31 | `> watching files from ${chalk.cyan(src)} to ${chalk.cyan(dst)}` 32 | ); 33 | 34 | const watch = new CheapWatch({ dir: src }); 35 | await watch.init(); 36 | buildOnce({ paths: watch.paths, src, dst, outputExtension }); 37 | 38 | watch.on("+", ({ path: _path, stats, isNew }) => { 39 | if (stats.isDirectory()) return; 40 | compileFile({ src, dst, semiPath: _path, outputExtension }); 41 | const phrase = isNew ? "created" : "updated"; 42 | console.log(`> ${phrase} ` + path.join(dst, _path)); 43 | }); 44 | watch.on("-", ({ path: _path, stats }) => { 45 | const p = path.join(dst, _path); 46 | fs.unlinkSync(p); 47 | console.log("> deleted " + p); 48 | }); 49 | }); 50 | 51 | prog 52 | .command("build") 53 | .describe( 54 | "Transpile only once a directory of .react files to .js and .css files (default from react to src). Ctrl+C to kill." 55 | ) 56 | .example("build") 57 | .option("--from, -f", "Source dir (default react)") 58 | .option("--to, -t", "Dist dir (default src)") 59 | .option("--extension, -e", "Filetype (default js)") 60 | .action(async (opts) => { 61 | const src = opts.from || "react"; 62 | const dst = opts.to || "src"; 63 | const outputExtension = opts.extension || "js"; // pass tsx for typescript? 64 | console.log( 65 | `> building files from ${chalk.cyan(src)} to ${chalk.cyan(dst)}` 66 | ); 67 | 68 | const watch = new CheapWatch({ dir: src, watch: false }); 69 | await watch.init(); 70 | buildOnce({ paths: watch.paths, src, dst, outputExtension }); 71 | }); 72 | 73 | 74 | /** 75 | * 76 | * CORE FUNCTIONALITY 77 | * 78 | */ 79 | 80 | function buildOnce({ paths, src, dst, outputExtension }) { 81 | for (const [changedFileSemiPath, stats] of paths) { 82 | try { 83 | if (!stats.isDirectory()) { 84 | compileFile({ src, dst, semiPath: changedFileSemiPath, outputExtension }); 85 | console.log("> output " + path.join(dst, changedFileSemiPath)); 86 | // todo: check dst dir to delete files that are no longer in src? 87 | } 88 | } catch (err) { 89 | console.warn( 90 | chalk.yellow( 91 | `Unable to compile ${changedFileSemiPath}, please edit and save` 92 | ) 93 | ); 94 | console.error(err); 95 | } 96 | } 97 | } 98 | 99 | function compileFile({ src, dst, semiPath, outputExtension }) { 100 | if (!semiPath.endsWith(".react")) return; 101 | try { 102 | const changedFilePath = path.join(src, semiPath); 103 | const code = fs.readFileSync(changedFilePath, "utf8"); 104 | const output = Compiler({ code }); 105 | const pathWithNoReact = 106 | semiPath.slice(0, semiPath.length - 5) + outputExtension; 107 | const destinaFilePath = path.join(dst, pathWithNoReact); 108 | mkdirp(destinaFilePath); 109 | fs.writeFileSync(destinaFilePath, output.js.code); 110 | } catch (err) { 111 | console.warn( 112 | chalk.yellow(`Unable to compile ${semiPath}, please edit and save`) 113 | ); 114 | console.error(err); 115 | } 116 | } 117 | 118 | 119 | 120 | prog.parse(process.argv); 121 | 122 | 123 | /** 124 | * 125 | * UTILS 126 | * 127 | */ 128 | 129 | // stats: Stats { 130 | // dev: 16777220, 131 | // mode: 33188, 132 | // nlink: 1, 133 | // uid: 501, 134 | // gid: 20, 135 | // rdev: 0, 136 | // blksize: 4096, 137 | // ino: 14077282, 138 | // size: 37, 139 | // blocks: 8, 140 | // atimeMs: 1596930405579.274, 141 | // mtimeMs: 1596930404101.0032, 142 | // ctimeMs: 1596930404101.0032, 143 | // birthtimeMs: 1596930394717.8994, 144 | // atime: 2020-08-08T23:46:45.579Z, 145 | // mtime: 2020-08-08T23:46:44.101Z, 146 | // ctime: 2020-08-08T23:46:44.101Z, 147 | // birthtime: 2020-08-08T23:46:34.718Z 148 | // } 149 | 150 | function mkdirp(path) { 151 | path.split("/").reduce(function (prev, next) { 152 | if (!fs.existsSync(prev)) fs.mkdirSync(prev); 153 | return prev + "/" + next; 154 | }); 155 | } 156 | -------------------------------------------------------------------------------- /fixtures/README.md: -------------------------------------------------------------------------------- 1 | this folder is meant as a test for the cli version of `react-sfc`. 2 | 3 | 4 | as a contributor, here's what you do 5 | 6 | - stay at project root 7 | - `./bin/cli.js watch -f fixtures/basic/react -t fixtures/basic/src` 8 | - now you have a running react-sfc server 9 | - add files to `/fixtures/basic/react` 10 | - watch them get populated in `src` -------------------------------------------------------------------------------- /fixtures/basic/react/component.react: -------------------------------------------------------------------------------- 1 | let _abc = 123 2 | 3 | export default () => { 4 | return 5 | } -------------------------------------------------------------------------------- /fixtures/basic/react/super/nexted/foo.react: -------------------------------------------------------------------------------- 1 | let _sdlkj = { 2 | foo: 23 3 | } 4 | 5 | export default () => { 6 | return 7 | } -------------------------------------------------------------------------------- /fixtures/basic/src/component.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default () => { 4 | let [_abc, set_abc] = use_abc_State(123) 5 | return 6 | } 7 | function use_abc_State(v) { 8 | const x = React.useState(v); 9 | React.useDebugValue('_abc: ' + x[0]); return x; 10 | } -------------------------------------------------------------------------------- /fixtures/basic/src/super/nexted/foo.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default () => { 4 | let [_sdlkj, set_sdlkj] = use_sdlkj_State({ 5 | foo: 23 6 | }) 7 | return { 9 | let temp = Object.assign({}, foo); 10 | temp._sdlkj = e.target.value; 11 | setfoo(temp); 12 | }}> 13 | } 14 | function use_sdlkj_State(v) { 15 | const x = React.useState(v); 16 | React.useDebugValue('_sdlkj: ' + x[0]); return x; 17 | } -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-sfc", 3 | "version": "0.1.1", 4 | "description": "Swyx's proposal for bringing Single File Components to React. [Other proposals can be found here](https://github.com/react-sfc/react-sfc-proposal).", 5 | "main": "dist/compiler.js", 6 | "bin": { 7 | "react-sfc": "./bin/cli.js", 8 | "rsfc": "./bin/cli.js" 9 | }, 10 | "files": [ 11 | "src", 12 | "dist", 13 | "README.md", 14 | "LICENSE" 15 | ], 16 | "directories": { 17 | "test": "tests" 18 | }, 19 | "scripts": { 20 | "build": "tsc", 21 | "version": "auto-changelog -p --template keepachangelog && git add CHANGELOG.md", 22 | "prepublishOnly": "npm run build && git push && git push --tags && gh-release", 23 | "test": "jest --watch" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/react-sfc/react-sfc-swyx.git" 28 | }, 29 | "keywords": [], 30 | "author": "swyx", 31 | "license": "MIT", 32 | "bugs": { 33 | "url": "https://github.com/react-sfc/react-sfc-swyx/issues" 34 | }, 35 | "homepage": "https://github.com/react-sfc/react-sfc-swyx#readme", 36 | "devDependencies": { 37 | "@babel/preset-typescript": "^7.10.4", 38 | "@types/estree": "0.0.45", 39 | "@types/jest": "^26.0.9", 40 | "acorn-jsx": "^5.2.0", 41 | "auto-changelog": "^2.2.0", 42 | "gh-release": "^3.5.0", 43 | "jest": "^26.2.2", 44 | "ts-jest": "^26.1.4", 45 | "typescript": "^3.9.7" 46 | }, 47 | "dependencies": { 48 | "acorn": "^7.4.0", 49 | "chalk": "^4.1.0", 50 | "cheap-watch": "^1.0.2", 51 | "estree-walker": "^2.0.1", 52 | "magic-string": "^0.25.7", 53 | "sade": "^1.7.3" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/compiler.ts: -------------------------------------------------------------------------------- 1 | import { walk } from "estree-walker"; 2 | const { Parser } = require("acorn"); 3 | import MagicString from "magic-string"; 4 | import { 5 | ImportDeclaration, 6 | TemplateLiteral, 7 | ExportNamedDeclaration, 8 | ArrowFunctionExpression, 9 | ExportDefaultDeclaration, 10 | FunctionDeclaration, 11 | ReturnStatement, 12 | VariableDeclaration, 13 | VariableDeclarator, 14 | AssignmentExpression, 15 | UpdateExpression, 16 | Identifier, 17 | Literal, 18 | MemberExpression, 19 | Expression, 20 | } from "estree"; 21 | 22 | const hooks = [ 23 | "useState", 24 | "useEffect", 25 | "useContext", 26 | "useReducer", 27 | "useCallback", 28 | "useMemo", 29 | "useRef", 30 | "useImperativeHandle", 31 | "useLayoutEffect", 32 | "useDebugValue", 33 | ]; 34 | 35 | interface ASTNode { 36 | start: number; 37 | end: number; 38 | type: string; 39 | } 40 | 41 | interface JSXOpeningElement extends ASTNode { 42 | type: "JSXOpeningElement"; 43 | attributes: JSXAttribute[]; 44 | name: JSXIdentifier; 45 | selfClosing: boolean; 46 | } 47 | interface JSXClosingElement extends ASTNode { 48 | type: "JSXClosingElement"; 49 | name: JSXIdentifier; 50 | } 51 | interface JSXElement extends ASTNode { 52 | type: "JSXElement"; 53 | attributes: JSXAttribute[]; 54 | openingElement: JSXOpeningElement; 55 | closingElement: JSXClosingElement; 56 | children: (JSXElement | JSXText)[]; 57 | } 58 | interface JSXText extends ASTNode { 59 | type: "JSXText"; 60 | value: string; 61 | raw: string; 62 | } 63 | interface JSXAttribute extends ASTNode { 64 | type: "JSXAttribute"; 65 | name: JSXNamespacedName | JSXIdentifier; 66 | value: JSXExpressionContainer | Literal; 67 | } 68 | interface JSXIdentifier extends ASTNode { 69 | type: "JSXIdentifier"; 70 | name: string; 71 | } 72 | interface JSXNamespacedName extends ASTNode { 73 | type: "JSXNamespacedName"; 74 | namespace: JSXIdentifier; 75 | name: JSXIdentifier; 76 | } 77 | interface JSXExpressionContainer extends ASTNode { 78 | type: "JSXExpressionContainer"; 79 | expression: Expression; 80 | } 81 | 82 | type CompilerOptions = { 83 | useStateWithLabel?: boolean; 84 | componentDisplayName?: string; 85 | }; 86 | 87 | export function Compiler(args: { 88 | code: string; 89 | parser?: any; // TODO: parser type 90 | options?: CompilerOptions; 91 | }) { 92 | // console.log({acorn}) 93 | function defaultacorn(x: string) { 94 | const MyParser = Parser.extend(require("acorn-jsx")()); 95 | return MyParser.parse(x, { 96 | sourceType: "module", 97 | }); 98 | } 99 | const parser = args.parser || defaultacorn; 100 | const ast: ASTNode = parser(args.code); 101 | let ms = new MagicString(args.code); 102 | 103 | const isProduction = false; // TODO: figure out how to get this from rollup 104 | // undocumented option - tbd if we actually want to let users configure 105 | // TODO: can make it dev-only, or maybe also useful in prod? 106 | const options = args.options || ({} as CompilerOptions); 107 | const componentDisplayName = options.componentDisplayName; 108 | const userWantsUSWL = options.useStateWithLabel || !isProduction; 109 | 110 | let STYLECONTENT, STYLEDECLARATION: any; // typescript is stupid about assignment inside an if, thinks this is a `never` or a `undefined` 111 | let STYLESTATICCSS: any; // just stubbing this out 112 | 113 | /** refactoring to exportDefaultNode.. remind me to delete */ 114 | // let lastChildofDefault: any; // typescript is stupid about assignment inside an if, thinks this is a `never` or a `undefined` 115 | // let pos_HeadOfDefault: number; 116 | 117 | let exportDefaultNode: { 118 | lastChildofDefault?: any; 119 | pos_HeadOfDefault?: number; 120 | node?: ExportDefaultDeclaration & ASTNode; 121 | } = {}; 122 | let stateMap = new Map(); 123 | let assignmentsMap = new Map(); 124 | let bindValuesMap = new Map(); 125 | let isReactImported = false; 126 | let hasExportDefault = false; // ensure there is a default export 127 | walk(ast, { 128 | enter(node, parent, prop, index) { 129 | if (node.type === "ImportDeclaration") { 130 | let _node = node as ImportDeclaration; 131 | if (_node.source.value === "react") isReactImported = true; 132 | // TODO: check that the name React is actually defined! 133 | // most people will not run into this, but you could be nasty 134 | } 135 | if (node.type === "CallExpression") { 136 | let _node = node as any; // CallExpression would be nice but SimpleLiteral is annoying 137 | if (hooks.some((hook) => _node.callee.name === hook)) { 138 | ms.prependLeft(_node.callee.start, "React."); 139 | } 140 | } 141 | 142 | if (node.type === "ExportNamedDeclaration") { 143 | let _node = (node as ExportNamedDeclaration).declaration; 144 | if ( 145 | _node && 146 | _node.type === "VariableDeclaration" && 147 | _node?.declarations[0].id.type === "Identifier" 148 | ) { 149 | if (_node?.declarations[0].id.name === "STYLE") { 150 | let loc = _node.declarations[0].init as ( 151 | | TemplateLiteral 152 | | ArrowFunctionExpression 153 | ) & 154 | ASTNode; // klooge as i believe TemplateLiteral type is incomplete 155 | // TODO: check return type of ArrowFunctionExpression as well 156 | 157 | /** 158 | * 159 | * MVP of css static export feature 160 | * 161 | */ 162 | if (loc.type === "TemplateLiteral" && loc.quasis.length === 1) { 163 | STYLESTATICCSS = loc.quasis[0].value.raw; 164 | // TODO: namespace to scope to component 165 | // TODO: take care of css nesting 166 | } 167 | /** 168 | * 169 | * end MVP of css static export feature 170 | * 171 | */ 172 | 173 | if (loc) { 174 | STYLEDECLARATION = node as VariableDeclaration & ASTNode; 175 | STYLECONTENT = ms.slice(loc.start, loc.end); 176 | } 177 | // TODO - consider whether to handle Literal (just string) 178 | // TODO - there are bunch of other expression types we may someday need to support (unlikely) 179 | } 180 | } 181 | } 182 | if (node.type === "ExportDefaultDeclaration") { 183 | let _node = (node as ExportDefaultDeclaration).declaration as 184 | | FunctionDeclaration 185 | | ArrowFunctionExpression; // TODO: consider other expressions 186 | exportDefaultNode.node = node as ExportDefaultDeclaration & ASTNode; 187 | if (_node.body.type === "BlockStatement") { 188 | let RSArg = (_node.body.body.find( 189 | (x) => x.type === "ReturnStatement" 190 | ) as ReturnStatement)?.argument as any; // TODO: this type is missing JSXElement 191 | if (RSArg.type === "JSXElement") { 192 | exportDefaultNode.pos_HeadOfDefault = (_node.body as any).start + 1; 193 | exportDefaultNode.lastChildofDefault = RSArg.children.slice(-1)[0]; 194 | hasExportDefault = true; 195 | // use start and end 196 | } else { 197 | throw new Error("not returning JSX in export default function"); // TODO: fix this? 198 | } 199 | } 200 | if (!hasExportDefault) { 201 | throw new Error( 202 | "a React SFC must export default a function component. None was detected. Pls file an issue if this is a mistake." 203 | ); 204 | } 205 | } 206 | // usestate 207 | if (node.type === "VariableDeclaration") { 208 | let dec = (node as VariableDeclaration) 209 | .declarations[0] as VariableDeclarator & { init: ASTNode }; 210 | if (dec.id.type === "Identifier" && dec.id.name.startsWith("_")) { 211 | stateMap.set(dec.id.name, { 212 | node, // for replacement 213 | value: ms.slice(dec.init.start, dec.init.end), // for use in templating 214 | }); 215 | } 216 | } 217 | 218 | // SETSTATE 219 | if (node.type === "AssignmentExpression") { 220 | // todo: maybe only read assignmentexpressions if the LHS is in the stateMap 221 | let LHS = (node as AssignmentExpression).left; 222 | if (LHS.type === "Identifier" && LHS.name.startsWith("_")) { 223 | assignmentsMap.set(LHS.name, { node }); 224 | } 225 | } 226 | if (node.type === "UpdateExpression") { 227 | // todo: maybe only read assignmentexpressions if the LHS is in the stateMap 228 | let ID = (node as UpdateExpression).argument as Identifier; 229 | if (ID.name.startsWith("_")) { 230 | assignmentsMap.set(ID.name, { node }); 231 | } 232 | } 233 | 234 | // BINDING 235 | if (node.type === "JSXAttribute") { 236 | // // bind:value syntax - we may want to use this in future? 237 | // let _node = node as JSXAttribute; 238 | // if ( 239 | // _node.name.type === "JSXNamespacedName" && 240 | // _node.name.namespace.name === "bind" 241 | // ) { 242 | // let RHSobject, RHSname; 243 | // // TODO: in future - support RHS which is just a Literal? MAAAYBE, maybe not 244 | // if (_node.value.type === "JSXExpressionContainer") { 245 | // if (_node.value.expression.type === "Identifier") { 246 | // // RHS is just an identifier 247 | // RHSname = _node.value.expression.name; 248 | // } else if (_node.value.expression.type === "MemberExpression") { 249 | // // RHS is an object access 250 | // let exp = _node.value.expression as MemberExpression & ASTNode; 251 | // RHSobject = { 252 | // objectName: 253 | // (exp.object as Identifier).name || 254 | // ((exp.object as MemberExpression).object as Identifier).name, // either its an identifier '$foo.bar` or a memberexpression `$foo.bar.baz` 255 | // fullAccessName: ms.slice(exp.start, exp.end), 256 | // }; 257 | // } else { 258 | // throw new Error( 259 | // "warning - unrecognized RHS expression type in binding: " + 260 | // _node.value.expression.type + 261 | // ". We will probably do this wrong, pls report this along with your code" 262 | // ); 263 | // } 264 | // } 265 | 266 | // bindValuesMap.set( 267 | // node, // to replace 268 | // { 269 | // // LHSname: _node.name.name.name.slice(1), // only tested to work for 'value'. remove the leading $ 270 | // LHSname: _node.name.name.name, // only tested to work for 'value'. remove the leading $ 271 | // RHSname, 272 | // RHSobject, 273 | // } 274 | // ); 275 | // } 276 | 277 | // $ prefix syntax 278 | let _node = node as JSXAttribute; 279 | if ( 280 | _node.name.type === "JSXIdentifier" && 281 | _node.name.name.startsWith("$") 282 | ) { 283 | let RHSobject, RHSname; 284 | // TODO: in future - support RHS which is just a Literal? MAAAYBE, maybe not 285 | if (_node.value.type === "JSXExpressionContainer") { 286 | if (_node.value.expression.type === "Identifier") { 287 | // RHS is just an identifier 288 | RHSname = _node.value.expression.name; 289 | } else if (_node.value.expression.type === "MemberExpression") { 290 | // RHS is an object access 291 | let exp = _node.value.expression as MemberExpression & ASTNode; 292 | RHSobject = { 293 | objectName: 294 | (exp.object as Identifier).name || 295 | ((exp.object as MemberExpression).object as Identifier).name, // either its an identifier '$foo.bar` or a memberexpression `$foo.bar.baz` 296 | fullAccessName: ms.slice(exp.start, exp.end), 297 | }; 298 | } else { 299 | throw new Error( 300 | "warning - unrecognized RHS expression type in binding: " + 301 | _node.value.expression.type + 302 | ". We will probably do this wrong, pls report this along with your code" 303 | ); 304 | } 305 | } 306 | 307 | bindValuesMap.set( 308 | node, // to replace 309 | { 310 | LHSname: _node.name.name.slice(1), // only tested to work for 'value'. remove the leading $ 311 | RHSname, 312 | RHSobject, 313 | } 314 | ); 315 | } 316 | } 317 | }, 318 | // leave(node) { 319 | // // if (node.scope) scope = scope.parent; 320 | // } 321 | }); 322 | 323 | // validations 324 | if (exportDefaultNode.pos_HeadOfDefault === undefined) return; 325 | 326 | /* 327 | 328 | // process it! 329 | 330 | */ 331 | if (!isReactImported) ms.prepend(`import React from 'react';`); 332 | // remove STYLE and insert style jsx 333 | if (STYLEDECLARATION && STYLECONTENT) { 334 | ms.remove(STYLEDECLARATION.start, STYLEDECLARATION.end); 335 | if (exportDefaultNode.lastChildofDefault) 336 | ms.appendRight( 337 | exportDefaultNode.lastChildofDefault.end, 338 | `` 339 | ); 340 | } 341 | 342 | // useState 343 | if (stateMap.size) { 344 | // for each state hook 345 | stateMap.forEach(({ node, value }, key) => { 346 | ms.remove(node.start, node.end); 347 | let newStr; 348 | if (userWantsUSWL) { 349 | // should be 'let' bc we want to mutate it 350 | newStr = `\nlet [${key}, set${key}] = use${key}_State(${value})`; 351 | // i would like to use only one instance, of useStateWithLabel 352 | // https://stackoverflow.com/questions/57659640/is-there-any-way-to-see-names-of-fields-in-react-multiple-state-with-react-dev 353 | // but currently devtools uses the NAME OF THE HOOK for state hooks 354 | // rather than useDebugValue. so we do a simple alias of the hook 355 | ms.append(` 356 | function use${key}_State(v) { 357 | const x = React.useState(v); 358 | ${ 359 | isProduction 360 | ? "return x;" 361 | : `React.useDebugValue('${key}: ' + x[0]); return x;` 362 | } 363 | }`); 364 | } else { 365 | // just plain useState 366 | // should be 'let' bc we want to mutate it 367 | newStr = `\nlet [${key}, set${key}] = React.useState(${value})`; 368 | } 369 | ms.appendRight(exportDefaultNode.pos_HeadOfDefault!, newStr); 370 | }); 371 | } 372 | 373 | // setState 374 | if (assignmentsMap.size) { 375 | assignmentsMap.forEach(({ node }, key) => { 376 | // strategy: use comma separator to turn 377 | // $count = $count + 1 378 | // into 379 | // ($count = $count + 1, set$count($count)) 380 | ms.prependLeft(node.start, "("); 381 | ms.appendRight(node.end, `, set${key}(${key}))`); 382 | }); 383 | } 384 | 385 | // binding 386 | if (bindValuesMap.size) { 387 | bindValuesMap.forEach(({ LHSname, RHSname, RHSobject }, node) => { 388 | if (RHSobject) { 389 | // create new object, mutate new object, THEN set it 390 | // must be new object or react doesnt rerender 391 | ms.overwrite( 392 | node.start, 393 | node.end, 394 | `${LHSname}={${RHSobject.fullAccessName}} 395 | onChange={e => { 396 | let temp = Object.assign({}, ${RHSobject.objectName}); 397 | temp${RHSobject.fullAccessName.slice( 398 | RHSobject.objectName.length 399 | )} = e.target.${LHSname}; 400 | set${RHSobject.objectName}(temp); 401 | }}` 402 | ); 403 | } else if (RHSname) { 404 | ms.overwrite( 405 | node.start, 406 | node.end, 407 | `${LHSname}={${RHSname}} onChange={e => set${RHSname}(e.target.${LHSname})}` 408 | ); 409 | } else { 410 | throw new Error("we should not get here. pls repurt this binding bug"); 411 | } 412 | }); 413 | } 414 | 415 | // adjust for displayName 416 | if (options.componentDisplayName && exportDefaultNode.node) { 417 | const componentDeclarationNode: any = exportDefaultNode.node.declaration; 418 | ms.overwrite( 419 | exportDefaultNode.node.start, 420 | exportDefaultNode.node.end, 421 | `const $$Component = ${ms.slice( 422 | componentDeclarationNode.start, 423 | componentDeclarationNode.end 424 | )}; 425 | $$Component.displayName = "${options.componentDisplayName}" 426 | export default $$Component 427 | ` 428 | ); 429 | } 430 | 431 | let code = ms.toString(); 432 | return { 433 | js: { 434 | code, 435 | map: null, // todo 436 | }, 437 | css: { 438 | // TODO 439 | // TODO 440 | // TODO 441 | // TODO 442 | // TODO 443 | // TODO 444 | // TODO 445 | // static css 446 | // THIS WHOLE THING IS A MASSIVE TODO 447 | code: STYLESTATICCSS, 448 | map: null, 449 | // TODO 450 | // TODO 451 | // TODO 452 | // TODO 453 | // TODO 454 | // TODO 455 | // TODO 456 | // TODO 457 | }, 458 | }; 459 | } 460 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | note to reader - these tests arent setup at all yet 2 | 3 | just stubs/holdovers from prev attempts. 4 | 5 | tbd... -------------------------------------------------------------------------------- /tests/__snapshots__/snapshots.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`passes binding-nested-obj snapshot test 1`] = ` 4 | Object { 5 | "css": Object { 6 | "code": undefined, 7 | "map": null, 8 | }, 9 | "js": Object { 10 | "code": "import * as React from 'react' 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | export default () => { 19 | let [_count, set_count] = use_count_State(23) 20 | let [_text, set_text] = use_text_State({ 21 | foo: 1, 22 | moo: { 23 | djs: 3 24 | } 25 | }) 26 | React.useEffect(() => console.log('rerendered')) // no need for React import 27 | return ( 28 |
29 | Some Text 30 | { 32 | let temp = Object.assign({}, _text); 33 | temp.moo.djs = e.target.value; 34 | set_text(temp); 35 | }} /> 36 | (++_count, set_count(_count))}> 37 | Count {_count} 38 | 39 |
44 | ) 45 | } 46 | 47 | 48 | // I can define inline Components like normal 49 | function MyButton({onClick}) { 50 | return 51 | } 52 | function use_count_State(v) { 53 | const x = React.useState(v); 54 | React.useDebugValue('_count: ' + x[0]); return x; 55 | } 56 | function use_text_State(v) { 57 | const x = React.useState(v); 58 | React.useDebugValue('_text: ' + x[0]); return x; 59 | }", 60 | "map": null, 61 | }, 62 | } 63 | `; 64 | 65 | exports[`passes options-displayName snapshot test 1`] = ` 66 | Object { 67 | "css": Object { 68 | "code": undefined, 69 | "map": null, 70 | }, 71 | "js": Object { 72 | "code": "import React from 'react';const $$Component = () => { 73 | return
div
74 | }; 75 | $$Component.displayName = \\"MySFC\\" 76 | export default $$Component 77 | ", 78 | "map": null, 79 | }, 80 | } 81 | `; 82 | 83 | exports[`passes state-simple snapshot test 1`] = ` 84 | Object { 85 | "css": Object { 86 | "code": undefined, 87 | "map": null, 88 | }, 89 | "js": Object { 90 | "code": "import React from 'react'; 91 | 92 | 93 | 94 | export default () => { 95 | let [_count, set_count] = use_count_State(0) 96 | return 102 | } 103 | function use_count_State(v) { 104 | const x = React.useState(v); 105 | React.useDebugValue('_count: ' + x[0]); return x; 106 | }", 107 | "map": null, 108 | }, 109 | } 110 | `; 111 | 112 | exports[`passes styles-static snapshot test 1`] = ` 113 | Object { 114 | "css": Object { 115 | "code": " 116 | div { 117 | // scoped by default 118 | background-color: papayawhip; 119 | .Button { 120 | border-color: cadetblue; 121 | } 122 | } 123 | ", 124 | "map": null, 125 | }, 126 | "js": Object { 127 | "code": "import React from 'react'; 128 | 129 | export default ({ onClick }) => { 130 | React.useEffect(() => console.log(\\"rerendered\\")); // no need for React import 131 | return ( 132 |
133 | Some Text 134 | My Call To Action 135 |
144 | ); 145 | }; 146 | 147 | // I can define inline Components like normal 148 | function MyButton({onClick}) { 149 | return 150 | }", 151 | "map": null, 152 | }, 153 | } 154 | `; 155 | 156 | exports[`passes temp snapshot test 1`] = ` 157 | Object { 158 | "css": Object { 159 | "code": undefined, 160 | "map": null, 161 | }, 162 | "js": Object { 163 | "code": "import * as React from \\"react\\"; 164 | 165 | 166 | 167 | 168 | 169 | export default () => { 170 | let [_count, set_count] = use_count_State(23) 171 | React.useEffect(() => console.log(\\"rerendered\\")); // no need for React import 172 | return ( 173 |
174 | Some Text 175 | {/* */} 176 | (++_count, set_count(_count))}>Count {_count} 177 |
182 | ); 183 | }; 184 | 185 | // I can define inline Components like normal 186 | function MyButton({ onClick }) { 187 | return ( 188 | 191 | ); 192 | } 193 | 194 | function use_count_State(v) { 195 | const x = React.useState(v); 196 | React.useDebugValue('_count: ' + x[0]); return x; 197 | }", 198 | "map": null, 199 | }, 200 | } 201 | `; 202 | -------------------------------------------------------------------------------- /tests/binding-nested-obj/src.react: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | 3 | let _count = 23 4 | let _text = { 5 | foo: 1, 6 | moo: { 7 | djs: 3 8 | } 9 | } 10 | 11 | export const STYLE = ` 12 | div { 13 | color: ${_count > 5 ? 'blue' : 'green'}; 14 | } 15 | ` 16 | 17 | 18 | export default () => { 19 | useEffect(() => console.log('rerendered')) // no need for React import 20 | return ( 21 |
22 | Some Text 23 | 24 | ++_count}> 25 | Count {_count} 26 | 27 |
28 | ) 29 | } 30 | 31 | 32 | // I can define inline Components like normal 33 | function MyButton({onClick}) { 34 | return 35 | } -------------------------------------------------------------------------------- /tests/options-displayName/options.json: -------------------------------------------------------------------------------- 1 | { 2 | "componentDisplayName": "MySFC" 3 | } -------------------------------------------------------------------------------- /tests/options-displayName/src.react: -------------------------------------------------------------------------------- 1 | export default () => { 2 | return
div
3 | } -------------------------------------------------------------------------------- /tests/snapshots.test.ts: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | // import path from "path"; 3 | import { Compiler } from "../src/compiler"; 4 | fs.readdirSync("tests") 5 | .filter((p) => fs.lstatSync("tests/" + p).isDirectory()) 6 | .filter(p => !p.startsWith('__snapshots__')) 7 | .forEach((p) => { 8 | // console.log({ p }); 9 | it(`passes ${p} snapshot test`, () => { 10 | const src = fs.readFileSync("tests/" + p + "/src.react", "utf8"); 11 | let options 12 | if (fs.existsSync(`tests/${p}/options.json`)) { 13 | options = JSON.parse(fs.readFileSync(`tests/${p}/options.json`, 'utf8')) 14 | console.log({options}) 15 | } 16 | const output = Compiler({ code: src, options }); 17 | expect(output).toMatchSnapshot(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /tests/state-simple/src.react: -------------------------------------------------------------------------------- 1 | let _count = 0 2 | 3 | export const STYLE = ` 4 | button { 5 | // scoped by default 6 | background-color: ${_count > 5 ? 'red' : 'papayawhip'}; 7 | } 8 | ` 9 | 10 | export default () => { 11 | return 12 | } -------------------------------------------------------------------------------- /tests/styles-static/src.react: -------------------------------------------------------------------------------- 1 | export const STYLE = ` 2 | div { 3 | // scoped by default 4 | background-color: papayawhip; 5 | .Button { 6 | border-color: cadetblue; 7 | } 8 | } 9 | `; 10 | 11 | export default ({ onClick }) => { 12 | useEffect(() => console.log("rerendered")); // no need for React import 13 | return ( 14 |
15 | Some Text 16 | My Call To Action 17 |
18 | ); 19 | }; 20 | 21 | // I can define inline Components like normal 22 | function MyButton({onClick}) { 23 | return 24 | } -------------------------------------------------------------------------------- /tests/temp/src.react: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | let _count = 23; 4 | 5 | export const STYLE = ` 6 | div { 7 | color: ${_count > 5 ? "blue" : "green"} 8 | } 9 | `; 10 | 11 | export default () => { 12 | useEffect(() => console.log("rerendered")); // no need for React import 13 | return ( 14 |
15 | Some Text 16 | {/* */} 17 | ++_count}>Count {_count} 18 |
19 | ); 20 | }; 21 | 22 | // I can define inline Components like normal 23 | function MyButton({ onClick }) { 24 | return ( 25 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es2015", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "./dist", /* Redirect output structure to the directory. */ 18 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | 43 | /* Module Resolution Options */ 44 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 45 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 46 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 47 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 48 | // "typeRoots": [], /* List of folders to include type definitions from. */ 49 | // "types": [], /* Type declaration files to be included in compilation. */ 50 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 51 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 52 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 53 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 54 | 55 | /* Source Map Options */ 56 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 59 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 60 | 61 | /* Experimental Options */ 62 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 63 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 64 | 65 | /* Advanced Options */ 66 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 67 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 68 | }, 69 | "exclude": [ 70 | "tests", 71 | "dist" 72 | ] 73 | } 74 | --------------------------------------------------------------------------------