@jbpark/live-editor 5.0.0 → 6.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +34 -32
- package/README.md +34 -32
- package/dist/{boundary-6_nTsWxP.js → boundary-CySnDUs2.js} +5 -4
- package/dist/boundary-CySnDUs2.js.map +1 -0
- package/dist/{compile-DwOyWrmI.js → compile-my0AjsCw.js} +3 -3
- package/dist/{compile-DwOyWrmI.js.map → compile-my0AjsCw.js.map} +1 -1
- package/dist/{constants-D-XKuEY2.js → constants-st9gnJ5g.js} +1 -4
- package/dist/{constants-D-XKuEY2.js.map → constants-st9gnJ5g.js.map} +1 -1
- package/dist/{context-DDR1xVKo.js → context-Bjq0CvBE.js} +3 -3
- package/dist/{context-DDR1xVKo.js.map → context-Bjq0CvBE.js.map} +1 -1
- package/dist/{core-CKazr4tT.js → core-BNVuH8-l.js} +51 -14
- package/dist/core-BNVuH8-l.js.map +1 -0
- package/dist/{detect-typescript-CoAO-QUJ.js → detect-typescript-CMF0FVKD.js} +2 -2
- package/dist/{detect-typescript-CoAO-QUJ.js.map → detect-typescript-CMF0FVKD.js.map} +1 -1
- package/dist/dnd/index.d.ts +2 -1
- package/dist/dnd/index.js +2 -1
- package/dist/{dnd-BlC1gqUy.js → dnd-COjjjuD7.js} +1242 -1459
- package/dist/dnd-COjjjuD7.js.map +1 -0
- package/dist/{document-DcwIEHRV.js → document-BkGWDZGc.js} +685 -531
- package/dist/document-BkGWDZGc.js.map +1 -0
- package/dist/edit-options-C_r9aCz4.d.ts +100 -0
- package/dist/editor/index.d.ts +1 -1
- package/dist/editor/index.js +2 -2
- package/dist/{editor-Bty5XzpV.js → editor-CvnzrM9J.js} +2 -2
- package/dist/{editor-Bty5XzpV.js.map → editor-CvnzrM9J.js.map} +1 -1
- package/dist/error/index.d.ts +1 -1
- package/dist/error/index.js +2 -2
- package/dist/{error-DyYx2NQ2.js → error-CFzvnLCf.js} +4 -4
- package/dist/error-CFzvnLCf.js.map +1 -0
- package/dist/{index-tAiuHZsC.d.ts → index-BZpZwrxT.d.ts} +2 -2
- package/dist/{index-13BDSrQc.d.ts → index-C93npmTU.d.ts} +4 -6
- package/dist/{index-Cl79M7ZJ.d.ts → index-CqsS207F.d.ts} +2 -10
- package/dist/{index-0VB5C3Jk.d.ts → index-DQcdHg_V.d.ts} +4 -100
- package/dist/{index-kKUjo9lr.d.ts → index-DfIvkht8.d.ts} +3 -3
- package/dist/index.d.ts +8 -7
- package/dist/index.js +6 -6
- package/dist/{messages-Yb3b7YJt.d.ts → messages-BpN3QIhj.d.ts} +4 -1
- package/dist/{messages-Bt5WXKTE.js → messages-DrINcRdG.js} +4 -1
- package/dist/messages-DrINcRdG.js.map +1 -0
- package/dist/preview/index.d.ts +2 -2
- package/dist/preview/index.js +2 -3
- package/dist/{preview-DC0pyOdu.js → preview-8ejC5VP2.js} +16 -13
- package/dist/preview-8ejC5VP2.js.map +1 -0
- package/dist/provider/index.d.ts +2 -2
- package/dist/provider/index.js +2 -2
- package/dist/{sections-4uirsXRU.js → sections-rD8NuRI8.js} +3 -3
- package/dist/{sections-4uirsXRU.js.map → sections-rD8NuRI8.js.map} +1 -1
- package/dist/style.css +801 -5372
- package/dist/ui-kit/index.d.ts +22 -0
- package/dist/ui-kit/index.js +1552 -0
- package/dist/ui-kit/index.js.map +1 -0
- package/dist/ui-kit/style.css +5039 -0
- package/dist/{update-DNInQSM7.js → update-DyFnX8yE.js} +3 -3
- package/dist/{update-DNInQSM7.js.map → update-DyFnX8yE.js.map} +1 -1
- package/dist/{use-dynamic-tailwind-CSYKXDmX.js → use-dynamic-tailwind-0nuRN57N.js} +30 -34
- package/dist/use-dynamic-tailwind-0nuRN57N.js.map +1 -0
- package/dist/use-field-state-iQGW0vc6.js +131 -0
- package/dist/use-field-state-iQGW0vc6.js.map +1 -0
- package/dist/utils/ast/index.js +2 -2
- package/dist/utils/index.js +2 -2
- package/package.json +14 -4
- package/dist/boundary-6_nTsWxP.js.map +0 -1
- package/dist/core-CKazr4tT.js.map +0 -1
- package/dist/dnd-BlC1gqUy.js.map +0 -1
- package/dist/document-DcwIEHRV.js.map +0 -1
- package/dist/error-DyYx2NQ2.js.map +0 -1
- package/dist/messages-Bt5WXKTE.js.map +0 -1
- package/dist/preview-DC0pyOdu.js.map +0 -1
- package/dist/use-dynamic-tailwind-CSYKXDmX.js.map +0 -1
package/README.ko.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
[English](./README.md) | [한국어](./README.ko.md)
|
|
4
4
|
|
|
5
|
-
실시간 프리뷰와 드래그 앤 드롭을 지원하는 인터랙티브 UI 에디터입니다. 캔버스에서의 편집 내용은 AST 변환을 통해 실제 소스 코드에 정확히 반영되며, 결과는 DOM/CSS 격리를 위해 iframe 안에서 렌더링됩니다. React 19와 TypeScript로 구현되었습니다. iframe은 보안 샌드박스가 아닙니다 — [보안 참고사항](
|
|
5
|
+
실시간 프리뷰와 드래그 앤 드롭을 지원하는 인터랙티브 UI 에디터입니다. 캔버스에서의 편집 내용은 AST 변환을 통해 실제 소스 코드에 정확히 반영되며, 결과는 DOM/CSS 격리를 위해 iframe 안에서 렌더링됩니다. React 19와 TypeScript로 구현되었습니다. iframe은 보안 샌드박스가 아닙니다 — [보안 참고사항](#보안-참고사항)을 확인하세요.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
**문서 & 라이브 데모:** https://live-editor-lab.vercel.app
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## 프로젝트 구조
|
|
10
10
|
|
|
11
11
|
```text
|
|
12
12
|
live-editor/
|
|
@@ -36,16 +36,16 @@ live-editor/
|
|
|
36
36
|
|
|
37
37
|
각 부분이 어떻게 맞물리는지(읽기·쓰기 경로, commit 단계, 용어집)는 [ARCHITECTURE.ko.md](./ARCHITECTURE.ko.md)에 있습니다.
|
|
38
38
|
|
|
39
|
-
##
|
|
39
|
+
## 주요 기능
|
|
40
40
|
|
|
41
41
|
- **AST 기반 실시간 코드 반영**: 캔버스 상호작용(추가/이동/삭제, 속성 변경)을 안전하게 소스 코드에 적용합니다.
|
|
42
42
|
- **인터랙티브 속성 패널**: 숫자, 문자열, 불리언, 배열, 객체를 패널에서 바로 편집합니다.
|
|
43
43
|
- **고급 JSX 바인딩 시스템**: 타입 기반 감지를 통해 모든 JSX 요소 속성(children, label, icon 등)을 자동으로 인식하고 편집 가능하게 합니다.
|
|
44
44
|
- **스마트 Items 에디터**: 배열 아이템을 추가/이동/삭제하고, 일반 속성과 중첩된 JSX 컴포넌트를 편집합니다. 순서 변경 시에도 안정적인 컴포넌트 ID를 유지합니다.
|
|
45
|
-
- **프리뷰 런타임**: 컴파일된 결과를 DOM/CSS 격리를 위해 iframe 안에서 렌더링합니다 (보안 샌드박스는 아닙니다 — [보안 참고사항](
|
|
45
|
+
- **프리뷰 런타임**: 컴파일된 결과를 DOM/CSS 격리를 위해 iframe 안에서 렌더링합니다 (보안 샌드박스는 아닙니다 — [보안 참고사항](#보안-참고사항) 참고).
|
|
46
46
|
- **강력한 드래그 앤 드롭**: `@dnd-kit` 기반으로 부드러운 정렬과 배치를 지원합니다.
|
|
47
47
|
|
|
48
|
-
##
|
|
48
|
+
## 설치
|
|
49
49
|
|
|
50
50
|
```bash
|
|
51
51
|
pnpm add @jbpark/live-editor react react-dom
|
|
@@ -55,7 +55,7 @@ pnpm add @jbpark/live-editor react react-dom
|
|
|
55
55
|
peer로, 에디터의 저장 시 포맷(format-on-save)을 쓰려면 함께 설치하세요. 없어도
|
|
56
56
|
에디터는 정상 동작하며 포맷만 건너뜁니다.
|
|
57
57
|
|
|
58
|
-
##
|
|
58
|
+
## 사용법
|
|
59
59
|
|
|
60
60
|
스타일시트를 앱 루트 근처에서 한 번 import하세요 — **필수**이며 JS 엔트리가
|
|
61
61
|
자동으로 불러오지 않습니다:
|
|
@@ -69,12 +69,12 @@ import '@jbpark/live-editor/style.css';
|
|
|
69
69
|
// 필수 — JS 엔트리가 import하지 않음
|
|
70
70
|
|
|
71
71
|
const SAMPLE = `
|
|
72
|
-
import * as ui from 'ui-kit';
|
|
73
|
-
|
|
74
72
|
const App = () => (
|
|
75
73
|
<div className="flex flex-col items-start gap-2 p-6">
|
|
76
|
-
<
|
|
77
|
-
<
|
|
74
|
+
<h3 className="text-xl font-semibold">Hello</h3>
|
|
75
|
+
<button type="button" className="rounded bg-indigo-500 px-3 py-1 text-white">
|
|
76
|
+
Edit me
|
|
77
|
+
</button>
|
|
78
78
|
</div>
|
|
79
79
|
);
|
|
80
80
|
|
|
@@ -96,32 +96,34 @@ export default function Example() {
|
|
|
96
96
|
전체 가이드(Tailwind가 프리뷰에 반영되는 방식, 커스텀 패널 등)는
|
|
97
97
|
[문서 사이트](https://live-editor-lab.vercel.app)를 참고하세요.
|
|
98
98
|
|
|
99
|
-
##
|
|
99
|
+
## 엔트리 포인트
|
|
100
100
|
|
|
101
101
|
패키지는 서브패스 export를 제공해, 한 기능 영역만 필요한 소비자가 나머지를
|
|
102
102
|
번들에 포함하지 않도록 합니다(예: 프리뷰 전용 빌드에는 CodeMirror 미포함) —
|
|
103
103
|
[#194](https://github.com/pjb0811/live-editor/issues/194) 참고:
|
|
104
104
|
|
|
105
|
-
| Import
|
|
106
|
-
|
|
|
107
|
-
| `@jbpark/live-editor`
|
|
108
|
-
| `@jbpark/live-editor/provider`
|
|
109
|
-
| `@jbpark/live-editor/dnd`
|
|
110
|
-
| `@jbpark/live-editor/editor`
|
|
111
|
-
| `@jbpark/live-editor/preview`
|
|
112
|
-
| `@jbpark/live-editor/error`
|
|
113
|
-
| `@jbpark/live-editor/utils`
|
|
114
|
-
| `@jbpark/live-editor/utils/ast`
|
|
115
|
-
| `@jbpark/live-editor/utils/tailwind`
|
|
116
|
-
| `@jbpark/live-editor/
|
|
117
|
-
|
|
118
|
-
|
|
105
|
+
| Import | 내용 |
|
|
106
|
+
| -------------------------------------- | --------------------------------------------------------------------- |
|
|
107
|
+
| `@jbpark/live-editor` | 전체 (`Live` 프로바이더 + 모든 서피스) |
|
|
108
|
+
| `@jbpark/live-editor/provider` | 공유 편집 컨텍스트 프로바이더만 |
|
|
109
|
+
| `@jbpark/live-editor/dnd` | 드래그 앤 드롭 캔버스 + 속성 패널 |
|
|
110
|
+
| `@jbpark/live-editor/editor` | CodeMirror 코드 에디터 |
|
|
111
|
+
| `@jbpark/live-editor/preview` | 격리된 프리뷰 런타임 |
|
|
112
|
+
| `@jbpark/live-editor/error` | 에러 바운더리 |
|
|
113
|
+
| `@jbpark/live-editor/utils` | 컴파일/섹션 헬퍼 |
|
|
114
|
+
| `@jbpark/live-editor/utils/ast` | AST 조작 및 코드 생성 |
|
|
115
|
+
| `@jbpark/live-editor/utils/tailwind` | Tailwind 테마 헬퍼 |
|
|
116
|
+
| `@jbpark/live-editor/ui-kit` | 선택형 `@jbpark/ui-kit` 필드·모듈·팔레트 (`@jbpark/ui-kit` 설치 필요) |
|
|
117
|
+
| `@jbpark/live-editor/ui-kit/style.css` | ui-kit 진입점용 ui-kit 스타일시트 |
|
|
118
|
+
| `@jbpark/live-editor/style.css` | 컴파일된 스타일시트 (필수) |
|
|
119
|
+
|
|
120
|
+
## 보안 참고사항
|
|
119
121
|
|
|
120
122
|
- 프리뷰 `<iframe>`(`src/components/frame/iframe/iframe.tsx`)의 `sandbox` prop은 **실제 보안 경계가 아닙니다**. 컴파일된 프리뷰 코드는 host 페이지 자신의 JS realm에서 `new Function(...)`으로 실행되며(`src/utils/compile.ts`의 `compileModule`), iframe에는 그 결과로 생성된 React 엘리먼트만 `contentDocument`에 포탈되어 DOM/CSS 렌더링 용도로만 쓰입니다. iframe 자체는 사용자 코드를 실행하지 않습니다.
|
|
121
123
|
- 실질적 의미: 미리보기되는 코드는 호스트 애플리케이션과 동일한 수준의 JS 접근 권한(쿠키, DOM, 메모리상의 상태 등)을 가지며, iframe 경계가 이를 막아주지 않습니다.
|
|
122
124
|
- 신뢰할 수 있는 프로젝트만 열어서 편집하세요. 직접 별도의 격리 조치(예: iframe 자체 `contentWindow` realm에서 컴파일을 실행하고 결과를 `postMessage`로 주고받는 방식)를 추가하지 않은 채로, 신뢰할 수 없는 제3자 프로젝트 파일을 미리보기하는 용도로 사용하지 마세요 — 그런 격리는 현재 구현되어 있지 않습니다.
|
|
123
125
|
|
|
124
|
-
##
|
|
126
|
+
## 기술 스택
|
|
125
127
|
|
|
126
128
|
- **Core**: React 19, TypeScript 6, Vite 8
|
|
127
129
|
- **DnD**: `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/modifiers`
|
|
@@ -129,16 +131,16 @@ export default function Example() {
|
|
|
129
131
|
- **UI/스타일**: `@jbpark/ui-kit`, `lucide-react`, Tailwind CSS 4
|
|
130
132
|
- **변환**: Babel(standalone) 기반 브라우저 내 변환
|
|
131
133
|
|
|
132
|
-
##
|
|
134
|
+
## 요구 사항
|
|
133
135
|
|
|
134
136
|
- Peer dependencies: `react >=19`, `react-dom >=19`
|
|
135
137
|
- 패키지 메타데이터는 Babel 8이 지원하는 범위인 Node.js `^22.18.0 || >=24.11.0`을 선언합니다. CI는 Node용 진입점(`utils`, `utils/ast`, `utils/tailwind`, `provider`, `error`)이 Node 22.18에서 로드되는지 확인합니다. 저장소 개발·빌드와 나머지 CI는 `.nvmrc`에 고정된 버전(현재 24.18.0)을 사용합니다.
|
|
136
138
|
- **pnpm**: 10.x 이상 ([Corepack](https://nodejs.org/api/corepack.html)으로 관리)
|
|
137
139
|
|
|
138
|
-
##
|
|
140
|
+
## 개발 (이 저장소)
|
|
139
141
|
|
|
140
142
|
> Live Editor 자체를 개발할 때 참고하세요. 패키지를 앱에서 _사용_ 하려면
|
|
141
|
-
> 위의 [설치](
|
|
143
|
+
> 위의 [설치](#설치)를 보세요. 코드 구조는
|
|
142
144
|
> [ARCHITECTURE.ko.md](./ARCHITECTURE.ko.md)부터 읽기를 권합니다.
|
|
143
145
|
|
|
144
146
|
### pnpm 설정 (권장)
|
|
@@ -191,7 +193,7 @@ pnpm run build:demo
|
|
|
191
193
|
pnpm exec vite preview --outDir dist-demo
|
|
192
194
|
```
|
|
193
195
|
|
|
194
|
-
##
|
|
196
|
+
## 버전 관리 & 배포
|
|
195
197
|
|
|
196
198
|
배포는 [changesets](https://github.com/changesets/changesets)로 완전히 자동화되어 있습니다:
|
|
197
199
|
|
|
@@ -207,6 +209,6 @@ CI 워크플로우:
|
|
|
207
209
|
- `release.yml`: 기존 태그에 대한 GitHub Release를 수동(`workflow_dispatch`)으로 재생성하는 백업 유틸리티
|
|
208
210
|
- Docusaurus 문서 사이트는 `website/`에서 빌드되며 Vercel로 배포됩니다(`vercel.json` 참고).
|
|
209
211
|
|
|
210
|
-
##
|
|
212
|
+
## 라이선스
|
|
211
213
|
|
|
212
214
|
[MIT License](./LICENSE) — Copyright (c) 2026 jbpark
|
package/README.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
[English](./README.md) | [한국어](./README.ko.md)
|
|
4
4
|
|
|
5
|
-
An interactive editor for building UIs with real-time preview and drag‑and‑drop. Canvas edits are synced back to source code via AST transforms, and the result renders inside an iframe for DOM/CSS isolation. Built with React 19 and TypeScript. See [Security Notes](
|
|
5
|
+
An interactive editor for building UIs with real-time preview and drag‑and‑drop. Canvas edits are synced back to source code via AST transforms, and the result renders inside an iframe for DOM/CSS isolation. Built with React 19 and TypeScript. See [Security Notes](#security-notes) — the iframe is not a security sandbox.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
**Documentation & live demos:** https://live-editor-lab.vercel.app
|
|
8
8
|
|
|
9
|
-
##
|
|
9
|
+
## Project Structure
|
|
10
10
|
|
|
11
11
|
```text
|
|
12
12
|
live-editor/
|
|
@@ -37,16 +37,16 @@ live-editor/
|
|
|
37
37
|
How the pieces fit together — the read and write paths, the commit levels and
|
|
38
38
|
a glossary — is in [ARCHITECTURE.md](./ARCHITECTURE.md).
|
|
39
39
|
|
|
40
|
-
##
|
|
40
|
+
## Highlights
|
|
41
41
|
|
|
42
42
|
- **Real-time code updates**: Canvas interactions (add/move/remove, property edits) are propagated back to source code safely.
|
|
43
43
|
- **Interactive property panel**: Edit numbers, strings, booleans, arrays, and objects from the side panel.
|
|
44
44
|
- **Advanced JSX binding system**: Automatically detects and enables editing for all JSX element properties (children, label, icon, etc.) through type-based detection.
|
|
45
45
|
- **Smart Items editor**: Manage array items with add/move/delete operations, edit properties and nested JSX components with stable component identity across reorders.
|
|
46
|
-
- **Preview runtime**: Renders compiled output inside an iframe for DOM/CSS isolation (not a security sandbox — see [Security Notes](
|
|
46
|
+
- **Preview runtime**: Renders compiled output inside an iframe for DOM/CSS isolation (not a security sandbox — see [Security Notes](#security-notes)).
|
|
47
47
|
- **Robust drag-and-drop**: Powered by `@dnd-kit` for smooth sorting and positioning.
|
|
48
48
|
|
|
49
|
-
##
|
|
49
|
+
## Installation
|
|
50
50
|
|
|
51
51
|
```bash
|
|
52
52
|
pnpm add @jbpark/live-editor react react-dom
|
|
@@ -56,7 +56,7 @@ pnpm add @jbpark/live-editor react react-dom
|
|
|
56
56
|
optional peer — install it too if you want the editor's format-on-save; without
|
|
57
57
|
it the editor still works and simply skips formatting.
|
|
58
58
|
|
|
59
|
-
##
|
|
59
|
+
## Usage
|
|
60
60
|
|
|
61
61
|
Import the stylesheet once near your app root — it is **required** and is not
|
|
62
62
|
imported by the JS entry:
|
|
@@ -70,12 +70,12 @@ import '@jbpark/live-editor/style.css';
|
|
|
70
70
|
// required — not imported by the JS entry
|
|
71
71
|
|
|
72
72
|
const SAMPLE = `
|
|
73
|
-
import * as ui from 'ui-kit';
|
|
74
|
-
|
|
75
73
|
const App = () => (
|
|
76
74
|
<div className="flex flex-col items-start gap-2 p-6">
|
|
77
|
-
<
|
|
78
|
-
<
|
|
75
|
+
<h3 className="text-xl font-semibold">Hello</h3>
|
|
76
|
+
<button type="button" className="rounded bg-indigo-500 px-3 py-1 text-white">
|
|
77
|
+
Edit me
|
|
78
|
+
</button>
|
|
79
79
|
</div>
|
|
80
80
|
);
|
|
81
81
|
|
|
@@ -97,32 +97,34 @@ export default function Example() {
|
|
|
97
97
|
See the [documentation site](https://live-editor-lab.vercel.app) for the full
|
|
98
98
|
guide (how Tailwind reaches the preview, custom panels, and more).
|
|
99
99
|
|
|
100
|
-
##
|
|
100
|
+
## Entry points
|
|
101
101
|
|
|
102
102
|
The package ships subpath exports so a consumer who needs only one feature area
|
|
103
103
|
can avoid pulling in the rest (e.g. no CodeMirror in a preview-only build) — see
|
|
104
104
|
[#194](https://github.com/pjb0811/live-editor/issues/194):
|
|
105
105
|
|
|
106
|
-
| Import
|
|
107
|
-
|
|
|
108
|
-
| `@jbpark/live-editor`
|
|
109
|
-
| `@jbpark/live-editor/provider`
|
|
110
|
-
| `@jbpark/live-editor/dnd`
|
|
111
|
-
| `@jbpark/live-editor/editor`
|
|
112
|
-
| `@jbpark/live-editor/preview`
|
|
113
|
-
| `@jbpark/live-editor/error`
|
|
114
|
-
| `@jbpark/live-editor/utils`
|
|
115
|
-
| `@jbpark/live-editor/utils/ast`
|
|
116
|
-
| `@jbpark/live-editor/utils/tailwind`
|
|
117
|
-
| `@jbpark/live-editor/
|
|
118
|
-
|
|
119
|
-
|
|
106
|
+
| Import | Contains |
|
|
107
|
+
| -------------------------------------- | ------------------------------------------------------------------------------ |
|
|
108
|
+
| `@jbpark/live-editor` | Everything (`Live` provider + all surfaces) |
|
|
109
|
+
| `@jbpark/live-editor/provider` | The shared editing context provider only |
|
|
110
|
+
| `@jbpark/live-editor/dnd` | Drag-and-drop canvas + property panel |
|
|
111
|
+
| `@jbpark/live-editor/editor` | CodeMirror code editor |
|
|
112
|
+
| `@jbpark/live-editor/preview` | Isolated preview runtime |
|
|
113
|
+
| `@jbpark/live-editor/error` | Error boundary |
|
|
114
|
+
| `@jbpark/live-editor/utils` | Compile/section helpers |
|
|
115
|
+
| `@jbpark/live-editor/utils/ast` | AST manipulation & code generation |
|
|
116
|
+
| `@jbpark/live-editor/utils/tailwind` | Tailwind theme helpers |
|
|
117
|
+
| `@jbpark/live-editor/ui-kit` | Opt-in `@jbpark/ui-kit` fields, modules and palette (install `@jbpark/ui-kit`) |
|
|
118
|
+
| `@jbpark/live-editor/ui-kit/style.css` | ui-kit's stylesheet, for the ui-kit entry |
|
|
119
|
+
| `@jbpark/live-editor/style.css` | Compiled stylesheet (required) |
|
|
120
|
+
|
|
121
|
+
## Security Notes
|
|
120
122
|
|
|
121
123
|
- The `sandbox` prop on the preview `<iframe>` (`src/components/frame/iframe/iframe.tsx`) is **not a security boundary**. Compiled preview code is executed via `new Function(...)` in the host page's own JS realm (`compileModule` in `src/utils/compile.ts`); only the resulting React elements are portaled into the iframe's `contentDocument` for DOM/CSS rendering. The iframe itself never evaluates user code.
|
|
122
124
|
- Practical implication: previewed code runs with the same JS-level access as the host application (cookies, DOM, in-memory state) — the iframe boundary does not contain it.
|
|
123
125
|
- Only open/edit projects you trust. Don't use this editor to preview arbitrary third-party project files without adding real isolation yourself (e.g. running compilation inside the iframe's own `contentWindow` realm and communicating results back via `postMessage`) — that isolation is not implemented here today.
|
|
124
126
|
|
|
125
|
-
##
|
|
127
|
+
## Tech Stack
|
|
126
128
|
|
|
127
129
|
- **Core**: React 19, TypeScript 6, Vite 8
|
|
128
130
|
- **DnD**: `@dnd-kit/core`, `@dnd-kit/sortable`, `@dnd-kit/modifiers`
|
|
@@ -130,16 +132,16 @@ can avoid pulling in the rest (e.g. no CodeMirror in a preview-only build) — s
|
|
|
130
132
|
- **UI/Styling**: `@jbpark/ui-kit`, `lucide-react`, Tailwind CSS 4
|
|
131
133
|
- **Transform**: Babel (standalone) for in-browser transforms
|
|
132
134
|
|
|
133
|
-
##
|
|
135
|
+
## Requirements
|
|
134
136
|
|
|
135
137
|
- Peer deps: `react >=19`, `react-dom >=19`
|
|
136
138
|
- Package metadata declares Node.js `^22.18.0 || >=24.11.0`, the range Babel 8 supports. CI checks that the entries meant for Node (`utils`, `utils/ast`, `utils/tailwind`, `provider`, `error`) load on Node 22.18. Repository development, builds and the rest of CI use the version pinned in `.nvmrc` (currently 24.18.0).
|
|
137
139
|
- **pnpm** (developing this repo only): the version in `packageManager` (currently 12.8.1), managed via [Corepack](https://nodejs.org/api/corepack.html). Installing the package works with any package manager.
|
|
138
140
|
|
|
139
|
-
##
|
|
141
|
+
## Development (this repo)
|
|
140
142
|
|
|
141
143
|
> Working on Live Editor itself. To _use_ the package in your app, see
|
|
142
|
-
> [Installation](
|
|
144
|
+
> [Installation](#installation) above. Start with
|
|
143
145
|
> [ARCHITECTURE.md](./ARCHITECTURE.md) for how the code is organized.
|
|
144
146
|
|
|
145
147
|
### pnpm Setup (Recommended)
|
|
@@ -193,7 +195,7 @@ pnpm run build:demo
|
|
|
193
195
|
pnpm exec vite preview --outDir dist-demo
|
|
194
196
|
```
|
|
195
197
|
|
|
196
|
-
##
|
|
198
|
+
## Versioning & Release
|
|
197
199
|
|
|
198
200
|
Releases are fully automated via [changesets](https://github.com/changesets/changesets):
|
|
199
201
|
|
|
@@ -210,6 +212,6 @@ CI workflows:
|
|
|
210
212
|
- The Docusaurus documentation site is built from `website/` and deployed via
|
|
211
213
|
Vercel (see `vercel.json`).
|
|
212
214
|
|
|
213
|
-
##
|
|
215
|
+
## License
|
|
214
216
|
|
|
215
217
|
[MIT License](./LICENSE) — Copyright (c) 2026 jbpark
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { i as useLiveMessages, t as MessagesContext } from "./messages-
|
|
1
|
+
import { i as useLiveMessages, t as MessagesContext } from "./messages-DrINcRdG.js";
|
|
2
2
|
import { t as cn } from "./cn-CAnad2vL.js";
|
|
3
3
|
import React from "react";
|
|
4
4
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
//#region src/components/error/error.tsx
|
|
6
|
-
const Error = ({ message, title, className, onReset }) => {
|
|
6
|
+
const Error = ({ message, title, className, style, onReset }) => {
|
|
7
7
|
const messages = useLiveMessages();
|
|
8
8
|
if (!message) return null;
|
|
9
9
|
return /* @__PURE__ */ jsxs("div", {
|
|
@@ -13,7 +13,8 @@ const Error = ({ message, title, className, onReset }) => {
|
|
|
13
13
|
backgroundColor: "#fee2e2",
|
|
14
14
|
border: "1px solid #fca5a5",
|
|
15
15
|
borderRadius: "6px",
|
|
16
|
-
color: "#7f1d1d"
|
|
16
|
+
color: "#7f1d1d",
|
|
17
|
+
...style
|
|
17
18
|
},
|
|
18
19
|
children: [
|
|
19
20
|
/* @__PURE__ */ jsx("h3", {
|
|
@@ -96,4 +97,4 @@ var ErrorBoundary = class extends React.Component {
|
|
|
96
97
|
//#endregion
|
|
97
98
|
export { Error as n, ErrorBoundary as t };
|
|
98
99
|
|
|
99
|
-
//# sourceMappingURL=boundary-
|
|
100
|
+
//# sourceMappingURL=boundary-CySnDUs2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"boundary-CySnDUs2.js","names":[],"sources":["../src/components/error/error.tsx","../src/components/error/boundary.tsx"],"sourcesContent":["import { useLiveMessages } from '~/components/context/messages';\nimport { cn } from '~/utils/cn';\n\nexport interface Props extends React.ComponentPropsWithRef<'div'> {\n message?: string | null;\n title?: string;\n onReset?: () => void;\n}\n\nconst Error = ({ message, title, className, style, onReset }: Props) => {\n const messages = useLiveMessages();\n\n if (!message) {\n return null;\n }\n\n return (\n <div\n className={cn(className)}\n style={{\n padding: '16px',\n backgroundColor: '#fee2e2',\n border: '1px solid #fca5a5',\n borderRadius: '6px',\n color: '#7f1d1d',\n ...style,\n }}\n >\n <h3 style={{ fontSize: '14px', fontWeight: 'bold', margin: 0 }}>\n {title ?? messages.errors.default}\n </h3>\n <pre\n style={{\n marginTop: '8px',\n fontSize: '12px',\n whiteSpace: 'pre-wrap',\n margin: '8px 0 0 0',\n }}\n >\n {message}\n </pre>\n {onReset && (\n <button\n style={{\n marginTop: '8px',\n padding: '4px 12px',\n fontSize: '12px',\n color: 'white',\n backgroundColor: '#dc2626',\n border: 'none',\n borderRadius: '4px',\n cursor: 'pointer',\n }}\n onClick={onReset}\n >\n {messages.errors.tryAgain}\n </button>\n )}\n </div>\n );\n};\n\nexport default Error;\n","import React from 'react';\n\nimport {\n type LiveMessages,\n MessagesContext,\n} from '~/components/context/messages';\n\nimport Error from './error';\n\ninterface Props {\n children: React.ReactNode;\n // `reset` clears the caught error and renders `children` again, the same\n // thing the built-in error's reset button does.\n fallback?: (message?: string, reset?: () => void) => React.ReactNode;\n onError?: (e: Error, info: React.ErrorInfo) => void;\n // When any of these values changes (compared one by one, by identity), a\n // caught error resets and `children` render again, such as when the code\n // that failed is edited.\n resetKeys?: readonly unknown[];\n}\n\ninterface State {\n hasError: boolean;\n error?: Error;\n // The resetKeys the last render saw, so getDerivedStateFromProps can tell\n // when they change.\n resetKeys?: readonly unknown[];\n}\n\nconst keysChanged = (\n prev: readonly unknown[] = [],\n next: readonly unknown[] = [],\n) => prev.length !== next.length || next.some((key, i) => key !== prev[i]);\n\nclass ErrorBoundary extends React.Component<Props, State> {\n static contextType = MessagesContext;\n declare context: LiveMessages;\n\n constructor(props: Props) {\n super(props);\n this.state = { hasError: false, resetKeys: props.resetKeys };\n }\n\n static getDerivedStateFromError(error: Error): Partial<State> {\n return { hasError: true, error };\n }\n\n componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {\n console.error('🚨 [Boundary] Rendering error:', error, errorInfo);\n this.props.onError?.(error, errorInfo);\n }\n\n // Clears a caught error in the same render that sees new resetKeys, so\n // `children` get their next chance right away and the fallback doesn't\n // render (and run its effects) once more first (#442). If `children` throw\n // again with the new keys, getDerivedStateFromError catches that in this\n // same render pass.\n static getDerivedStateFromProps(\n props: Props,\n state: State,\n ): Partial<State> | null {\n if (!keysChanged(state.resetKeys, props.resetKeys)) {\n return null;\n }\n\n return state.hasError\n ? { hasError: false, error: undefined, resetKeys: props.resetKeys }\n : { resetKeys: props.resetKeys };\n }\n\n reset = () => this.setState({ hasError: false, error: undefined });\n\n render() {\n if (this.state.hasError) {\n return this.props.fallback ? (\n <>{this.props.fallback(this.state.error?.message, this.reset)}</>\n ) : (\n <Error\n message={this.state.error?.message}\n onReset={this.reset}\n title={this.context.errors.rendering}\n />\n );\n }\n\n return this.props.children;\n }\n}\n\nexport default ErrorBoundary;\n"],"mappings":";;;;;AASA,MAAM,SAAS,EAAE,SAAS,OAAO,WAAW,OAAO,cAAqB;CACtE,MAAM,WAAW,gBAAgB;CAEjC,IAAI,CAAC,SACH,OAAO;CAGT,OACE,qBAAC,OAAD;EACE,WAAW,GAAG,SAAS;EACvB,OAAO;GACL,SAAS;GACT,iBAAiB;GACjB,QAAQ;GACR,cAAc;GACd,OAAO;GACP,GAAG;EACL;EATF,UAAA;GAWE,oBAAC,MAAD;IAAI,OAAO;KAAE,UAAU;KAAQ,YAAY;KAAQ,QAAQ;IAAE;IAC1D,UAAA,SAAS,SAAS,OAAO;GACxB,CAAA;GACJ,oBAAC,OAAD;IACE,OAAO;KACL,WAAW;KACX,UAAU;KACV,YAAY;KACZ,QAAQ;IACV;IAEC,UAAA;GACE,CAAA;GACJ,WACC,oBAAC,UAAD;IACE,OAAO;KACL,WAAW;KACX,SAAS;KACT,UAAU;KACV,OAAO;KACP,iBAAiB;KACjB,QAAQ;KACR,cAAc;KACd,QAAQ;IACV;IACA,SAAS;IAER,UAAA,SAAS,OAAO;GACX,CAAA;EAEP;;AAET;;;AC/BA,MAAM,eACJ,OAA2B,CAAC,GAC5B,OAA2B,CAAC,MACzB,KAAK,WAAW,KAAK,UAAU,KAAK,MAAM,KAAK,MAAM,QAAQ,KAAK,EAAE;AAEzE,IAAM,gBAAN,cAA4B,MAAM,UAAwB;CACxD,OAAO,cAAc;CAGrB,YAAY,OAAc;EACxB,MAAM,KAAK;EACX,KAAK,QAAQ;GAAE,UAAU;GAAO,WAAW,MAAM;EAAU;CAC7D;CAEA,OAAO,yBAAyB,OAA8B;EAC5D,OAAO;GAAE,UAAU;GAAM;EAAM;CACjC;CAEA,kBAAkB,OAAc,WAA4B;EAC1D,QAAQ,MAAM,kCAAkC,OAAO,SAAS;EAChE,KAAK,MAAM,UAAU,OAAO,SAAS;CACvC;CAOA,OAAO,yBACL,OACA,OACuB;EACvB,IAAI,CAAC,YAAY,MAAM,WAAW,MAAM,SAAS,GAC/C,OAAO;EAGT,OAAO,MAAM,WACT;GAAE,UAAU;GAAO,OAAO,KAAA;GAAW,WAAW,MAAM;EAAU,IAChE,EAAE,WAAW,MAAM,UAAU;CACnC;CAEA,cAAc,KAAK,SAAS;EAAE,UAAU;EAAO,OAAO,KAAA;CAAU,CAAC;CAEjE,SAAS;EACP,IAAI,KAAK,MAAM,UACb,OAAO,KAAK,MAAM,WAChB,oBAAA,UAAA,EAAA,UAAG,KAAK,MAAM,SAAS,KAAK,MAAM,OAAO,SAAS,KAAK,KAAK,EAAI,CAAA,IAEhE,oBAAC,OAAD;GACE,SAAS,KAAK,MAAM,OAAO;GAC3B,SAAS,KAAK;GACd,OAAO,KAAK,QAAQ,OAAO;EAC5B,CAAA;EAIL,OAAO,KAAK,MAAM;CACpB;AACF"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { n as CONFIG } from "./constants-
|
|
1
|
+
import { n as CONFIG } from "./constants-st9gnJ5g.js";
|
|
2
2
|
import { n as registerEditorCache } from "./editor-caches-ClMvdcQF.js";
|
|
3
3
|
import { t as createBoundedCache } from "./cache-B0-XcIxJ.js";
|
|
4
|
-
import { t as detectTypeScript } from "./detect-typescript-
|
|
4
|
+
import { t as detectTypeScript } from "./detect-typescript-CMF0FVKD.js";
|
|
5
5
|
import React from "react";
|
|
6
6
|
import * as Babel from "@babel/standalone";
|
|
7
7
|
//#region src/utils/scripts.ts
|
|
@@ -133,4 +133,4 @@ registerEditorCache(clearCompilationCache);
|
|
|
133
133
|
//#endregion
|
|
134
134
|
export { withScriptBlobs as i, compile as n, preloadScripts as r, clearCompilationCache as t };
|
|
135
135
|
|
|
136
|
-
//# sourceMappingURL=compile-
|
|
136
|
+
//# sourceMappingURL=compile-my0AjsCw.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compile-DwOyWrmI.js","names":[],"sources":["../src/utils/scripts.ts","../src/utils/compile.ts"],"sourcesContent":["import { CONFIG } from '../constants';\nimport { createBoundedCache } from './cache';\nimport { registerEditorCache } from './editor-caches';\n\n// How many callers have requested each script and not yet injected it. A\n// blob URL evicted in that time isn't revoked until the last of them is done\n// (`deferredRevocations`), or its `<script>` would fail to load without an\n// error (#443).\nconst pins = new Map<string, number>();\nconst deferredRevocations = new Map<string, string[]>();\n\nconst revoke = (src: string, blobUrl: string) => {\n if (!pins.has(src)) {\n URL.revokeObjectURL(blobUrl);\n return;\n }\n\n deferredRevocations.set(src, [\n ...(deferredRevocations.get(src) ?? []),\n blobUrl,\n ]);\n};\n\nconst pin = (src: string) => {\n pins.set(src, (pins.get(src) ?? 0) + 1);\n};\n\nconst unpin = (src: string) => {\n const count = (pins.get(src) ?? 0) - 1;\n\n if (count > 0) {\n pins.set(src, count);\n return;\n }\n\n pins.delete(src);\n deferredRevocations.get(src)?.forEach(blobUrl => {\n URL.revokeObjectURL(blobUrl);\n });\n deferredRevocations.delete(src);\n};\n\nconst scriptCache = createBoundedCache<string, string>(\n CONFIG.SCRIPT_CACHE_LIMIT,\n revoke,\n);\nconst loadingScriptCache = new Map<string, Promise<string>>();\n\nexport const getCachedScriptBlob = async (src: string): Promise<string> => {\n const cached = scriptCache.get(src);\n\n if (cached) {\n return cached;\n }\n\n const existing = loadingScriptCache.get(src);\n\n if (existing) {\n return existing;\n }\n\n const promise = fetch(src)\n .then(res => res.text())\n .then(text => {\n const blob = new Blob([text], { type: 'application/javascript' });\n const blobUrl = URL.createObjectURL(blob);\n\n scriptCache.set(src, blobUrl);\n\n return blobUrl;\n })\n // Clean up after a failure too: a rejected promise left here would be\n // reused by every later caller, so the script could never load again.\n .finally(() => {\n loadingScriptCache.delete(src);\n });\n\n loadingScriptCache.set(src, promise);\n\n return promise;\n};\n\n// Resolves every script to a blob URL and hands them to `inject` while none\n// of them can be revoked. `inject` must use them at once, as appending\n// `<script>` elements does: the browser resolves a blob URL on insertion.\n// Several scripts, or several frames, can push more scripts through the cache\n// than it holds before the last one arrives (#443).\nexport const withScriptBlobs = async (\n scripts: string[],\n inject: (blobUrls: string[]) => void,\n): Promise<void> => {\n scripts.forEach(pin);\n\n try {\n inject(await Promise.all(scripts.map(getCachedScriptBlob)));\n } finally {\n scripts.forEach(unpin);\n }\n};\n\nexport const preloadScripts = (scripts: string[]): void => {\n scripts.forEach(src => {\n // Not awaited, so the rejection is handled here. The real load reports a\n // failure to whoever awaits it.\n getCachedScriptBlob(src).catch(() => {});\n });\n};\n\n// Revokes every blob URL, through the same `onEvict` the LRU uses, and drops\n// the pending loads, so the next session doesn't get a URL created before\n// the revocation.\nexport const clearScriptCache = () => {\n scriptCache.clear();\n loadingScriptCache.clear();\n};\n\nregisterEditorCache(clearScriptCache);\n","import React from 'react';\n\nimport * as Babel from '@babel/standalone';\n\nimport type { Module } from '~/types';\n\nimport { CONFIG } from '../constants';\nimport { createBoundedCache } from './cache';\nimport { detectTypeScript } from './detect-typescript';\nimport { registerEditorCache } from './editor-caches';\n\ninterface CompilationKey {\n code: string;\n modules: [string, unknown][];\n}\n\n// The keys still in the LRU. Comparing at most `CACHE_LIMIT` of them avoids\n// keeping every module object ever seen, or serializing modules, which can be\n// cyclic or closures.\nconst compilationKeys = new Set<CompilationKey>();\nconst compilationCache = createBoundedCache<CompilationKey, Module>(\n CONFIG.CACHE_LIMIT,\n key => compilationKeys.delete(key),\n);\n\nconst createCacheKey = (\n code: string,\n modules: Record<string, unknown>,\n): CompilationKey => ({\n code,\n modules: Object.keys(modules)\n .sort()\n .map(name => [name, modules[name]]),\n});\n\n// Objects/functions compare by reference; primitives compare by value.\n// Replace a module object when its implementation changes. In-place edits\n// inside an existing module are intentionally not observed by this cache.\nexport const compile = (\n code: string,\n modules: Record<string, unknown>,\n): Module => {\n const candidate = createCacheKey(code, modules);\n\n for (const key of compilationKeys) {\n if (\n key.code === candidate.code &&\n key.modules.length === candidate.modules.length &&\n key.modules.every(\n ([name, value], index) =>\n name === candidate.modules[index]![0] &&\n Object.is(value, candidate.modules[index]![1]),\n )\n ) {\n return compilationCache.get(key)!;\n }\n }\n\n const result = compileModule(code, Object.fromEntries(candidate.modules));\n\n compilationCache.set(candidate, result);\n compilationKeys.add(candidate);\n\n return result;\n};\n\n// `compilationKeys` needs no separate pass: it is drained by the `onEvict`\n// the cache was built with, which `clear()` runs for every entry.\nexport const clearCompilationCache = () => {\n compilationCache.clear();\n};\n\n// JSX compiles to `React.createElement` (the classic runtime). The module runs\n// with `React` in scope and has no `react/jsx-runtime` to import.\nconst REACT_PRESET: [string, object] = ['react', { runtime: 'classic' }];\n\n// Transforms JSX and TypeScript with `@babel/standalone`'s presets, without\n// the `typescript` package (#192). Babel works per file without types, so a\n// `const enum` becomes a real enum and legacy decorators aren't supported.\n// On an error, the input comes back unchanged.\nconst transformCode = (code: string, isTypeScript = false): string => {\n try {\n const result = Babel.transform(code, {\n filename: isTypeScript ? 'preview.tsx' : 'preview.jsx',\n presets: isTypeScript\n ? ['typescript', 'env', REACT_PRESET]\n : ['env', REACT_PRESET],\n sourceType: 'module',\n plugins: [Babel.availablePlugins['transform-modules-commonjs']],\n })?.code;\n\n return result || '';\n } catch (e) {\n console.error('❌ Babel transformation error:', e);\n return code;\n }\n};\n\nconst compileModule = (\n code: string,\n modules: Record<string, unknown>,\n): Module => {\n const isTypeScript = detectTypeScript(code);\n\n type RenderFunction = (\n exports: Module['exports'],\n require: (name: string) => unknown,\n module: Module,\n React: typeof import('react'),\n ) => void;\n\n let render: RenderFunction;\n\n // Runs in this window's own JS realm, not inside the preview iframe — the iframe only\n // renders the resulting React elements via portal. See README \"Security Notes\".\n try {\n render = new Function(\n 'exports',\n 'require',\n 'module',\n 'React',\n transformCode(code, isTypeScript),\n ) as RenderFunction;\n } catch (e) {\n const errorMessage = e instanceof Error ? e.message : 'Syntax error';\n return { exports: {}, error: errorMessage };\n }\n\n const module: Module = { exports: {} };\n\n const customRequire = (name: string) => {\n if (name === 'react') {\n return React;\n }\n\n // Check presence, not truthiness: a module can be a falsy value (`0`, `''`,\n // `false`, `null`).\n if (name in modules) {\n return modules[name];\n }\n\n throw new Error(`Module not found: ${name}`);\n };\n\n try {\n render(module.exports, customRequire, module, React);\n } catch (e) {\n const errorMessage = e instanceof Error ? e.message : 'Runtime error';\n return { exports: {}, error: errorMessage };\n }\n\n return module;\n};\n\nregisterEditorCache(clearCompilationCache);\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"compile-my0AjsCw.js","names":[],"sources":["../src/utils/scripts.ts","../src/utils/compile.ts"],"sourcesContent":["import { CONFIG } from '../constants';\nimport { createBoundedCache } from './cache';\nimport { registerEditorCache } from './editor-caches';\n\n// How many callers have requested each script and not yet injected it. A\n// blob URL evicted in that time isn't revoked until the last of them is done\n// (`deferredRevocations`), or its `<script>` would fail to load without an\n// error (#443).\nconst pins = new Map<string, number>();\nconst deferredRevocations = new Map<string, string[]>();\n\nconst revoke = (src: string, blobUrl: string) => {\n if (!pins.has(src)) {\n URL.revokeObjectURL(blobUrl);\n return;\n }\n\n deferredRevocations.set(src, [\n ...(deferredRevocations.get(src) ?? []),\n blobUrl,\n ]);\n};\n\nconst pin = (src: string) => {\n pins.set(src, (pins.get(src) ?? 0) + 1);\n};\n\nconst unpin = (src: string) => {\n const count = (pins.get(src) ?? 0) - 1;\n\n if (count > 0) {\n pins.set(src, count);\n return;\n }\n\n pins.delete(src);\n deferredRevocations.get(src)?.forEach(blobUrl => {\n URL.revokeObjectURL(blobUrl);\n });\n deferredRevocations.delete(src);\n};\n\nconst scriptCache = createBoundedCache<string, string>(\n CONFIG.SCRIPT_CACHE_LIMIT,\n revoke,\n);\nconst loadingScriptCache = new Map<string, Promise<string>>();\n\nexport const getCachedScriptBlob = async (src: string): Promise<string> => {\n const cached = scriptCache.get(src);\n\n if (cached) {\n return cached;\n }\n\n const existing = loadingScriptCache.get(src);\n\n if (existing) {\n return existing;\n }\n\n const promise = fetch(src)\n .then(res => res.text())\n .then(text => {\n const blob = new Blob([text], { type: 'application/javascript' });\n const blobUrl = URL.createObjectURL(blob);\n\n scriptCache.set(src, blobUrl);\n\n return blobUrl;\n })\n // Clean up after a failure too: a rejected promise left here would be\n // reused by every later caller, so the script could never load again.\n .finally(() => {\n loadingScriptCache.delete(src);\n });\n\n loadingScriptCache.set(src, promise);\n\n return promise;\n};\n\n// Resolves every script to a blob URL and hands them to `inject` while none\n// of them can be revoked. `inject` must use them at once, as appending\n// `<script>` elements does: the browser resolves a blob URL on insertion.\n// Several scripts, or several frames, can push more scripts through the cache\n// than it holds before the last one arrives (#443).\nexport const withScriptBlobs = async (\n scripts: string[],\n inject: (blobUrls: string[]) => void,\n): Promise<void> => {\n scripts.forEach(pin);\n\n try {\n inject(await Promise.all(scripts.map(getCachedScriptBlob)));\n } finally {\n scripts.forEach(unpin);\n }\n};\n\nexport const preloadScripts = (scripts: string[]): void => {\n scripts.forEach(src => {\n // Not awaited, so the rejection is handled here. The real load reports a\n // failure to whoever awaits it.\n getCachedScriptBlob(src).catch(() => {});\n });\n};\n\n// Revokes every blob URL, through the same `onEvict` the LRU uses, and drops\n// the pending loads, so the next session doesn't get a URL created before\n// the revocation.\nexport const clearScriptCache = () => {\n scriptCache.clear();\n loadingScriptCache.clear();\n};\n\nregisterEditorCache(clearScriptCache);\n","import React from 'react';\n\nimport * as Babel from '@babel/standalone';\n\nimport type { Module } from '~/types';\n\nimport { CONFIG } from '../constants';\nimport { createBoundedCache } from './cache';\nimport { detectTypeScript } from './detect-typescript';\nimport { registerEditorCache } from './editor-caches';\n\ninterface CompilationKey {\n code: string;\n modules: [string, unknown][];\n}\n\n// The keys still in the LRU. Comparing at most `CACHE_LIMIT` of them avoids\n// keeping every module object ever seen, or serializing modules, which can be\n// cyclic or closures.\nconst compilationKeys = new Set<CompilationKey>();\nconst compilationCache = createBoundedCache<CompilationKey, Module>(\n CONFIG.CACHE_LIMIT,\n key => compilationKeys.delete(key),\n);\n\nconst createCacheKey = (\n code: string,\n modules: Record<string, unknown>,\n): CompilationKey => ({\n code,\n modules: Object.keys(modules)\n .sort()\n .map(name => [name, modules[name]]),\n});\n\n// Objects/functions compare by reference; primitives compare by value.\n// Replace a module object when its implementation changes. In-place edits\n// inside an existing module are intentionally not observed by this cache.\nexport const compile = (\n code: string,\n modules: Record<string, unknown>,\n): Module => {\n const candidate = createCacheKey(code, modules);\n\n for (const key of compilationKeys) {\n if (\n key.code === candidate.code &&\n key.modules.length === candidate.modules.length &&\n key.modules.every(\n ([name, value], index) =>\n name === candidate.modules[index]![0] &&\n Object.is(value, candidate.modules[index]![1]),\n )\n ) {\n return compilationCache.get(key)!;\n }\n }\n\n const result = compileModule(code, Object.fromEntries(candidate.modules));\n\n compilationCache.set(candidate, result);\n compilationKeys.add(candidate);\n\n return result;\n};\n\n// `compilationKeys` needs no separate pass: it is drained by the `onEvict`\n// the cache was built with, which `clear()` runs for every entry.\nexport const clearCompilationCache = () => {\n compilationCache.clear();\n};\n\n// JSX compiles to `React.createElement` (the classic runtime). The module runs\n// with `React` in scope and has no `react/jsx-runtime` to import.\nconst REACT_PRESET: [string, object] = ['react', { runtime: 'classic' }];\n\n// Transforms JSX and TypeScript with `@babel/standalone`'s presets, without\n// the `typescript` package (#192). Babel works per file without types, so a\n// `const enum` becomes a real enum and legacy decorators aren't supported.\n// On an error, the input comes back unchanged.\nconst transformCode = (code: string, isTypeScript = false): string => {\n try {\n const result = Babel.transform(code, {\n filename: isTypeScript ? 'preview.tsx' : 'preview.jsx',\n presets: isTypeScript\n ? ['typescript', 'env', REACT_PRESET]\n : ['env', REACT_PRESET],\n sourceType: 'module',\n plugins: [Babel.availablePlugins['transform-modules-commonjs']],\n })?.code;\n\n return result || '';\n } catch (e) {\n console.error('❌ Babel transformation error:', e);\n return code;\n }\n};\n\nconst compileModule = (\n code: string,\n modules: Record<string, unknown>,\n): Module => {\n const isTypeScript = detectTypeScript(code);\n\n type RenderFunction = (\n exports: Module['exports'],\n require: (name: string) => unknown,\n module: Module,\n React: typeof import('react'),\n ) => void;\n\n let render: RenderFunction;\n\n // Runs in this window's own JS realm, not inside the preview iframe — the iframe only\n // renders the resulting React elements via portal. See README \"Security Notes\".\n try {\n render = new Function(\n 'exports',\n 'require',\n 'module',\n 'React',\n transformCode(code, isTypeScript),\n ) as RenderFunction;\n } catch (e) {\n const errorMessage = e instanceof Error ? e.message : 'Syntax error';\n return { exports: {}, error: errorMessage };\n }\n\n const module: Module = { exports: {} };\n\n const customRequire = (name: string) => {\n if (name === 'react') {\n return React;\n }\n\n // Check presence, not truthiness: a module can be a falsy value (`0`, `''`,\n // `false`, `null`).\n if (name in modules) {\n return modules[name];\n }\n\n throw new Error(`Module not found: ${name}`);\n };\n\n try {\n render(module.exports, customRequire, module, React);\n } catch (e) {\n const errorMessage = e instanceof Error ? e.message : 'Runtime error';\n return { exports: {}, error: errorMessage };\n }\n\n return module;\n};\n\nregisterEditorCache(clearCompilationCache);\n"],"mappings":"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"}
|
|
@@ -40,9 +40,6 @@ const BINDING_PROP = {
|
|
|
40
40
|
};
|
|
41
41
|
const DEFAULT_CONTAINER_ID = "app-container";
|
|
42
42
|
const documentTemplate = (containerId) => `
|
|
43
|
-
import * as ui from 'ui-kit';
|
|
44
|
-
import { cn } from 'ui-kit/utils';
|
|
45
|
-
|
|
46
43
|
const App = () => {
|
|
47
44
|
return (
|
|
48
45
|
<main id="${containerId}"></main>
|
|
@@ -55,4 +52,4 @@ const DEFAULT_TEMPLATE = documentTemplate(DEFAULT_CONTAINER_ID);
|
|
|
55
52
|
//#endregion
|
|
56
53
|
export { DEFAULT_TEMPLATE as a, TS_PATTERNS as c, DEFAULT_CONTAINER_ID as i, documentTemplate as l, CONFIG as n, REGEX as o, DATA_ATTR as r, RESERVED_BINDING_PROPERTIES as s, BINDING_PROP as t };
|
|
57
54
|
|
|
58
|
-
//# sourceMappingURL=constants-
|
|
55
|
+
//# sourceMappingURL=constants-st9gnJ5g.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"constants-
|
|
1
|
+
{"version":3,"file":"constants-st9gnJ5g.js","names":[],"sources":["../src/constants/index.ts"],"sourcesContent":["export const STORAGE_KEY = 'live-editor-code';\n\nexport const CONFIG = {\n CACHE_LIMIT: 50,\n // The document parse cache. Its entries are whole Babel ASTs (about\n // 0.66 MB per document), and only the current and previous version get hits,\n // so it keeps few (#106).\n DOCUMENT_CACHE_LIMIT: 4,\n // The script cache (`utils/scripts.ts`). Its entries are live blob URLs,\n // revoked on eviction, and a preview loads only a few scripts (#104).\n SCRIPT_CACHE_LIMIT: 10,\n} as const;\n\nexport const DATA_ATTR = {\n ID: 'data-id',\n BINDING: 'data-binding',\n // Names an entry of `Live.Dnd`'s `bindingKeys` instead of declaring the\n // bindings inline (#513).\n BINDING_KEY: 'data-binding-key',\n ITEM: 'data-item',\n} as const;\n\n// Attributes the editor owns. `data-id` links the canvas to the source,\n// `data-name` is a section's name, and `data-binding` and `data-binding-key`\n// declare what each edit is checked against. A binding can't target them,\n// since rewriting one would break that link (#429).\nexport const RESERVED_BINDING_PROPERTIES: readonly string[] = [\n DATA_ATTR.ID,\n 'data-name',\n DATA_ATTR.BINDING,\n DATA_ATTR.BINDING_KEY,\n];\n\nexport const REGEX = {\n NUMBER: /^\\d+(\\.\\d+)?$/,\n BOOLEAN_OR_NULL: /^(true|false|null|undefined)$/,\n MODULE_IMPORT_EXPORT_LINE:\n /^\\s*(?:import|export)\\b[^\\n]*\\bfrom\\s+['\"][^'\"]*['\"];?\\s*$/gm,\n} as const;\n\nexport const TS_PATTERNS = [\n /interface\\s+\\w+/,\n /type\\s+\\w+\\s*=/,\n /:\\s*\\w+(\\[\\])?(\\s*\\||\\s*&|\\s*=|\\s*;|\\s*,|\\s*\\))/,\n /as\\s+\\w+/,\n /<[A-Z]\\w*>/,\n /enum\\s+\\w+/,\n /public\\s+|private\\s+|protected\\s+/,\n /readonly\\s+/,\n /\\?\\s*:/,\n] as const;\n\nexport const BINDING_PROP = {\n CHILDREN: 'children',\n INNER_TEXT: 'innerText',\n INNER_HTML: 'innerHTML',\n ITEMS: 'items',\n} as const;\n\n// The element whose `<section>` children are a document's sections, unless\n// `Live.Dnd`'s `containerId` says otherwise (#449).\nexport const DEFAULT_CONTAINER_ID = 'app-container';\n\n// A new, empty document built around `containerId`. `createDocument()` in\n// `@jbpark/live-editor/utils` is the public way in; this is the template\n// behind it and behind `DEFAULT_TEMPLATE`.\nexport const documentTemplate = (containerId: string) => `\nconst App = () => {\n return (\n <main id=\"${containerId}\"></main>\n )\n}\n\nexport default App;\n`;\n\nexport const DEFAULT_TEMPLATE = documentTemplate(DEFAULT_CONTAINER_ID);\n\nexport { PALETTE_SECTIONS } from './palette-sections';\n"],"mappings":";AAEA,MAAa,SAAS;CACpB,aAAa;CAIb,sBAAsB;CAGtB,oBAAoB;AACtB;AAEA,MAAa,YAAY;CACvB,IAAI;CACJ,SAAS;CAGT,aAAa;CACb,MAAM;AACR;AAMA,MAAa,8BAAiD;CAC5D,UAAU;CACV;CACA,UAAU;CACV,UAAU;AACZ;AAEA,MAAa,QAAQ;CACnB,QAAQ;CACR,iBAAiB;CACjB,2BACE;AACJ;AAEA,MAAa,cAAc;CACzB;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAEA,MAAa,eAAe;CAC1B,UAAU;CACV,YAAY;CACZ,YAAY;CACZ,OAAO;AACT;AAIA,MAAa,uBAAuB;AAKpC,MAAa,oBAAoB,gBAAwB;;;gBAGzC,YAAY;;;;;;AAO5B,MAAa,mBAAmB,iBAAiB,oBAAoB"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { a as DEFAULT_TEMPLATE } from "./constants-
|
|
1
|
+
import { a as DEFAULT_TEMPLATE } from "./constants-st9gnJ5g.js";
|
|
2
2
|
import { r as registerEditorSession } from "./editor-caches-ClMvdcQF.js";
|
|
3
|
-
import { r as mergeMessages, t as MessagesContext } from "./messages-
|
|
3
|
+
import { r as mergeMessages, t as MessagesContext } from "./messages-DrINcRdG.js";
|
|
4
4
|
import { n as PreviewContext, t as ErrorContext } from "./states-Ci1AvoQ9.js";
|
|
5
5
|
import { useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
6
6
|
import { jsx } from "react/jsx-runtime";
|
|
@@ -37,4 +37,4 @@ const ContextProvider = ({ children, messages }) => {
|
|
|
37
37
|
//#endregion
|
|
38
38
|
export { ContextProvider as t };
|
|
39
39
|
|
|
40
|
-
//# sourceMappingURL=context-
|
|
40
|
+
//# sourceMappingURL=context-Bjq0CvBE.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context-
|
|
1
|
+
{"version":3,"file":"context-Bjq0CvBE.js","names":[],"sources":["../src/components/context/context.tsx"],"sourcesContent":["'use client';\n\nimport React, {\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useState,\n} from 'react';\n\nimport { DEFAULT_TEMPLATE } from '~/constants';\nimport { registerEditorSession } from '~/utils/editor-caches';\n\nimport {\n type LiveMessagesInput,\n MessagesContext,\n mergeMessages,\n} from './messages';\nimport type { ErrorContextType, PreviewContextType } from './states';\nimport { ErrorContext, PreviewContext } from './states';\n\nexport interface Props {\n children?: React.ReactNode;\n // Replaces any of the editor's text, group by group; the rest stays\n // English, or the enclosing `Live`'s. Define it outside render or memoize\n // it: a new object re-renders everything that shows text (#524).\n messages?: LiveMessagesInput;\n}\n\nconst ContextProvider = ({ children, messages }: Props) => {\n const [code, setCodeState] = useState(DEFAULT_TEMPLATE);\n const [error, setError] = useState<ErrorContextType['error']>(null);\n\n // Clears the last error in the same update that sets the code. An effect\n // would run after the new code's own error was reported, and wipe it.\n const setCode = useCallback<PreviewContextType['setCode']>(next => {\n setError(null);\n setCodeState(next);\n }, []);\n\n // Counts this editor, so the page-wide caches are released when the last\n // one unmounts (`utils/editor-caches.ts`).\n useEffect(() => registerEditorSession(), []);\n\n // Memoized, so a parent re-render doesn't re-render every `usePreview()`\n // and `useError()` consumer.\n const previewValue = useMemo<PreviewContextType>(\n () => ({ code, setCode }),\n [code, setCode],\n );\n\n const errorValue = useMemo<ErrorContextType>(\n () => ({ error, setError }),\n [error, setError],\n );\n\n // A provider inside another one starts from the outer one's messages, so\n // a page can set them once around several editors.\n const inherited = useContext(MessagesContext);\n const messagesValue = useMemo(\n () => mergeMessages(messages, inherited),\n [messages, inherited],\n );\n\n return (\n <PreviewContext.Provider value={previewValue}>\n <ErrorContext.Provider value={errorValue}>\n <MessagesContext.Provider value={messagesValue}>\n {children}\n </MessagesContext.Provider>\n </ErrorContext.Provider>\n </PreviewContext.Provider>\n );\n};\n\nexport default ContextProvider;\n"],"mappings":";;;;;;;AA6BA,MAAM,mBAAmB,EAAE,UAAU,eAAsB;CACzD,MAAM,CAAC,MAAM,gBAAgB,SAAS,gBAAgB;CACtD,MAAM,CAAC,OAAO,YAAY,SAAoC,IAAI;CAIlE,MAAM,UAAU,aAA2C,SAAQ;EACjE,SAAS,IAAI;EACb,aAAa,IAAI;CACnB,GAAG,CAAC,CAAC;CAIL,gBAAgB,sBAAsB,GAAG,CAAC,CAAC;CAI3C,MAAM,eAAe,eACZ;EAAE;EAAM;CAAQ,IACvB,CAAC,MAAM,OAAO,CAChB;CAEA,MAAM,aAAa,eACV;EAAE;EAAO;CAAS,IACzB,CAAC,OAAO,QAAQ,CAClB;CAIA,MAAM,YAAY,WAAW,eAAe;CAC5C,MAAM,gBAAgB,cACd,cAAc,UAAU,SAAS,GACvC,CAAC,UAAU,SAAS,CACtB;CAEA,OACE,oBAAC,eAAe,UAAhB;EAAyB,OAAO;EAC9B,UAAA,oBAAC,aAAa,UAAd;GAAuB,OAAO;GAC5B,UAAA,oBAAC,gBAAgB,UAAjB;IAA0B,OAAO;IAC9B;GACuB,CAAA;EACL,CAAA;CACA,CAAA;AAE7B"}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { t as cn } from "./cn-CAnad2vL.js";
|
|
2
|
-
import { t as detectTypeScript } from "./detect-typescript-
|
|
3
|
-
import { useCallback, useMemo } from "react";
|
|
2
|
+
import { t as detectTypeScript } from "./detect-typescript-CMF0FVKD.js";
|
|
3
|
+
import { useCallback, useMemo, useRef } from "react";
|
|
4
4
|
import { jsx } from "react/jsx-runtime";
|
|
5
|
-
import
|
|
5
|
+
import { javascript } from "@codemirror/lang-javascript";
|
|
6
6
|
import { vscodeLight } from "@uiw/codemirror-theme-vscode";
|
|
7
|
-
import "@uiw/react-codemirror";
|
|
7
|
+
import CodeMirror, { EditorView } from "@uiw/react-codemirror";
|
|
8
8
|
//#region src/components/editor/use-format-code.ts
|
|
9
9
|
const loadPrettier = async () => {
|
|
10
10
|
const [prettier, babel, estree, typescript] = await Promise.all([
|
|
@@ -55,24 +55,61 @@ const useFormatCode = ({ fragment, prettierOptions } = {}) => {
|
|
|
55
55
|
};
|
|
56
56
|
//#endregion
|
|
57
57
|
//#region src/components/editor/core.tsx
|
|
58
|
+
const EXTENSIONS = [javascript({
|
|
59
|
+
jsx: true,
|
|
60
|
+
typescript: true
|
|
61
|
+
}), EditorView.lineWrapping];
|
|
58
62
|
const Core = ({ value, theme, height, className, prettierOptions, fragment, raw, onChange, onSave, onError, ...props }) => {
|
|
59
63
|
const formatCode = useFormatCode({
|
|
60
64
|
fragment,
|
|
61
65
|
prettierOptions
|
|
62
66
|
});
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
+
const editorRef = useRef(null);
|
|
68
|
+
const save = async () => {
|
|
69
|
+
const view = editorRef.current?.view;
|
|
70
|
+
if (!view) return;
|
|
71
|
+
const current = view.state.doc.toString();
|
|
72
|
+
try {
|
|
73
|
+
const next = raw ? current : await formatCode(current);
|
|
74
|
+
if (view.state.doc.toString() !== current) {
|
|
75
|
+
onError?.(null);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (next !== current) view.dispatch({
|
|
79
|
+
changes: {
|
|
80
|
+
from: 0,
|
|
81
|
+
to: current.length,
|
|
82
|
+
insert: next
|
|
83
|
+
},
|
|
84
|
+
selection: { anchor: Math.min(view.state.selection.main.head, next.length) }
|
|
85
|
+
});
|
|
86
|
+
onChange?.(next);
|
|
87
|
+
onSave?.(next);
|
|
88
|
+
onError?.(null);
|
|
89
|
+
} catch (e) {
|
|
90
|
+
onError?.(e instanceof Error ? e.message : String(e));
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
return /* @__PURE__ */ jsx("div", {
|
|
67
94
|
className: cn(className),
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
95
|
+
onKeyDown: (e) => {
|
|
96
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "s") {
|
|
97
|
+
e.preventDefault();
|
|
98
|
+
save();
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
children: /* @__PURE__ */ jsx(CodeMirror, {
|
|
102
|
+
ref: editorRef,
|
|
103
|
+
value,
|
|
104
|
+
theme: theme ?? vscodeLight,
|
|
105
|
+
height: height || "100%",
|
|
106
|
+
extensions: EXTENSIONS,
|
|
107
|
+
onChange,
|
|
108
|
+
...props
|
|
109
|
+
})
|
|
73
110
|
});
|
|
74
111
|
};
|
|
75
112
|
//#endregion
|
|
76
113
|
export { useFormatCode as n, Core as t };
|
|
77
114
|
|
|
78
|
-
//# sourceMappingURL=core-
|
|
115
|
+
//# sourceMappingURL=core-BNVuH8-l.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"core-BNVuH8-l.js","names":[],"sources":["../src/components/editor/use-format-code.ts","../src/components/editor/core.tsx"],"sourcesContent":["import { useCallback, useMemo } from 'react';\n\nimport { detectTypeScript } from '~/utils/detect-typescript';\n\n// Prettier is an optional peer dependency (#282), loaded on first use.\n// Without it, code comes back unformatted.\nconst loadPrettier = async () => {\n const [prettier, babel, estree, typescript] = await Promise.all([\n import('prettier'),\n import('prettier/plugins/babel'),\n import('prettier/plugins/estree'),\n import('prettier/plugins/typescript'),\n ]);\n\n return {\n format: prettier.format,\n plugins: [babel.default, estree.default, typescript.default],\n };\n};\n\nconst DEFAULT_PRETTIER_OPTIONS: Record<string, unknown> = {\n tabWidth: 2,\n singleQuote: true,\n trailingComma: 'all',\n htmlWhitespaceSensitivity: 'ignore',\n arrowParens: 'avoid',\n printWidth: 60,\n};\n\ninterface UseFormatCodeOptions {\n fragment?: boolean;\n prettierOptions?: Record<string, unknown>;\n}\n\n// The formatter the built-in editor uses, shared with a custom\n// `renderEditor`.\nexport const useFormatCode = ({\n fragment,\n prettierOptions,\n}: UseFormatCodeOptions = {}) => {\n const prettierConfig = useMemo(\n () => ({\n ...DEFAULT_PRETTIER_OPTIONS,\n ...prettierOptions,\n }),\n [prettierOptions],\n );\n\n return useCallback(\n async (code: string) => {\n const isTypeScript = detectTypeScript(code);\n const source = fragment ? `<>${code}</>` : code;\n\n // Only a missing prettier is swallowed here; a genuine format error\n // (prettier present, but the code is unparseable) still propagates so\n // the editor's Cmd+S path can surface it via onError.\n let prettier: Awaited<ReturnType<typeof loadPrettier>>;\n try {\n prettier = await loadPrettier();\n } catch {\n return code;\n }\n\n const formatted = await prettier.format(source, {\n parser: isTypeScript ? 'typescript' : 'babel',\n plugins: prettier.plugins,\n ...prettierConfig,\n });\n\n if (fragment) {\n return formatted\n .replace(/^<>\\n?/, '')\n .replace(/\\n?<\\/>;?\\s*$/, '')\n .replace(/^ {2}/gm, '')\n .trim();\n }\n\n return formatted;\n },\n [prettierConfig, fragment],\n );\n};\n","import { useRef } from 'react';\n\nimport { javascript } from '@codemirror/lang-javascript';\nimport { vscodeLight } from '@uiw/codemirror-theme-vscode';\nimport CodeMirror, {\n EditorView,\n type Extension,\n type ReactCodeMirrorProps,\n type ReactCodeMirrorRef,\n} from '@uiw/react-codemirror';\n\nimport { cn } from '~/utils/cn';\n\nimport { useFormatCode } from './use-format-code';\n\nexport interface Props {\n value: string;\n height?: string;\n theme?: Extension | 'light' | 'dark' | 'none';\n prettierOptions?: Record<string, unknown>;\n fragment?: boolean;\n raw?: boolean;\n className?: string;\n onChange?: (value: string) => void;\n onSave?: (value: string) => void;\n onError?: (error: string | null) => void;\n onCreateEditor?: ReactCodeMirrorProps['onCreateEditor'];\n onUpdate?: ReactCodeMirrorProps['onUpdate'];\n}\n\n// JSX and TypeScript highlighting, with long lines wrapped.\nconst EXTENSIONS = [\n javascript({ jsx: true, typescript: true }),\n EditorView.lineWrapping,\n];\n\n// CodeMirror with this library's options (#346, #309). Cmd/Ctrl+S formats\n// the code with Prettier, shaped by `raw`, `fragment` and\n// `prettierOptions`, then calls `onChange` and `onSave`; a formatter error\n// goes to `onError`.\nconst Core = ({\n value,\n theme,\n height,\n className,\n prettierOptions,\n fragment,\n raw,\n onChange,\n onSave,\n onError,\n ...props\n}: Props) => {\n const formatCode = useFormatCode({ fragment, prettierOptions });\n\n const editorRef = useRef<ReactCodeMirrorRef>(null);\n\n const save = async () => {\n const view = editorRef.current?.view;\n\n if (!view) {\n return;\n }\n\n const current = view.state.doc.toString();\n\n try {\n // `raw` (e.g. innerHTML) skips Prettier.\n const next = raw ? current : await formatCode(current);\n\n // An edit landed while Prettier ran: keep it rather than the result.\n if (view.state.doc.toString() !== current) {\n onError?.(null);\n\n return;\n }\n\n if (next !== current) {\n view.dispatch({\n changes: { from: 0, to: current.length, insert: next },\n selection: {\n anchor: Math.min(view.state.selection.main.head, next.length),\n },\n });\n }\n\n onChange?.(next);\n onSave?.(next);\n onError?.(null);\n } catch (e) {\n onError?.(e instanceof Error ? e.message : String(e));\n }\n };\n\n return (\n <div\n className={cn(className)}\n onKeyDown={e => {\n if ((e.metaKey || e.ctrlKey) && e.key === 's') {\n e.preventDefault();\n void save();\n }\n }}\n >\n <CodeMirror\n ref={editorRef}\n value={value}\n theme={theme ?? vscodeLight}\n height={height || '100%'}\n extensions={EXTENSIONS}\n onChange={onChange}\n {...props}\n />\n </div>\n );\n};\n\nexport default Core;\n"],"mappings":";;;;;;;;AAMA,MAAM,eAAe,YAAY;CAC/B,MAAM,CAAC,UAAU,OAAO,QAAQ,cAAc,MAAM,QAAQ,IAAI;EAC9D,OAAO;EACP,OAAO;EACP,OAAO;EACP,OAAO;CACT,CAAC;CAED,OAAO;EACL,QAAQ,SAAS;EACjB,SAAS;GAAC,MAAM;GAAS,OAAO;GAAS,WAAW;EAAO;CAC7D;AACF;AAEA,MAAM,2BAAoD;CACxD,UAAU;CACV,aAAa;CACb,eAAe;CACf,2BAA2B;CAC3B,aAAa;CACb,YAAY;AACd;AASA,MAAa,iBAAiB,EAC5B,UACA,oBACwB,CAAC,MAAM;CAC/B,MAAM,iBAAiB,eACd;EACL,GAAG;EACH,GAAG;CACL,IACA,CAAC,eAAe,CAClB;CAEA,OAAO,YACL,OAAO,SAAiB;EACtB,MAAM,eAAe,iBAAiB,IAAI;EAC1C,MAAM,SAAS,WAAW,KAAK,KAAK,OAAO;EAK3C,IAAI;EACJ,IAAI;GACF,WAAW,MAAM,aAAa;EAChC,QAAQ;GACN,OAAO;EACT;EAEA,MAAM,YAAY,MAAM,SAAS,OAAO,QAAQ;GAC9C,QAAQ,eAAe,eAAe;GACtC,SAAS,SAAS;GAClB,GAAG;EACL,CAAC;EAED,IAAI,UACF,OAAO,UACJ,QAAQ,UAAU,EAAE,CAAC,CACrB,QAAQ,iBAAiB,EAAE,CAAC,CAC5B,QAAQ,WAAW,EAAE,CAAC,CACtB,KAAK;EAGV,OAAO;CACT,GACA,CAAC,gBAAgB,QAAQ,CAC3B;AACF;;;AClDA,MAAM,aAAa,CACjB,WAAW;CAAE,KAAK;CAAM,YAAY;AAAK,CAAC,GAC1C,WAAW,YACb;AAMA,MAAM,QAAQ,EACZ,OACA,OACA,QACA,WACA,iBACA,UACA,KACA,UACA,QACA,SACA,GAAG,YACQ;CACX,MAAM,aAAa,cAAc;EAAE;EAAU;CAAgB,CAAC;CAE9D,MAAM,YAAY,OAA2B,IAAI;CAEjD,MAAM,OAAO,YAAY;EACvB,MAAM,OAAO,UAAU,SAAS;EAEhC,IAAI,CAAC,MACH;EAGF,MAAM,UAAU,KAAK,MAAM,IAAI,SAAS;EAExC,IAAI;GAEF,MAAM,OAAO,MAAM,UAAU,MAAM,WAAW,OAAO;GAGrD,IAAI,KAAK,MAAM,IAAI,SAAS,MAAM,SAAS;IACzC,UAAU,IAAI;IAEd;GACF;GAEA,IAAI,SAAS,SACX,KAAK,SAAS;IACZ,SAAS;KAAE,MAAM;KAAG,IAAI,QAAQ;KAAQ,QAAQ;IAAK;IACrD,WAAW,EACT,QAAQ,KAAK,IAAI,KAAK,MAAM,UAAU,KAAK,MAAM,KAAK,MAAM,EAC9D;GACF,CAAC;GAGH,WAAW,IAAI;GACf,SAAS,IAAI;GACb,UAAU,IAAI;EAChB,SAAS,GAAG;GACV,UAAU,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC,CAAC;EACtD;CACF;CAEA,OACE,oBAAC,OAAD;EACE,WAAW,GAAG,SAAS;EACvB,YAAW,MAAK;GACd,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,KAAK;IAC7C,EAAE,eAAe;IACjB,KAAU;GACZ;EACF;EAEA,UAAA,oBAAC,YAAD;GACE,KAAK;GACE;GACP,OAAO,SAAS;GAChB,QAAQ,UAAU;GAClB,YAAY;GACF;GACV,GAAI;EACL,CAAA;CACE,CAAA;AAET"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { c as TS_PATTERNS, o as REGEX } from "./constants-
|
|
1
|
+
import { c as TS_PATTERNS, o as REGEX } from "./constants-st9gnJ5g.js";
|
|
2
2
|
//#region src/utils/detect-typescript.ts
|
|
3
3
|
const detectTypeScript = (code) => {
|
|
4
4
|
const withoutModuleLines = code.replace(REGEX.MODULE_IMPORT_EXPORT_LINE, "");
|
|
@@ -7,4 +7,4 @@ const detectTypeScript = (code) => {
|
|
|
7
7
|
//#endregion
|
|
8
8
|
export { detectTypeScript as t };
|
|
9
9
|
|
|
10
|
-
//# sourceMappingURL=detect-typescript-
|
|
10
|
+
//# sourceMappingURL=detect-typescript-CMF0FVKD.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"detect-typescript-
|
|
1
|
+
{"version":3,"file":"detect-typescript-CMF0FVKD.js","names":[],"sources":["../src/utils/detect-typescript.ts"],"sourcesContent":["import { REGEX, TS_PATTERNS } from '../constants';\n\nexport const detectTypeScript = (code: string): boolean => {\n // Drop `import` and `export ... from` lines first: their `as`\n // (`import * as ui`) is plain JavaScript but matches the `as\\s+\\w+` check\n // below.\n const withoutModuleLines = code.replace(REGEX.MODULE_IMPORT_EXPORT_LINE, '');\n\n return TS_PATTERNS.some(pattern => pattern.test(withoutModuleLines));\n};\n"],"mappings":";;AAEA,MAAa,oBAAoB,SAA0B;CAIzD,MAAM,qBAAqB,KAAK,QAAQ,MAAM,2BAA2B,EAAE;CAE3E,OAAO,YAAY,MAAK,YAAW,QAAQ,KAAK,kBAAkB,CAAC;AACrE"}
|