├── .eslintrc.json ├── .gitignore ├── .prettierrc ├── .vscode └── settings.json ├── README.md ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── favicon.ico ├── favicons │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ └── site.webmanifest ├── frame.html ├── logo.svg ├── vendor │ └── sass.worker.js └── vercel.svg ├── src ├── components │ ├── Editor │ │ ├── EditorDesktop.js │ │ ├── format.js │ │ ├── index.js │ │ └── setupTsxMode.js │ ├── Preview │ │ └── index.jsx │ ├── codemirror │ │ └── index.js │ ├── header │ │ ├── LayoutSwitch.js │ │ ├── SaveBtn.js │ │ └── index.js │ ├── monaco │ │ ├── index.js │ │ └── markdown.js │ ├── select │ │ └── index.js │ └── setting │ │ └── Modal.js ├── hooks │ ├── useDebouncedState.js │ └── useIsomorphicLayoutEffect.js ├── pages │ ├── _app.js │ ├── api │ │ ├── hello.js │ │ └── thumbnail.js │ ├── index.js │ ├── pen │ │ └── [...id].js │ ├── preview │ │ └── [...id].js │ └── qrcode-generator.js ├── styles │ ├── Home.module.css │ ├── codemirror │ │ ├── base.css │ │ └── dark.css │ ├── globals.css │ └── tailwind.css ├── utils │ ├── compile.js │ ├── database.js │ ├── theme.js │ └── workers.js └── workers │ ├── compile.worker.js │ └── prettier.worker.js ├── tailwind.config.js └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "commentTranslate.hover.enabled": false 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | ``` 12 | 13 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 14 | 15 | You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file. 16 | 17 | [API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`. 18 | 19 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const MonacoWebpackPlugin = require("monaco-editor-webpack-plugin"); 2 | const path = require("path"); 3 | 4 | module.exports = { 5 | eslint: { 6 | ignoreDuringBuilds: true, 7 | }, 8 | webpack: (config, { isServer, webpack, dev }) => { 9 | config.module.rules 10 | .filter((rule) => rule.oneOf) 11 | .forEach((rule) => { 12 | rule.oneOf.forEach((r) => { 13 | if ( 14 | r.issuer && 15 | r.issuer.and && 16 | r.issuer.and.length === 1 && 17 | r.issuer.and[0].source && 18 | r.issuer.and[0].source.replace(/\\/g, "") === 19 | path.resolve(process.cwd(), "src/pages/_app") 20 | ) { 21 | r.issuer.or = [ 22 | ...r.issuer.and, 23 | /[\\/]node_modules[\\/]monaco-editor[\\/]/, 24 | ]; 25 | delete r.issuer.and; 26 | } 27 | }); 28 | }); 29 | 30 | config.output.globalObject = "self"; 31 | if (!isServer) { 32 | config.plugins.push( 33 | new MonacoWebpackPlugin({ 34 | languages: [ 35 | "json", 36 | "markdown", 37 | "css", 38 | "typescript", 39 | "javascript", 40 | "html", 41 | "scss", 42 | "less", 43 | ], 44 | filename: "static/[name].worker.js", 45 | }) 46 | ); 47 | } 48 | return config; 49 | }, 50 | }; 51 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-pen", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "chrome-aws-lambda": "^10.1.0", 13 | "next": "12.1.5", 14 | "node-fetch": "^3.2.4", 15 | "playwright-core": "^1.21.1", 16 | "react": "17.x", 17 | "react-dom": "17.x" 18 | }, 19 | "devDependencies": { 20 | "@babel/standalone": "^7.17.9", 21 | "@headlessui/react": "^1.6.0", 22 | "autoprefixer": "^10.4.4", 23 | "clsx": "^1.1.1", 24 | "codemirror": "^5.57.0", 25 | "debounce": "^1.2.1", 26 | "dlv": "^1.1.3", 27 | "eslint": "8.13.0", 28 | "eslint-config-next": "12.1.5", 29 | "is-mobile": "^3.1.1", 30 | "less": "^4.1.2", 31 | "monaco-editor": "^0.33.0", 32 | "monaco-editor-webpack-plugin": "^7.0.1", 33 | "next-transpile-modules": "^9.0.0", 34 | "p-queue": "^7.2.0", 35 | "postcss": "^8.4.12", 36 | "prettier": "^2.6.2", 37 | "qrcode": "^1.5.0", 38 | "react-split-pane": "^0.1.92", 39 | "react-use": "^17.3.2", 40 | "sass.js": "^0.11.1", 41 | "tailwindcss": "^3.0.24", 42 | "worker-loader": "^3.0.8" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maqi1520/next-code-pen/a779f00a148a64e2b54bd80c78ea5ce148163de0/public/favicon.ico -------------------------------------------------------------------------------- /public/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maqi1520/next-code-pen/a779f00a148a64e2b54bd80c78ea5ce148163de0/public/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/favicons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maqi1520/next-code-pen/a779f00a148a64e2b54bd80c78ea5ce148163de0/public/favicons/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maqi1520/next-code-pen/a779f00a148a64e2b54bd80c78ea5ce148163de0/public/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maqi1520/next-code-pen/a779f00a148a64e2b54bd80c78ea5ce148163de0/public/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maqi1520/next-code-pen/a779f00a148a64e2b54bd80c78ea5ce148163de0/public/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maqi1520/next-code-pen/a779f00a148a64e2b54bd80c78ea5ce148163de0/public/favicons/favicon.ico -------------------------------------------------------------------------------- /public/favicons/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /public/frame.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 85 | 86 | 87 | 88 | 89 | 113 | 114 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | 6 | 10 | 14 | 18 | 25 | + 26 | 27 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/Editor/EditorDesktop.js: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect } from "react"; 2 | import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; 3 | import { CommandsRegistry } from "monaco-editor/esm/vs/platform/commands/common/commands"; 4 | import { registerDocumentFormattingEditProviders } from "./format"; 5 | 6 | function setupKeybindings(editor) { 7 | let formatCommandId = "editor.action.formatDocument"; 8 | const { handler, when } = CommandsRegistry.getCommand(formatCommandId); 9 | editor._standaloneKeybindingService.addDynamicKeybinding( 10 | formatCommandId, 11 | monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS, 12 | handler, 13 | when 14 | ); 15 | } 16 | registerDocumentFormattingEditProviders(); 17 | 18 | const languageToMode = { 19 | html: "html", 20 | css: "css", 21 | less: "less", 22 | scss: "scss", 23 | javascript: "javascript", 24 | babel: "javascript", 25 | typescript: "typescript", 26 | }; 27 | 28 | const Editor = ({ language, defaultValue, value, onChange }) => { 29 | const divEl = useRef(null); 30 | const editor = useRef(null); 31 | 32 | useEffect(() => { 33 | if (divEl.current) { 34 | editor.current = monaco.editor.create(divEl.current, { 35 | minimap: { enabled: false }, 36 | theme: "vs-dark", 37 | }); 38 | editor.current.onDidChangeModelContent(() => { 39 | onChange(editor.current.getValue()); 40 | }); 41 | } 42 | 43 | setupKeybindings(editor.current); 44 | 45 | return () => { 46 | editor.current.dispose(); 47 | }; 48 | }, []); 49 | 50 | useEffect(() => { 51 | const model = editor.current.getModel(); 52 | monaco.editor.setModelLanguage(model, languageToMode[language]); 53 | }, [language]); 54 | 55 | useEffect(() => { 56 | if (defaultValue) { 57 | editor.current.setValue(defaultValue); 58 | } 59 | }, []); 60 | 61 | useEffect(() => { 62 | if (value) { 63 | editor.current.setValue(value); 64 | } 65 | }, [value]); 66 | 67 | useEffect(() => { 68 | const observer = new ResizeObserver(() => { 69 | window.setTimeout(() => editor.current.layout(), 0); 70 | }); 71 | observer.observe(divEl.current); 72 | return () => { 73 | observer.disconnect(); 74 | }; 75 | }, []); 76 | 77 | return
; 78 | }; 79 | 80 | export default Editor; 81 | -------------------------------------------------------------------------------- /src/components/Editor/format.js: -------------------------------------------------------------------------------- 1 | import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; 2 | import PrettierWorker from "worker-loader!../../workers/prettier.worker.js"; 3 | import { createWorkerQueue } from "../../utils/workers"; 4 | 5 | export function registerDocumentFormattingEditProviders() { 6 | const disposables = []; 7 | let prettierWorker; 8 | 9 | const formattingEditProvider = { 10 | async provideDocumentFormattingEdits(model, _options, _token) { 11 | if (!prettierWorker) { 12 | prettierWorker = createWorkerQueue(PrettierWorker); 13 | } 14 | const { canceled, error, pretty } = await prettierWorker.emit({ 15 | text: model.getValue(), 16 | language: model.getLanguageId(), 17 | }); 18 | if (canceled || error) return []; 19 | return [ 20 | { 21 | range: model.getFullModelRange(), 22 | text: pretty, 23 | }, 24 | ]; 25 | }, 26 | }; 27 | 28 | // // override the built-in HTML formatter 29 | // const _registerDocumentFormattingEditProvider = 30 | // monaco.languages.registerDocumentFormattingEditProvider; 31 | // monaco.languages.registerDocumentFormattingEditProvider = (id, provider) => { 32 | // if ((['css','less','scss','javascript','typescript','html'].includes(id))) { 33 | // return _registerDocumentFormattingEditProvider( 34 | // , 35 | // formattingEditProvider 36 | // ); 37 | 38 | // }else{ 39 | // return _registerDocumentFormattingEditProvider(id, provider); 40 | // } 41 | 42 | // }; 43 | ["css", "less", "scss", "javascript", "typescript", "html"].forEach((id) => { 44 | disposables.push( 45 | monaco.languages.registerDocumentFormattingEditProvider( 46 | id, 47 | formattingEditProvider 48 | ) 49 | ); 50 | }); 51 | 52 | return { 53 | dispose() { 54 | disposables.forEach((disposable) => disposable.dispose()); 55 | if (prettierWorker) { 56 | prettierWorker.terminate(); 57 | } 58 | }, 59 | }; 60 | } 61 | -------------------------------------------------------------------------------- /src/components/Editor/index.js: -------------------------------------------------------------------------------- 1 | import dynamic from "next/dynamic"; 2 | import isMobile from "is-mobile"; 3 | const EditorMobile = dynamic(() => import("../codemirror"), { 4 | ssr: false, 5 | }); 6 | 7 | const EditorDesktop = dynamic(() => import("./EditorDesktop"), { 8 | ssr: false, 9 | }); 10 | 11 | export const Editor = isMobile() ? EditorMobile : EditorDesktop; 12 | -------------------------------------------------------------------------------- /src/components/Editor/setupTsxMode.js: -------------------------------------------------------------------------------- 1 | import * as monaco from "monaco-editor/esm/vs/editor/editor.api"; 2 | 3 | export function setupTsxMode(content) { 4 | const modelUri = monaco.Uri.file("index.tsx"); 5 | const codeModel = monaco.editor.createModel( 6 | content || "", 7 | "typescript", 8 | modelUri 9 | ); 10 | 11 | // 设置typescript 使用jsx 的编译方式 12 | monaco.languages.typescript.typescriptDefaults.setCompilerOptions({ 13 | jsx: "react", 14 | }); 15 | 16 | monaco.languages.typescript.typescriptDefaults.setDiagnosticsOptions({ 17 | noSemanticValidation: false, 18 | noSyntaxValidation: false, 19 | }); 20 | return codeModel; 21 | } 22 | 23 | export function setupHtmlMode(content) { 24 | const modelUri = monaco.Uri.file("index.html"); 25 | const codeModel = monaco.editor.createModel(content || "", "html", modelUri); 26 | return codeModel; 27 | } 28 | 29 | export function setupJavascriptMode(content) { 30 | const modelUri = monaco.Uri.file("index.js"); 31 | const codeModel = monaco.editor.createModel( 32 | content || "", 33 | "javascript", 34 | modelUri 35 | ); 36 | return codeModel; 37 | } 38 | 39 | export function setupTypescriptMode(content) { 40 | const modelUri = monaco.Uri.file("index.ts"); 41 | const codeModel = monaco.editor.createModel( 42 | content || "", 43 | "typescript", 44 | modelUri 45 | ); 46 | return codeModel; 47 | } 48 | 49 | export function setupCssMode(content) { 50 | const modelUri = monaco.Uri.file("index.css"); 51 | const codeModel = monaco.editor.createModel(content || "", "css", modelUri); 52 | return codeModel; 53 | } 54 | -------------------------------------------------------------------------------- /src/components/Preview/index.jsx: -------------------------------------------------------------------------------- 1 | import { forwardRef, useMemo, useState } from "react"; 2 | import clsx from "clsx"; 3 | import { useIsomorphicLayoutEffect } from "../../hooks/useIsomorphicLayoutEffect"; 4 | 5 | export default forwardRef(function Preview( 6 | { onLoad, iframeClassName = "", scripts, styles }, 7 | ref 8 | ) { 9 | const [resizing, setResizing] = useState(); 10 | useIsomorphicLayoutEffect(() => { 11 | function onMouseMove(e) { 12 | e.preventDefault(); 13 | setResizing(true); 14 | } 15 | function onMouseUp(e) { 16 | e.preventDefault(); 17 | setResizing(); 18 | } 19 | window.addEventListener("mousemove", onMouseMove); 20 | window.addEventListener("mouseup", onMouseUp); 21 | window.addEventListener("touchmove", onMouseMove); 22 | window.addEventListener("touchend", onMouseUp); 23 | return () => { 24 | window.removeEventListener("mousemove", onMouseMove); 25 | window.removeEventListener("mouseup", onMouseUp); 26 | window.removeEventListener("touchmove", onMouseMove); 27 | window.removeEventListener("touchend", onMouseUp); 28 | }; 29 | }, []); 30 | 31 | const scriptsdom = scripts 32 | .map((s) => { 33 | if (s.trim() !== "") { 34 | return ``; 35 | } else { 36 | return false; 37 | } 38 | }) 39 | .filter(Boolean) 40 | .join(""); 41 | const stylessdom = styles 42 | .map((s) => { 43 | if (s.trim() !== "") { 44 | return ``; 45 | } else { 46 | return false; 47 | } 48 | }) 49 | .filter(Boolean) 50 | .join(""); 51 | 52 | const srcDoc = ` 53 | 54 | 55 | 56 | ${stylessdom} 57 | 113 | 114 | ${scriptsdom} 115 | 139 | 140 | `; 141 | 142 | return ( 143 |