├── .editorconfig ├── .env ├── .eslintrc.json ├── .github └── workflows │ └── check-update.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── dprint.json ├── env.d.ts ├── next-env.d.ts ├── package.json ├── pnpm-lock.yaml ├── public └── swc.svg ├── scripts └── check-swc-update.mts ├── src ├── components │ ├── CompressOptionsModal.tsx │ ├── ConfigEditorModal.tsx │ ├── Configuration.tsx │ ├── HeaderBar.tsx │ ├── InputEditor.tsx │ ├── MangleOptionsModal.tsx │ ├── OutputEditor.tsx │ ├── VersionSelect.tsx │ └── Workspace.tsx ├── pages │ ├── _app.tsx │ └── index.tsx ├── state.ts ├── swc.ts └── utils.ts ├── tsconfig.json └── vercel.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_SWC_VERSION=1.11.29 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next", 3 | "root": true 4 | } 5 | -------------------------------------------------------------------------------- /.github/workflows/check-update.yml: -------------------------------------------------------------------------------- 1 | name: Check SWC Update 2 | 3 | on: 4 | workflow_dispatch: {} 5 | schedule: 6 | - cron: '0 0 * * *' 7 | 8 | jobs: 9 | check-update: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | - name: Setup pnpm 15 | uses: pnpm/action-setup@v4.0.0 16 | with: 17 | version: latest 18 | run_install: true 19 | - name: Run checker script 20 | run: pnpm tsx scripts/check-swc-update.mts 21 | - name: Commit update 22 | uses: stefanzweifel/git-auto-commit-action@v4 23 | with: 24 | commit_message: upgrade SWC 25 | file_pattern: .env 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .next 3 | *.tsbuildinfo 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021-present Pig Fang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🎲 swc playground 2 | 3 | The playground of swc. Visit [https://play.swc.rs/](https://play.swc.rs/) to try it out. 4 | 5 | ## ✨ Features 6 | 7 | - Two editors powered by [Monaco Editor](https://github.com/microsoft/monaco-editor) for editing input code and showing output code. 8 | - Configure swc with a friendly form. 9 | - Share code and config by URL. 10 | - Choose different versions of swc with an option. 11 | - Configure swc by editing JSON manually with IntelliSense. 12 | - Show AST of input code. 13 | - Dark mode. 14 | 15 | ## 🙌 Related 16 | 17 | - [SWC CSS Playground](http://github.com/g-plane/swc-css-playground) - Playground for SWC CSS. 18 | 19 | ## 📜 License 20 | 21 | MIT License 22 | 23 | Copyright (c) 2021-present [Pig Fang](https://github.com/g-plane) 24 | -------------------------------------------------------------------------------- /dprint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "https://dprint.gplane.win/2024-01.json", 3 | "excludes": [".next"] 4 | } 5 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | namespace NodeJS { 2 | interface ProcessEnv { 3 | NEXT_PUBLIC_SWC_VERSION: string 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swc-playground", 3 | "version": "0.0.0", 4 | "description": "swc playground", 5 | "private": true, 6 | "scripts": { 7 | "start": "next start", 8 | "build": "next build", 9 | "dev": "next dev", 10 | "lint": "tsc -p . && eslint --ext=ts,tsx src", 11 | "fmt": "dprint fmt" 12 | }, 13 | "author": "Pig Fang ", 14 | "license": "MIT", 15 | "dependencies": { 16 | "@chakra-ui/react": "^2.8.1", 17 | "@emotion/react": "^11.11.1", 18 | "@emotion/styled": "^11.11.0", 19 | "@monaco-editor/react": "^4.6.0", 20 | "eslint-config-next": "^13.5.5", 21 | "framer-motion": "^10.16.4", 22 | "jotai": "^2.4.3", 23 | "js-base64": "^3.7.5", 24 | "jsonc-parser": "^3.2.0", 25 | "monaco-editor": "^0.44.0", 26 | "next": "^13.5.5", 27 | "pako": "^2.1.0", 28 | "react": "^18.2.0", 29 | "react-dom": "^18.2.0", 30 | "react-icons": "^4.11.0", 31 | "semver": "^7.5.4", 32 | "strip-ansi": "^7.1.0", 33 | "swr": "^2.2.4", 34 | "ts-results": "^3.3.0" 35 | }, 36 | "devDependencies": { 37 | "@gplane/tsconfig": "^6.1.0", 38 | "@swc/wasm-typescript-esm": "^1.7.1", 39 | "@types/node": "^20.8.6", 40 | "@types/pako": "^2.0.1", 41 | "@types/react": "^18.2.28", 42 | "@types/react-dom": "^18.2.13", 43 | "@types/semver": "^7.5.3", 44 | "dprint": "^0.45.0", 45 | "eslint": "^8.51.0", 46 | "tsx": "^4.10.4", 47 | "typescript": "^5.2.2", 48 | "undici": "^5.26.3" 49 | }, 50 | "pnpm": { 51 | "peerDependencyRules": { 52 | "ignoreMissing": [ 53 | "@babel/core" 54 | ] 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /public/swc.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /scripts/check-swc-update.mts: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs/promises' 2 | import { fetch } from 'undici' 3 | 4 | const response = await fetch( 5 | 'https://data.jsdelivr.com/v1/package/npm/@swc/wasm-web' 6 | ) 7 | const { 8 | tags: { latest }, 9 | } = (await response.json()) as { tags: { latest: string } } 10 | 11 | const envFile = await fs.readFile('.env', 'utf8') 12 | const current = /^NEXT_PUBLIC_SWC_VERSION=(?\S+)\s*$/.exec(envFile) 13 | ?.groups 14 | ?.current 15 | if (!current) { 16 | throw new Error(`NEXT_PUBLIC_SWC_VERSION not found in .env:\n\n${envFile}`) 17 | } 18 | if (current !== latest) { 19 | await fs.writeFile( 20 | '.env', 21 | envFile.replace( 22 | `NEXT_PUBLIC_SWC_VERSION=${current}`, 23 | `NEXT_PUBLIC_SWC_VERSION=${latest}` 24 | ) 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /src/components/CompressOptionsModal.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Checkbox, 4 | FormControl, 5 | FormLabel, 6 | Grid, 7 | Modal, 8 | ModalBody, 9 | ModalCloseButton, 10 | ModalContent, 11 | ModalFooter, 12 | ModalHeader, 13 | ModalOverlay, 14 | NumberDecrementStepper, 15 | NumberIncrementStepper, 16 | NumberInput, 17 | NumberInputField, 18 | NumberInputStepper, 19 | Text, 20 | useDisclosure, 21 | } from '@chakra-ui/react' 22 | import { useAtom } from 'jotai' 23 | import { applyEdits, modify } from 'jsonc-parser' 24 | import { useState } from 'react' 25 | import type { ChangeEvent } from 'react' 26 | import { parsedSwcConfigAtom, swcConfigAtom } from '../state' 27 | import type { CompressOptions } from '../swc' 28 | import { JSONC_FORMATTING_OPTIONS } from '../utils' 29 | 30 | export default function CompressOptionsModal() { 31 | const [, setSwcConfig] = useAtom(swcConfigAtom) 32 | const [parsedSwcConfig] = useAtom(parsedSwcConfigAtom) 33 | const [options, setOptions] = useState( 34 | parsedSwcConfig.jsc?.minify?.compress 35 | ) 36 | const { isOpen, onOpen, onClose } = useDisclosure() 37 | 38 | const handleApply = () => { 39 | setSwcConfig((config) => 40 | applyEdits( 41 | config, 42 | modify(config, ['jsc', 'minify', 'compress'], options, { 43 | formattingOptions: JSONC_FORMATTING_OPTIONS, 44 | }) 45 | ) 46 | ) 47 | onClose() 48 | } 49 | 50 | const handleOpen = () => { 51 | setOptions(parsedSwcConfig.jsc?.minify?.compress) 52 | onOpen() 53 | } 54 | 55 | const handleClose = () => { 56 | setOptions(parsedSwcConfig.jsc?.minify?.compress) 57 | onClose() 58 | } 59 | 60 | if (!options) { 61 | return null 62 | } 63 | 64 | const handleOptionChange = ( 65 | key: keyof CompressOptions, 66 | event: ChangeEvent, 67 | ) => { 68 | setOptions((options) => 69 | options && typeof options === 'object' 70 | ? { ...options, [key]: event.target.checked } 71 | : options 72 | ) 73 | } 74 | 75 | const handlePassesChange = (value: number) => { 76 | setOptions((options) => 77 | options && typeof options === 'object' 78 | ? { ...options, passes: value } 79 | : options 80 | ) 81 | } 82 | 83 | return ( 84 | <> 85 | 88 | 95 | 96 | 97 | Compress Options 98 | 99 | 100 | 101 | 102 | Not all options are shown here. You can also configure by closing this dialog then 103 | clicking the "Edit as JSON" button. 104 | 105 | 110 | {Object.entries(options) 111 | .filter(([, value]) => typeof value === 'boolean') 112 | .map(([key, value]) => ( 113 | handleOptionChange(key as keyof CompressOptions, event)} 117 | > 118 | {key} 119 | 120 | ))} 121 | 122 | {options && typeof options === 'object' && ( 123 | 124 | Passes 125 | handlePassesChange(value)} 130 | > 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | )} 139 | 140 | 141 | 142 | 145 | 146 | 147 | 148 | 149 | 150 | ) 151 | } 152 | -------------------------------------------------------------------------------- /src/components/ConfigEditorModal.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Code, 4 | Modal, 5 | ModalBody, 6 | ModalCloseButton, 7 | ModalContent, 8 | ModalFooter, 9 | ModalHeader, 10 | ModalOverlay, 11 | Text, 12 | useDisclosure, 13 | useToast, 14 | } from '@chakra-ui/react' 15 | import Editor, { useMonaco } from '@monaco-editor/react' 16 | import { useAtom } from 'jotai' 17 | import type { editor } from 'monaco-editor' 18 | import { useEffect, useState } from 'react' 19 | import { swcConfigAtom } from '../state' 20 | import { editorOptions as sharedEditorOptions, useMonacoThemeValue } from '../utils' 21 | 22 | const editorOptions: editor.IEditorConstructionOptions = { 23 | ...sharedEditorOptions, 24 | scrollBeyondLastLine: false, 25 | } 26 | 27 | export default function ConfigEditorModal() { 28 | const [swcConfig, setSwcConfig] = useAtom(swcConfigAtom) 29 | const [editingConfig, setEditingConfig] = useState(swcConfig) 30 | const monacoTheme = useMonacoThemeValue() 31 | const monaco = useMonaco() 32 | const { isOpen, onOpen, onClose } = useDisclosure() 33 | const toast = useToast() 34 | 35 | useEffect(() => { 36 | if (!monaco) { 37 | return 38 | } 39 | 40 | monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ 41 | allowComments: true, 42 | trailingCommas: 'ignore', 43 | enableSchemaRequest: true, 44 | schemas: [ 45 | { 46 | uri: 'https://swc.rs/schema.json', 47 | fileMatch: ['.swcrc'], 48 | }, 49 | ], 50 | }) 51 | }, [monaco]) 52 | 53 | const handleOpen = () => { 54 | setEditingConfig(swcConfig) 55 | onOpen() 56 | } 57 | 58 | const handleClose = () => { 59 | setEditingConfig(swcConfig) 60 | onClose() 61 | } 62 | 63 | const handleApply = () => { 64 | try { 65 | setSwcConfig(editingConfig) 66 | onClose() 67 | } catch (error) { 68 | toast({ 69 | title: 'Error', 70 | description: String(error), 71 | status: 'error', 72 | duration: 5000, 73 | position: 'top', 74 | isClosable: true, 75 | }) 76 | } 77 | } 78 | 79 | const handleEditorChange = (value: string | undefined) => { 80 | if (value != null) { 81 | setEditingConfig(value) 82 | } 83 | } 84 | 85 | return ( 86 | <> 87 | 90 | 91 | 92 | 93 | 94 | SWC Configuration (.swcrc) 95 | 96 | 97 | 98 | 99 | 100 | You can paste your config here, or just manually type directly. 101 | 102 | 111 | 112 | 113 | 114 | 117 | 118 | 119 | 120 | 121 | 122 | ) 123 | } 124 | -------------------------------------------------------------------------------- /src/components/Configuration.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Flex, 3 | FormControl, 4 | FormLabel, 5 | Heading, 6 | Input, 7 | Select, 8 | Switch, 9 | VStack, 10 | } from '@chakra-ui/react' 11 | import { useAtom } from 'jotai' 12 | import { Base64 } from 'js-base64' 13 | import { applyEdits, format, modify } from 'jsonc-parser' 14 | import { ungzip } from 'pako' 15 | import { useEffect } from 'react' 16 | import type * as React from 'react' 17 | import semver from 'semver' 18 | import { 19 | defaultCompressOptions, 20 | defaultEnvOptions, 21 | defaultMangleOptions, 22 | parsedSwcConfigAtom, 23 | swcConfigAtom, 24 | } from '../state' 25 | import { type ParserOptions } from '../swc' 26 | import { JSONC_FORMATTING_OPTIONS, useBgColor, useBorderColor } from '../utils' 27 | import CompressOptionsModal from './CompressOptionsModal' 28 | import ConfigEditorModal from './ConfigEditorModal' 29 | import MangleOptionsModal from './MangleOptionsModal' 30 | 31 | const STORAGE_KEY = 'v1.config' 32 | 33 | interface Props { 34 | swcVersion: string 35 | stripTypes: boolean 36 | onStripTypesChange(value: boolean): void 37 | } 38 | 39 | export default function Configuration(props: Props) { 40 | const [swcConfig, setSwcConfig] = useAtom(swcConfigAtom) 41 | const [parsedSwcConfig] = useAtom(parsedSwcConfigAtom) 42 | const bg = useBgColor() 43 | const borderColor = useBorderColor() 44 | 45 | useEffect(() => { 46 | const url = new URL(location.href) 47 | const encodedConfig = url.searchParams.get('config') 48 | const storedConfig = localStorage.getItem(STORAGE_KEY) 49 | const configJSON = encodedConfig 50 | ? ungzip(Base64.toUint8Array(encodedConfig), { to: 'string' }) 51 | : storedConfig 52 | if (!configJSON) { 53 | return 54 | } 55 | if (configJSON.startsWith('{"')) { 56 | // pretty format JSON 57 | setSwcConfig( 58 | applyEdits( 59 | configJSON, 60 | format(configJSON, undefined, JSONC_FORMATTING_OPTIONS) 61 | ) 62 | ) 63 | } else { 64 | setSwcConfig(configJSON) 65 | } 66 | }, [setSwcConfig]) 67 | 68 | useEffect(() => { 69 | localStorage.setItem(STORAGE_KEY, swcConfig) 70 | }, [swcConfig]) 71 | 72 | const handleLanguageChange = ( 73 | event: React.ChangeEvent, 74 | ) => { 75 | setSwcConfig((config) => { 76 | const jsxOrTsx = parsedSwcConfig.jsc.parser.syntax === 'typescript' 77 | ? parsedSwcConfig.jsc.parser.tsx 78 | : parsedSwcConfig.jsc.parser.jsx 79 | const parserOptions: ParserOptions = event.target.value === 'typescript' 80 | ? { syntax: 'typescript', tsx: jsxOrTsx } 81 | : { syntax: 'ecmascript', jsx: jsxOrTsx } 82 | 83 | return applyEdits( 84 | config, 85 | modify(config, ['jsc', 'parser'], parserOptions, { 86 | formattingOptions: JSONC_FORMATTING_OPTIONS, 87 | }) 88 | ) 89 | }) 90 | } 91 | 92 | const handleTargetChange = (event: React.ChangeEvent) => { 93 | setSwcConfig((config) => 94 | applyEdits( 95 | config, 96 | modify(config, ['jsc', 'target'], event.target.value, { 97 | formattingOptions: JSONC_FORMATTING_OPTIONS, 98 | }) 99 | ) 100 | ) 101 | } 102 | 103 | const handleModuleChange = (event: React.ChangeEvent) => { 104 | setSwcConfig((config) => 105 | applyEdits( 106 | config, 107 | modify( 108 | config, 109 | ['module'], 110 | { type: event.target.value }, 111 | { 112 | formattingOptions: JSONC_FORMATTING_OPTIONS, 113 | } 114 | ) 115 | ) 116 | ) 117 | } 118 | 119 | const handleSourceTypeChange = ({ 120 | target: { value }, 121 | }: React.ChangeEvent) => { 122 | const isModule = (() => { 123 | switch (value) { 124 | case 'module': 125 | return true 126 | case 'script': 127 | return false 128 | default: 129 | return 'unknown' 130 | } 131 | })() 132 | setSwcConfig((config) => 133 | applyEdits( 134 | config, 135 | modify(config, ['isModule'], isModule, { 136 | formattingOptions: JSONC_FORMATTING_OPTIONS, 137 | }) 138 | ) 139 | ) 140 | } 141 | 142 | const handleToggleJSX = (event: React.ChangeEvent) => { 143 | setSwcConfig((config) => 144 | applyEdits( 145 | config, 146 | modify(config, ['jsc', 'parser', 'jsx'], event.target.checked, { 147 | formattingOptions: JSONC_FORMATTING_OPTIONS, 148 | }) 149 | ) 150 | ) 151 | } 152 | 153 | const handleToggleTSX = (event: React.ChangeEvent) => { 154 | setSwcConfig((config) => 155 | applyEdits( 156 | config, 157 | modify(config, ['jsc', 'parser', 'tsx'], event.target.checked, { 158 | formattingOptions: JSONC_FORMATTING_OPTIONS, 159 | }) 160 | ) 161 | ) 162 | } 163 | 164 | const handleToggleMinify = (event: React.ChangeEvent) => { 165 | setSwcConfig((config) => 166 | applyEdits( 167 | config, 168 | modify(config, ['minify'], event.target.checked, { 169 | formattingOptions: JSONC_FORMATTING_OPTIONS, 170 | }) 171 | ) 172 | ) 173 | } 174 | 175 | const handleToggleCompress = (event: React.ChangeEvent) => { 176 | const options = event.target.checked ? defaultCompressOptions : false 177 | setSwcConfig((config) => 178 | applyEdits( 179 | config, 180 | modify(config, ['jsc', 'minify', 'compress'], options, { 181 | formattingOptions: JSONC_FORMATTING_OPTIONS, 182 | }) 183 | ) 184 | ) 185 | } 186 | 187 | const handleToggleMangle = (event: React.ChangeEvent) => { 188 | const options = event.target.checked ? defaultMangleOptions : false 189 | setSwcConfig((config) => 190 | applyEdits( 191 | config, 192 | modify(config, ['jsc', 'minify', 'mangle'], options, { 193 | formattingOptions: JSONC_FORMATTING_OPTIONS, 194 | }) 195 | ) 196 | ) 197 | } 198 | 199 | const handleToggleLoose = (event: React.ChangeEvent) => { 200 | setSwcConfig((config) => 201 | applyEdits( 202 | config, 203 | modify(config, ['jsc', 'loose'], event.target.checked, { 204 | formattingOptions: JSONC_FORMATTING_OPTIONS, 205 | }) 206 | ) 207 | ) 208 | } 209 | 210 | const handleToggleEnvTargets = ( 211 | event: React.ChangeEvent, 212 | ) => { 213 | const options = event.target.checked ? defaultEnvOptions : undefined 214 | setSwcConfig((config) => 215 | applyEdits( 216 | config, 217 | [ 218 | ...modify(config, ['env'], options, { 219 | formattingOptions: JSONC_FORMATTING_OPTIONS, 220 | }), 221 | ...modify( 222 | config, 223 | ['jsc', 'target'], 224 | event.target.checked ? undefined : 'es5', 225 | { 226 | formattingOptions: JSONC_FORMATTING_OPTIONS, 227 | getInsertionIndex: (properties) => properties.indexOf('parser') + 1, 228 | } 229 | ), 230 | ] 231 | ) 232 | ) 233 | } 234 | 235 | const handleEnvTargetsChange = ( 236 | event: React.ChangeEvent, 237 | ) => { 238 | setSwcConfig((config) => 239 | applyEdits( 240 | config, 241 | modify(config, ['env', 'targets'], event.target.value, { 242 | formattingOptions: JSONC_FORMATTING_OPTIONS, 243 | }) 244 | ) 245 | ) 246 | } 247 | 248 | const handleToggleEnvBugfixes = ( 249 | event: React.ChangeEvent, 250 | ) => { 251 | setSwcConfig((config) => 252 | applyEdits( 253 | config, 254 | modify( 255 | config, 256 | ['env', 'bugfixes'], 257 | event.target.checked ? true : undefined, 258 | { formattingOptions: JSONC_FORMATTING_OPTIONS } 259 | ) 260 | ) 261 | ) 262 | } 263 | 264 | const handleToggleIsolatedDts = ( 265 | event: React.ChangeEvent, 266 | ) => { 267 | setSwcConfig((config) => 268 | applyEdits( 269 | config, 270 | modify( 271 | config, 272 | ['jsc', 'experimental', 'emitIsolatedDts'], 273 | !!event.target.checked, 274 | { formattingOptions: JSONC_FORMATTING_OPTIONS } 275 | ) 276 | ) 277 | ) 278 | } 279 | 280 | return ( 281 | 282 | 283 | Configuration 284 | 285 | 292 | 293 | 294 | Language 295 | 303 | 304 | 305 | Target 306 | 324 | 325 | 326 | Module 327 | 338 | 339 | 340 | Source Type 341 | 354 | 355 | {parsedSwcConfig.jsc.parser.syntax === 'ecmascript' 356 | ? ( 357 | 358 | 363 | 364 | JSX 365 | 366 | 367 | ) 368 | : ( 369 | 370 | 375 | 376 | TSX 377 | 378 | 379 | )} 380 | 381 | 386 | 387 | Loose 388 | 389 | 390 | 391 | 396 | 397 | Minify 398 | 399 | 400 | 401 | 406 | 407 | Compress 408 | 409 | {parsedSwcConfig.jsc?.minify?.compress && } 410 | 411 | 412 | 417 | 418 | Mangle 419 | 420 | {parsedSwcConfig.jsc?.minify?.mangle && } 421 | 422 | 423 | 428 | 429 | Env Targets 430 | 431 | 432 | {typeof parsedSwcConfig.env?.targets === 'string' && ( 433 | <> 434 | 435 | 441 | 442 | 443 | 448 | 449 | Bugfixes 450 | 451 | 452 | 453 | )} 454 | 455 | props.onStripTypesChange(event.target.checked)} 459 | disabled={semver.lt(props.swcVersion, '1.7.1')} 460 | /> 461 | 462 | Strip Types Only 463 | 464 | 465 | 466 | ) 469 | ?.emitIsolatedDts} 470 | onChange={handleToggleIsolatedDts} 471 | disabled={semver.lt(props.swcVersion, '1.10.0') || 472 | parsedSwcConfig.jsc?.parser?.syntax !== 'typescript'} 473 | /> 474 | 475 | Emit Isolated .d.ts 476 | 477 | 478 | 479 | 480 | 481 | 482 | ) 483 | } 484 | -------------------------------------------------------------------------------- /src/components/HeaderBar.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Button, Flex, HStack, Link, useColorMode, useColorModeValue } from '@chakra-ui/react' 2 | import Image from 'next/image' 3 | import { useEffect } from 'react' 4 | import { CgExternal, CgMoon, CgSun } from 'react-icons/cg' 5 | 6 | export default function HeaderBar() { 7 | const { colorMode, toggleColorMode, setColorMode } = useColorMode() 8 | const bg = useColorModeValue('gray.100', 'gray.900') 9 | const borderColor = useColorModeValue('gray.300', 'gray.700') 10 | 11 | useEffect(() => { 12 | const query = window.matchMedia?.('(prefers-color-scheme: dark)') 13 | if (query?.matches) { 14 | setColorMode('dark') 15 | } 16 | 17 | const listener = (event: MediaQueryListEvent) => { 18 | setColorMode(event.matches ? 'dark' : 'light') 19 | } 20 | 21 | query?.addEventListener('change', listener) 22 | 23 | return () => query?.removeEventListener('change', listener) 24 | }, [setColorMode]) 25 | 26 | return ( 27 | 37 | 38 | swc 39 | 40 | 41 | 44 | 50 | GitHub 51 | 52 | 53 | 54 | 55 | 56 | 57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /src/components/InputEditor.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Button, Flex, HStack, Heading } from '@chakra-ui/react' 2 | import Editor, { useMonaco } from '@monaco-editor/react' 3 | import { useAtom } from 'jotai' 4 | import type { editor } from 'monaco-editor' 5 | import { useEffect, useRef } from 'react' 6 | import { CgFileDocument, CgShare } from 'react-icons/cg' 7 | import { parsedSwcConfigAtom } from '../state' 8 | import { editorOptions, parseSWCError, useBorderColor, useMonacoThemeValue } from '../utils' 9 | 10 | interface Props { 11 | code: string 12 | error: string | null 13 | onCodeChange(code: string): void 14 | onReportIssue(): void 15 | onShare(): void 16 | } 17 | 18 | export default function InputEditor(props: Props) { 19 | const [parsedSwcConfig] = useAtom(parsedSwcConfigAtom) 20 | const monacoTheme = useMonacoThemeValue() 21 | const borderColor = useBorderColor() 22 | const monaco = useMonaco() 23 | const editorRef = useRef(null) 24 | 25 | useEffect(() => { 26 | monaco?.languages.typescript.typescriptDefaults.setDiagnosticsOptions({ 27 | noSyntaxValidation: true, 28 | noSemanticValidation: true, 29 | noSuggestionDiagnostics: true, 30 | }) 31 | }, [monaco]) 32 | 33 | useEffect(() => { 34 | const model = editorRef.current?.getModel() 35 | if (!monaco || !model) { 36 | return 37 | } 38 | 39 | if (props.error) { 40 | const markers = Array.from(parseSWCError(props.error)).map( 41 | ([_, message, line, col]): editor.IMarkerData => { 42 | const lineNumber = Number.parseInt(line!), 43 | column = Number.parseInt(col!) 44 | 45 | return { 46 | source: 'swc', 47 | message: message!, 48 | severity: monaco.MarkerSeverity.Error, 49 | startLineNumber: lineNumber, 50 | startColumn: column, 51 | endLineNumber: lineNumber, 52 | endColumn: column, 53 | } 54 | } 55 | ) 56 | monaco.editor.setModelMarkers(model, 'swc', markers) 57 | } 58 | 59 | return () => monaco.editor.setModelMarkers(model, 'swc', []) 60 | }, [props.error, monaco]) 61 | 62 | const handleEditorDidMount = (instance: editor.IStandaloneCodeEditor) => { 63 | editorRef.current = instance 64 | } 65 | 66 | const handleEditorChange = (value: string | undefined) => { 67 | if (value != null) { 68 | props.onCodeChange(value) 69 | } 70 | } 71 | 72 | const language = parsedSwcConfig.jsc.parser.syntax === 'ecmascript' 73 | ? 'javascript' 74 | : 'typescript' 75 | 76 | return ( 77 | 78 | 79 | 80 | Input 81 | 82 | 83 | 90 | 93 | 94 | 95 | 101 | 110 | 111 | 112 | ) 113 | } 114 | -------------------------------------------------------------------------------- /src/components/MangleOptionsModal.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Checkbox, 4 | Modal, 5 | ModalBody, 6 | ModalCloseButton, 7 | ModalContent, 8 | ModalFooter, 9 | ModalHeader, 10 | ModalOverlay, 11 | Text, 12 | VStack, 13 | useDisclosure, 14 | } from '@chakra-ui/react' 15 | import { useAtom } from 'jotai' 16 | import { applyEdits, modify } from 'jsonc-parser' 17 | import { useState } from 'react' 18 | import type { ChangeEvent } from 'react' 19 | import { parsedSwcConfigAtom, swcConfigAtom } from '../state' 20 | import type { MangleOptions } from '../swc' 21 | import { JSONC_FORMATTING_OPTIONS } from '../utils' 22 | 23 | export default function MangleOptionsModal() { 24 | const [, setSwcConfig] = useAtom(swcConfigAtom) 25 | const [parsedSwcConfig] = useAtom(parsedSwcConfigAtom) 26 | const [options, setOptions] = useState( 27 | parsedSwcConfig.jsc?.minify?.mangle 28 | ) 29 | const { isOpen, onOpen, onClose } = useDisclosure() 30 | 31 | const handleApply = () => { 32 | setSwcConfig((config) => 33 | applyEdits( 34 | config, 35 | modify(config, ['jsc', 'minify', 'mangle'], options, { 36 | formattingOptions: JSONC_FORMATTING_OPTIONS, 37 | }) 38 | ) 39 | ) 40 | onClose() 41 | } 42 | 43 | const handleOpen = () => { 44 | setOptions(parsedSwcConfig.jsc?.minify?.mangle) 45 | onOpen() 46 | } 47 | 48 | const handleClose = () => { 49 | setOptions(parsedSwcConfig.jsc?.minify?.mangle) 50 | onClose() 51 | } 52 | 53 | if (!options) { 54 | return null 55 | } 56 | 57 | const handleOptionChange = ( 58 | key: keyof MangleOptions, 59 | event: ChangeEvent, 60 | ) => { 61 | setOptions((options) => 62 | options && typeof options === 'object' 63 | ? { ...options, [key]: event.target.checked } 64 | : options 65 | ) 66 | } 67 | 68 | return ( 69 | <> 70 | 73 | 74 | 75 | 76 | Mangle Options 77 | 78 | 79 | 80 | 81 | Not all options are shown here. You can also configure by closing this dialog then 82 | clicking the "Edit as JSON" button. 83 | 84 | 85 | {Object.entries(options).map(([key, value]) => ( 86 | handleOptionChange(key as keyof MangleOptions, event)} 90 | > 91 | {key} 92 | 93 | ))} 94 | 95 | 96 | 97 | 98 | 101 | 102 | 103 | 104 | 105 | 106 | ) 107 | } 108 | -------------------------------------------------------------------------------- /src/components/OutputEditor.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Flex, Heading, Select } from '@chakra-ui/react' 2 | import Editor, { useMonaco } from '@monaco-editor/react' 3 | import type { editor } from 'monaco-editor' 4 | import { useEffect } from 'react' 5 | import type { ChangeEvent } from 'react' 6 | import stripAnsi from 'strip-ansi' 7 | import type { 8 | OutputStructure, 9 | ParserResult, 10 | TransformationOutput, 11 | TransformationResult, 12 | } from '../swc' 13 | import { 14 | editorOptions as sharedEditorOptions, 15 | useBgColor, 16 | useBorderColor, 17 | useMonacoThemeValue, 18 | } from '../utils' 19 | 20 | function isTransformedCode(value: unknown): value is TransformationOutput { 21 | return typeof (value as TransformationOutput).code === 'string' 22 | } 23 | 24 | function containsOutput(value: unknown): value is OutputStructure { 25 | return typeof (value as OutputStructure).output === 'string' 26 | } 27 | 28 | type Output = Record<'text' | 'language' | 'path', string> 29 | 30 | function handleOutput(output: TransformationResult | ParserResult): Output { 31 | if (output.err) { 32 | const text = stripAnsi(output.val) 33 | return { 34 | text, 35 | language: 'text', 36 | path: 'error.log', 37 | } 38 | } 39 | 40 | if (containsOutput(output.val)) { 41 | try { 42 | const text = JSON.parse(output.val.output).__swc_isolated_declarations__ 43 | if (typeof text === 'string') { 44 | return { 45 | text, 46 | language: 'typescript', 47 | path: 'output.d.ts', 48 | } 49 | } 50 | } catch {} 51 | } 52 | 53 | if (isTransformedCode(output.val)) { 54 | const text = output.val.code 55 | return { 56 | text, 57 | language: 'javascript', 58 | path: 'output.js', 59 | } 60 | } else { 61 | const text = JSON.stringify(output.val, null, 2) 62 | return { 63 | text, 64 | language: 'json', 65 | path: 'output.json', 66 | } 67 | } 68 | } 69 | 70 | interface Props { 71 | output: TransformationResult | ParserResult 72 | viewMode: string 73 | onViewModeChange(viewMode: string): void 74 | } 75 | 76 | const editorOptions: editor.IStandaloneEditorConstructionOptions = { 77 | ...sharedEditorOptions, 78 | readOnly: true, 79 | wordWrap: 'on', 80 | renderControlCharacters: false, 81 | tabSize: 4, // this aligns with swc 82 | } 83 | 84 | export default function OutputEditor({ 85 | output, 86 | viewMode, 87 | onViewModeChange, 88 | }: Props) { 89 | const borderColor = useBorderColor() 90 | const bg = useBgColor() 91 | const monacoTheme = useMonacoThemeValue() 92 | const monaco = useMonaco() 93 | 94 | useEffect(() => { 95 | monaco?.languages.typescript.javascriptDefaults.setDiagnosticsOptions({ 96 | noSyntaxValidation: true, 97 | noSemanticValidation: true, 98 | noSuggestionDiagnostics: true, 99 | }) 100 | }, [monaco]) 101 | 102 | const handleViewModeChange = (event: ChangeEvent) => { 103 | onViewModeChange(event.target.value) 104 | } 105 | 106 | const { text: outputContent, path, language: editorLanguage } = handleOutput(output) 107 | 108 | return ( 109 | 110 | 111 | 112 | Output 113 | 114 | 115 | View: 116 | 126 | 127 | 128 | 129 | 137 | 138 | 139 | ) 140 | } 141 | -------------------------------------------------------------------------------- /src/components/VersionSelect.tsx: -------------------------------------------------------------------------------- 1 | import { Box, CircularProgress, Flex, HStack, Heading, Link, Select, Text } from '@chakra-ui/react' 2 | import type { ChangeEvent } from 'react' 3 | import { HiExternalLink } from 'react-icons/hi' 4 | import semver from 'semver' 5 | import useSWR from 'swr' 6 | import { useBgColor, useBorderColor } from '../utils' 7 | 8 | type PackageInfo = { 9 | tags: { 10 | latest: string, 11 | }, 12 | versions: string[], 13 | } 14 | 15 | const fetchSwcVersions = (packageName: string): Promise => 16 | fetch(`https://data.jsdelivr.com/v1/package/npm/${packageName}`).then( 17 | (response) => response.json() 18 | ) 19 | 20 | function mergeVersions(...versions: string[][]): string[] { 21 | return [...new Set(versions.flat())].sort(semver.rcompare) 22 | } 23 | 24 | interface Props { 25 | isLoadingSwc: boolean 26 | swcVersion: string 27 | onSwcVersionChange: (version: string) => void 28 | } 29 | 30 | export default function VersionSelect({ isLoadingSwc, swcVersion, onSwcVersionChange }: Props) { 31 | const { data: oldSWC, error: errorOfOld } = useSWR( 32 | '@swc/wasm-web', 33 | fetchSwcVersions, 34 | { revalidateOnFocus: false } 35 | ) 36 | const { data: newSWC, error: errorOfNew } = useSWR( 37 | '@swc/binding_core_wasm', 38 | fetchSwcVersions, 39 | { revalidateOnFocus: false } 40 | ) 41 | const bg = useBgColor() 42 | const borderColor = useBorderColor() 43 | 44 | const versions = mergeVersions(oldSWC?.versions ?? [], newSWC?.versions ?? []) 45 | .filter((version, index) => 46 | index === 0 || !version.includes('nightly') || version === swcVersion 47 | ) 48 | 49 | const handleCurrentVersionChange = ( 50 | event: ChangeEvent, 51 | ) => { 52 | onSwcVersionChange(event.target.value) 53 | } 54 | 55 | const isLoading = isLoadingSwc || (!oldSWC && !errorOfOld) || 56 | (!newSWC && !errorOfNew) 57 | 58 | return ( 59 | 60 | 61 | Version 62 | 63 | 70 | {oldSWC && newSWC 71 | ? ( 72 | 79 | ) 80 | : ( 81 | 84 | )} 85 | 86 | {isLoading && ( 87 | <> 88 | 89 | Please wait... 90 | 91 | )} 92 | 93 | 94 | More links: 95 | 96 | 102 | Docs 103 | 104 | 105 | 106 | 107 | 113 | GitHub 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | ) 123 | } 124 | -------------------------------------------------------------------------------- /src/components/Workspace.tsx: -------------------------------------------------------------------------------- 1 | import { Center, CircularProgress, VStack, useToast } from '@chakra-ui/react' 2 | import styled from '@emotion/styled' 3 | import { loader } from '@monaco-editor/react' 4 | import { useAtom } from 'jotai' 5 | import { Base64 } from 'js-base64' 6 | import { gzip, ungzip } from 'pako' 7 | import { useEffect, useMemo, useState } from 'react' 8 | import semver from 'semver' 9 | import useSWR from 'swr' 10 | import { Err } from 'ts-results' 11 | import { fileNameAtom, parsedSwcConfigAtom, swcConfigAtom } from '../state' 12 | import { type AST, loadSwc, parse, stripTypes, transform } from '../swc' 13 | import Configuration from './Configuration' 14 | import InputEditor from './InputEditor' 15 | import OutputEditor from './OutputEditor' 16 | import VersionSelect from './VersionSelect' 17 | 18 | const STORAGE_KEY = 'v1.code' 19 | 20 | function getIssueReportUrl({ 21 | code, 22 | version, 23 | config, 24 | playgroundLink, 25 | }: { 26 | code: string, 27 | version: string, 28 | config: string, 29 | playgroundLink: string, 30 | }): string { 31 | const reportUrl = new URL( 32 | `https://github.com/swc-project/swc/issues/new?assignees=&labels=C-bug&template=bug_report.yml` 33 | ) 34 | reportUrl.searchParams.set('code', code) 35 | reportUrl.searchParams.set('config', config) 36 | reportUrl.searchParams.set('repro-link', playgroundLink) 37 | reportUrl.searchParams.set('version', version) 38 | return reportUrl.toString() 39 | } 40 | 41 | const Main = styled.main` 42 | display: grid; 43 | padding: 1em; 44 | gap: 1em; 45 | 46 | grid-template-columns: 1fr; 47 | grid-template-rows: repeat(3, 1fr); 48 | grid-template-areas: 'sidebar' 'input' 'output'; 49 | 50 | min-height: 88vh; 51 | 52 | @media screen and (min-width: 600px) { 53 | grid-template-columns: 256px 1fr; 54 | grid-template-rows: repeat(2, 1fr); 55 | grid-template-areas: 'sidebar input' 'sidebar output'; 56 | 57 | min-height: calc(100vh - 80px); 58 | } 59 | 60 | @media screen and (min-width: 1200px) { 61 | grid-template-columns: 256px repeat(2, 1fr); 62 | grid-template-rows: 1fr; 63 | grid-template-areas: 'sidebar input output'; 64 | 65 | min-height: calc(100vh - 80px); 66 | } 67 | ` 68 | 69 | export default function Workspace() { 70 | const { data: monaco } = useSWR('monaco', () => loader.init()) 71 | const [swcVersion, setSwcVersion] = useState(() => 72 | new URLSearchParams(location.search).get('version') ?? 73 | process.env.NEXT_PUBLIC_SWC_VERSION 74 | ) 75 | const { data: swc, error } = useSWR(swcVersion, loadSwc, { 76 | revalidateOnFocus: false, 77 | }) 78 | const [code, setCode] = useState(() => localStorage.getItem(STORAGE_KEY) ?? '') 79 | const [swcConfigJSON] = useAtom(swcConfigAtom) 80 | const [swcConfig] = useAtom(parsedSwcConfigAtom) 81 | const [fileName] = useAtom(fileNameAtom) 82 | const [viewMode, setViewMode] = useState('code') 83 | const [isStripTypes, setIsStripTypes] = useState(false) 84 | const output = useMemo(() => { 85 | if (error) { 86 | return Err(String(error)) 87 | } 88 | 89 | if (!swc) { 90 | return Err('Loading swc...') 91 | } 92 | 93 | switch (viewMode) { 94 | case 'ast': 95 | return parse({ code, config: swcConfig, swc: swc[0] }) 96 | case 'code': 97 | default: 98 | return isStripTypes && swc[1] 99 | ? stripTypes({ code, fileName, config: swcConfig, swc: swc[1] }) 100 | : transform({ code, fileName, config: swcConfig, swc: swc[0] }) 101 | } 102 | }, [code, fileName, swc, error, swcConfig, viewMode, isStripTypes]) 103 | const toast = useToast() 104 | 105 | useEffect(() => { 106 | if (error) { 107 | toast({ 108 | title: 'Failed to load swc.', 109 | description: String(error), 110 | status: 'error', 111 | duration: 5000, 112 | position: 'top', 113 | isClosable: true, 114 | }) 115 | } 116 | }, [error, toast]) 117 | 118 | useEffect(() => { 119 | const url = new URL(location.href) 120 | const encodedInput = url.searchParams.get('code') 121 | if (encodedInput) { 122 | setCode(ungzip(Base64.toUint8Array(encodedInput), { to: 'string' })) 123 | } 124 | setIsStripTypes(url.searchParams.has('strip-types')) 125 | }, []) 126 | 127 | useEffect(() => { 128 | localStorage.setItem(STORAGE_KEY, code) 129 | }, [code]) 130 | 131 | const shareUrl = useMemo(() => { 132 | const url = new URL(location.href) 133 | url.searchParams.set('version', swcVersion) 134 | const encodedInput = Base64.fromUint8Array(gzip(code)) 135 | url.searchParams.set('code', encodedInput) 136 | const encodedConfig = Base64.fromUint8Array(gzip(swcConfigJSON)) 137 | url.searchParams.set('config', encodedConfig) 138 | if (isStripTypes) { 139 | url.searchParams.set('strip-types', '') 140 | } 141 | return url.toString() 142 | }, [code, swcConfigJSON, swcVersion, isStripTypes]) 143 | 144 | const issueReportUrl = useMemo( 145 | () => 146 | getIssueReportUrl({ 147 | code, 148 | config: swcConfigJSON, 149 | version: swcVersion, 150 | playgroundLink: shareUrl, 151 | }), 152 | [code, swcConfigJSON, swcVersion, shareUrl] 153 | ) 154 | 155 | function handleReportIssue() { 156 | if (code.length > 2000) { 157 | toast({ 158 | title: 'Code too long', 159 | description: 160 | 'Your input is too large to share. Please copy the code and paste it into the issue.', 161 | status: 'error', 162 | duration: 5000, 163 | isClosable: true, 164 | }) 165 | return 166 | } 167 | window.open(issueReportUrl, '_blank') 168 | } 169 | 170 | async function handleShare() { 171 | if (!navigator.clipboard) { 172 | toast({ 173 | title: 'Error', 174 | description: 'Clipboard is not supported in your environment.', 175 | status: 'error', 176 | duration: 3000, 177 | position: 'top', 178 | isClosable: true, 179 | }) 180 | return 181 | } 182 | 183 | window.history.replaceState(null, '', shareUrl) 184 | await navigator.clipboard.writeText(shareUrl) 185 | toast({ 186 | title: 'URL is copied to clipboard.', 187 | status: 'success', 188 | duration: 3000, 189 | position: 'top', 190 | isClosable: true, 191 | }) 192 | } 193 | 194 | function handleSwcVersionChange(version: string) { 195 | setSwcVersion(version) 196 | if (semver.lt(version, '1.7.1')) { 197 | setIsStripTypes(false) 198 | } 199 | } 200 | 201 | const isLoadingMonaco = !monaco 202 | if (isLoadingMonaco && !swc) { 203 | return ( 204 |
205 | 206 |
207 | Loading swc {swcVersion} 208 | {isLoadingMonaco && ' and editor'}... 209 |
210 |
211 | ) 212 | } 213 | 214 | if (output.ok === true && viewMode === 'ast') { 215 | const val = output.val as AST 216 | adjustOffsetOfAst(val, val.span.start) 217 | } 218 | 219 | return ( 220 |
221 | 222 | 227 | 232 | 233 | 240 | 245 |
246 | ) 247 | } 248 | 249 | function adjustOffsetOfAst(obj: unknown, startOffset: number) { 250 | if (Array.isArray(obj)) { 251 | obj.forEach((item) => adjustOffsetOfAst(item, startOffset)) 252 | } else if (isRecord(obj)) { 253 | Object.entries(obj).forEach(([key, value]) => { 254 | if (key === 'span' && value && isSpan(value)) { 255 | const span = value 256 | span.start -= startOffset 257 | span.end -= startOffset 258 | } else { 259 | adjustOffsetOfAst(obj[key], startOffset) 260 | } 261 | }) 262 | } 263 | } 264 | 265 | function isRecord(obj: unknown): obj is Record { 266 | return typeof obj === 'object' && obj !== null 267 | } 268 | 269 | function isSpan(obj: unknown): obj is { start: number, end: number } { 270 | return ( 271 | typeof obj === 'object' && obj !== null && 'start' in obj && 'end' in obj 272 | ) 273 | } 274 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { ChakraProvider } from '@chakra-ui/react' 2 | import type { AppProps } from 'next/app' 3 | import Head from 'next/head' 4 | 5 | function App({ Component, pageProps }: AppProps) { 6 | return ( 7 | 8 | 9 | SWC Playground 10 | 11 | 12 | 13 | 14 | ) 15 | } 16 | 17 | export default App 18 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { Box, useColorModeValue } from '@chakra-ui/react' 2 | import dynamic from 'next/dynamic' 3 | import HeaderBar from '../components/HeaderBar' 4 | 5 | const Workspace = dynamic(() => import('../components/Workspace'), { 6 | ssr: false, 7 | }) 8 | 9 | export default function App() { 10 | const bg = useColorModeValue('gray.50', 'gray.800') 11 | 12 | return ( 13 | 14 | 15 | 16 | 17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /src/state.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'jotai' 2 | import { parse } from 'jsonc-parser' 3 | import type { CompressOptions, Config, EnvOptions, MangleOptions } from './swc' 4 | 5 | /** @see https://github.com/swc-project/swc/blob/dada2d7d554fa0733a3c65c512777f1548d41a35/crates/swc_ecma_minifier/src/option/mod.rs#L114 */ 6 | export const defaultCompressOptions: CompressOptions = { 7 | arguments: false, 8 | arrows: true, 9 | booleans: true, 10 | booleans_as_integers: false, 11 | collapse_vars: true, 12 | comparisons: true, 13 | computed_props: true, 14 | conditionals: true, 15 | dead_code: true, 16 | directives: true, 17 | drop_console: false, 18 | drop_debugger: true, 19 | evaluate: true, 20 | expression: false, 21 | hoist_funs: false, 22 | hoist_props: true, 23 | hoist_vars: false, 24 | if_return: true, 25 | join_vars: true, 26 | keep_classnames: false, 27 | keep_fargs: true, 28 | keep_fnames: false, 29 | keep_infinity: false, 30 | loops: true, 31 | negate_iife: true, 32 | properties: true, 33 | reduce_funcs: false, 34 | reduce_vars: false, 35 | side_effects: true, 36 | switches: true, 37 | typeofs: true, 38 | unsafe: false, 39 | unsafe_arrows: false, 40 | unsafe_comps: false, 41 | unsafe_Function: false, 42 | unsafe_math: false, 43 | unsafe_symbols: false, 44 | unsafe_methods: false, 45 | unsafe_proto: false, 46 | unsafe_regexp: false, 47 | unsafe_undefined: false, 48 | unused: true, 49 | const_to_let: true, 50 | pristine_globals: true, 51 | } 52 | 53 | export const defaultMangleOptions: MangleOptions = { 54 | toplevel: false, 55 | keep_classnames: false, 56 | keep_fnames: false, 57 | keep_private_props: false, 58 | ie8: false, 59 | safari10: false, 60 | } 61 | 62 | export const defaultEnvOptions: EnvOptions = { 63 | targets: '', 64 | } 65 | 66 | export const swcConfigAtom = atom( 67 | JSON.stringify( 68 | { 69 | jsc: { 70 | parser: { 71 | syntax: 'ecmascript', 72 | jsx: false, 73 | }, 74 | target: 'es5', 75 | loose: false, 76 | minify: { 77 | compress: false, 78 | mangle: false, 79 | }, 80 | }, 81 | module: { 82 | type: 'es6', 83 | }, 84 | minify: false, 85 | isModule: true, 86 | }, 87 | null, 88 | 2 89 | ) 90 | ) 91 | 92 | export const parsedSwcConfigAtom = atom((get) => 93 | parse(get(swcConfigAtom), undefined, { allowTrailingComma: true }) 94 | ) 95 | 96 | export const fileNameAtom = atom((get) => { 97 | const config = get(parsedSwcConfigAtom) 98 | 99 | if (config.jsc.parser.syntax === 'typescript') { 100 | if (config.jsc.parser.tsx) { 101 | return 'input.tsx' 102 | } else { 103 | return 'input.ts' 104 | } 105 | } else { 106 | if (config.jsc.parser.jsx) { 107 | return 'input.jsx' 108 | } else { 109 | return 'input.js' 110 | } 111 | } 112 | }) 113 | -------------------------------------------------------------------------------- /src/swc.ts: -------------------------------------------------------------------------------- 1 | import type * as SwcStripTypes from '@swc/wasm-typescript-esm' 2 | import semver from 'semver' 3 | import { Err, Ok, type Result } from 'ts-results' 4 | 5 | interface SwcModule { 6 | default(): Promise 7 | parseSync(code: string, options: ParseOnlyOptions): AST 8 | transformSync(code: string, options: Config): TransformationOutput 9 | } 10 | 11 | export interface Config { 12 | jsc: { 13 | parser: ParserOptions, 14 | target?: EsVersion, 15 | loose?: boolean, 16 | minify?: { 17 | compress?: boolean | CompressOptions, 18 | mangle?: boolean | MangleOptions, 19 | format?: Record, 20 | ecma?: number | string, 21 | keepClassnames?: boolean, 22 | keepFnames?: boolean, 23 | module?: boolean, 24 | safari10?: boolean, 25 | toplevel?: boolean, 26 | sourceMap?: boolean | TerserSourceMapOption, 27 | outputPath?: string, 28 | inlineSourcesContent?: boolean, 29 | emitSourceMapColumns?: boolean, 30 | }, 31 | transform?: TransformOptions, 32 | externalHelpers?: boolean, 33 | keepClassNames?: boolean, 34 | baseUrl?: string, 35 | paths?: Record, 36 | experimental?: Record, 37 | } 38 | module?: ModuleOptions 39 | minify?: boolean 40 | env?: EnvOptions 41 | isModule?: boolean | 'unknown' 42 | sourceMaps?: boolean | 'inline' 43 | inlineSourcesContent?: boolean 44 | filename?: string 45 | } 46 | 47 | export type ParserOptions = 48 | | { 49 | syntax: 'ecmascript', 50 | jsx?: boolean, 51 | functionBind?: boolean, 52 | decorators?: boolean, 53 | decoratorsBeforeExport?: boolean, 54 | exportDefaultFrom?: boolean, 55 | importAssertions?: boolean, 56 | staticBlocks?: boolean, 57 | privateInObject?: boolean, 58 | } 59 | | { 60 | syntax: 'typescript', 61 | tsx?: boolean, 62 | decorators?: boolean, 63 | } 64 | 65 | export type EsVersion = 66 | | 'es3' 67 | | 'es5' 68 | | 'es2015' 69 | | 'es2016' 70 | | 'es2017' 71 | | 'es2018' 72 | | 'es2019' 73 | | 'es2020' 74 | | 'es2021' 75 | | 'es2022' 76 | | 'es2023' 77 | | 'es2024' 78 | 79 | export type ModuleOptions = 80 | | { 81 | type: 'es6', 82 | strict?: boolean, 83 | strictMode?: boolean, 84 | lazy?: boolean, 85 | noInterop?: boolean, 86 | } 87 | | { 88 | type: 'commonjs', 89 | strict?: boolean, 90 | strictMode?: boolean, 91 | lazy?: boolean, 92 | noInterop?: boolean, 93 | } 94 | | { 95 | type: 'amd', 96 | moduleId?: string, 97 | strict?: boolean, 98 | strictMode?: boolean, 99 | lazy?: boolean, 100 | noInterop?: boolean, 101 | } 102 | | { 103 | type: 'umd', 104 | globals?: Record, 105 | strict?: boolean, 106 | strictMode?: boolean, 107 | lazy?: boolean, 108 | noInterop?: boolean, 109 | } 110 | | { 111 | type: 'systemjs', 112 | allowTopLevelThis?: boolean, 113 | } 114 | 115 | export interface CompressOptions { 116 | arguments?: boolean 117 | arrows?: boolean 118 | booleans?: boolean 119 | booleans_as_integers?: boolean 120 | collapse_vars?: boolean 121 | comparisons?: boolean 122 | computed_props?: boolean 123 | conditionals?: boolean 124 | dead_code?: boolean 125 | defaults?: boolean 126 | directives?: boolean 127 | drop_console?: boolean 128 | drop_debugger?: boolean 129 | ecma?: number | string 130 | evaluate?: boolean 131 | expression?: boolean 132 | global_defs?: Record 133 | hoist_funs?: boolean 134 | hoist_props?: boolean 135 | hoist_vars?: boolean 136 | ie8?: boolean 137 | if_return?: boolean 138 | inline?: boolean | number 139 | join_vars?: boolean 140 | keep_classnames?: boolean 141 | keep_fargs?: boolean 142 | keep_fnames?: boolean 143 | keep_infinity?: boolean 144 | loops?: boolean 145 | negate_iife?: boolean 146 | passes?: number 147 | properties?: boolean 148 | pure_getters?: boolean | 'strict' | string 149 | pure_funcs?: string[] 150 | reduce_funcs?: boolean 151 | reduce_vars?: boolean 152 | sequences?: boolean | number 153 | side_effects?: boolean 154 | switches?: boolean 155 | top_retain?: string[] | string | null 156 | toplevel?: boolean | string 157 | typeofs?: boolean 158 | unsafe?: boolean 159 | unsafe_arrows?: boolean 160 | unsafe_comps?: boolean 161 | unsafe_Function?: boolean 162 | unsafe_math?: boolean 163 | unsafe_symbols?: boolean 164 | unsafe_methods?: boolean 165 | unsafe_proto?: boolean 166 | unsafe_regexp?: boolean 167 | unsafe_undefined?: boolean 168 | unused?: boolean 169 | module?: boolean 170 | const_to_let?: boolean 171 | pristine_globals?: boolean 172 | } 173 | 174 | export interface MangleOptions { 175 | props?: { 176 | reserved?: string[], 177 | undeclared?: boolean, 178 | regex?: null | string, 179 | } 180 | toplevel?: boolean 181 | keep_classnames?: boolean 182 | keep_fnames?: boolean 183 | keep_private_props?: boolean 184 | ie8?: boolean 185 | safari10?: boolean 186 | } 187 | 188 | export interface TerserSourceMapOption { 189 | filename?: string 190 | url?: string 191 | root?: string 192 | content?: string 193 | } 194 | 195 | export interface TransformOptions { 196 | react?: { 197 | runtime?: 'automatic' | 'classic', 198 | importSource?: string, 199 | pragma?: string, 200 | pragmaFrag?: string, 201 | throwIfNamespace?: boolean, 202 | development?: boolean, 203 | useBuiltins?: boolean, 204 | refresh?: { 205 | refreshReg?: string, 206 | refreshSig?: string, 207 | emitFullSignatures?: boolean, 208 | }, 209 | } 210 | constModules?: { 211 | globals?: Record>, 212 | } 213 | optimizer?: { 214 | globals?: { 215 | vars?: Record, 216 | envs?: string[] | Record, 217 | typeofs?: Record, 218 | }, 219 | simplify?: boolean, 220 | jsonify?: { 221 | minCost?: number, 222 | }, 223 | } 224 | legacyDecorator?: boolean 225 | decoratorMetadata?: boolean 226 | useDefineForClassFields?: boolean 227 | verbatimModuleSyntax?: boolean 228 | } 229 | 230 | export interface EnvOptions { 231 | targets?: 232 | | string 233 | | string[] 234 | | Record< 235 | | 'chrome' 236 | | 'opera' 237 | | 'edge' 238 | | 'firefox' 239 | | 'safari' 240 | | 'ie' 241 | | 'ios' 242 | | 'android' 243 | | 'node' 244 | | 'electron', 245 | string 246 | > 247 | mode?: 'usage' | 'entry' 248 | skip?: string[] 249 | dynamicImport?: boolean 250 | loose?: boolean 251 | include?: string[] 252 | exclude?: string[] 253 | coreJs?: 2 | 3 | string 254 | shippedProposals?: boolean 255 | forceAllTransforms?: boolean 256 | bugfixes?: boolean 257 | } 258 | 259 | export type ParseOnlyOptions = ParserOptions & { 260 | comments?: boolean, 261 | isModule?: boolean | 'unknown', 262 | target?: EsVersion, 263 | } 264 | 265 | export interface AST { 266 | type: 'Module' | 'Script' 267 | body: unknown 268 | span: { start: number, end: number, ctxt: number } 269 | } 270 | 271 | export interface TransformationOutput { 272 | code: string 273 | } 274 | 275 | export interface OutputStructure { 276 | output: string 277 | } 278 | 279 | /** SWC renamed npm package since v1.2.166. */ 280 | export function getPackageName(version: string) { 281 | return semver.gt(version, '1.2.165') && semver.lte(version, '1.2.170') 282 | ? '@swc/binding_core_wasm' 283 | : '@swc/wasm-web' 284 | } 285 | 286 | export function loadSwc( 287 | version: string, 288 | ): Promise<[SwcModule, typeof SwcStripTypes | null]> { 289 | return Promise.all([ 290 | loadSwcCore(version), 291 | semver.gte(version, '1.7.1') ? loadSwcStripTypes(version) : null, 292 | ]) 293 | } 294 | 295 | async function loadSwcCore(version: string): Promise { 296 | const packageName = getPackageName(version) 297 | const entryFileName = semver.gt(version, '1.2.165') && semver.lt(version, '1.6.7') 298 | ? 'wasm-web.js' 299 | : 'wasm.js' 300 | const swcModule: SwcModule = await import( 301 | /* webpackIgnore: true */ 302 | `https://cdn.jsdelivr.net/npm/${packageName}@${version}/${entryFileName}` 303 | ) 304 | await swcModule.default() 305 | return swcModule 306 | } 307 | 308 | async function loadSwcStripTypes(version: string): Promise { 309 | const swcModule: typeof SwcStripTypes = await import( 310 | /* webpackIgnore: true */ 311 | `https://cdn.jsdelivr.net/npm/@swc/wasm-typescript-esm@${version}/wasm.js` 312 | ) 313 | await swcModule.default() 314 | return swcModule 315 | } 316 | 317 | export type TransformationResult = Result 318 | 319 | export function transform({ 320 | code, 321 | config, 322 | fileName, 323 | swc, 324 | }: { 325 | code: string, 326 | fileName: string, 327 | config: Config, 328 | swc: SwcModule, 329 | }): TransformationResult { 330 | try { 331 | return Ok(swc.transformSync(code, { ...config, filename: fileName })) 332 | } catch (error) { 333 | return handleSwcError(error) 334 | } 335 | } 336 | 337 | export type ParserResult = Result 338 | 339 | export function parse({ 340 | code, 341 | config, 342 | swc, 343 | }: { 344 | code: string, 345 | config: Config, 346 | swc: SwcModule, 347 | }): ParserResult { 348 | try { 349 | return Ok( 350 | swc.parseSync(code, { 351 | ...config.jsc.parser, 352 | target: config.jsc.target, 353 | isModule: config.isModule ?? 'unknown', 354 | }) 355 | ) 356 | } catch (error) { 357 | return handleSwcError(error) 358 | } 359 | } 360 | 361 | function handleSwcError(error: unknown): Err { 362 | if (typeof error === 'string') { 363 | return Err(error) 364 | } else if (error instanceof Error) { 365 | return Err(`${error.toString()}\n\n${error.stack}`) 366 | } else if (isStripTypeError(error)) { 367 | return Err(`${error.code}\n\n${error.message}`) 368 | } else { 369 | return Err(String(error)) 370 | } 371 | } 372 | 373 | type StripTypeError = Record<'code' | 'message', string> 374 | 375 | function isStripTypeError(error: unknown): error is StripTypeError { 376 | return ( 377 | typeof error === 'object' && 378 | error !== null && 379 | 'code' in error && 380 | 'message' in error 381 | ) 382 | } 383 | 384 | export function stripTypes({ 385 | code, 386 | config, 387 | fileName, 388 | swc, 389 | }: { 390 | code: string, 391 | config: Config, 392 | fileName: string, 393 | swc: typeof SwcStripTypes, 394 | }): Result { 395 | try { 396 | return Ok( 397 | swc.transformSync(code, { 398 | filename: fileName, 399 | module: typeof config.isModule === 'boolean' ? config.isModule : undefined, 400 | }) 401 | ) 402 | } catch (error) { 403 | return handleSwcError(error) 404 | } 405 | } 406 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { useColorModeValue } from '@chakra-ui/react' 2 | import type { FormattingOptions } from 'jsonc-parser' 3 | import type { editor } from 'monaco-editor' 4 | 5 | export const editorOptions: editor.IStandaloneEditorConstructionOptions = { 6 | fontFamily: '"Cascadia Code", "Jetbrains Mono", "Fira Code", "Menlo", "Consolas", monospace', 7 | fontLigatures: true, 8 | fontSize: 14, 9 | lineHeight: 24, 10 | minimap: { enabled: false }, 11 | tabSize: 2, 12 | } 13 | 14 | export function useMonacoThemeValue() { 15 | return useColorModeValue('light', 'vs-dark') 16 | } 17 | 18 | export function useBorderColor() { 19 | return useColorModeValue('gray.400', 'gray.600') 20 | } 21 | 22 | export function useBgColor() { 23 | return useColorModeValue('white', 'gray.700') 24 | } 25 | 26 | const RE_SWC_ERROR = /error:\s(.+?)\n\s-->\s.+?:(\d+):(\d+)/gm 27 | 28 | export function parseSWCError(message: string) { 29 | return message.matchAll(RE_SWC_ERROR) 30 | } 31 | 32 | export const JSONC_FORMATTING_OPTIONS: FormattingOptions = { 33 | tabSize: 2, 34 | insertSpaces: true, 35 | } 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@gplane/tsconfig", 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "target": "esnext", 6 | "noEmit": true, 7 | "lib": ["dom", "dom.iterable", "esnext"], 8 | "forceConsistentCasingInFileNames": true, 9 | "incremental": true, 10 | "jsx": "preserve", 11 | "moduleResolution": "bundler" 12 | }, 13 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 14 | "exclude": ["node_modules"] 15 | } 16 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "silent": true 4 | } 5 | } 6 | --------------------------------------------------------------------------------