@jbpark/live-editor 6.0.0 → 6.1.1

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.
Files changed (67) hide show
  1. package/README.ko.md +8 -0
  2. package/README.md +8 -0
  3. package/dist/{boundary-CySnDUs2.js → boundary-CLMNMZFa.js} +4 -4
  4. package/dist/boundary-CLMNMZFa.js.map +1 -0
  5. package/dist/{cn-CAnad2vL.js → compile-BWZ5U59y.js} +135 -2
  6. package/dist/compile-BWZ5U59y.js.map +1 -0
  7. package/dist/{context-Bjq0CvBE.js → context-DJxX9epB.js} +2 -3
  8. package/dist/context-DJxX9epB.js.map +1 -0
  9. package/dist/{core-BNVuH8-l.js → core-BfJwD8Jx.js} +3 -6
  10. package/dist/core-BfJwD8Jx.js.map +1 -0
  11. package/dist/dnd/index.js +2 -2
  12. package/dist/{dnd-BvjEDo-x.js → dnd-DAGjlJ0_.js} +241 -170
  13. package/dist/dnd-DAGjlJ0_.js.map +1 -0
  14. package/dist/{document-uHqw0mvr.js → document-MkQGxfry.js} +1182 -636
  15. package/dist/document-MkQGxfry.js.map +1 -0
  16. package/dist/editor/index.js +2 -2
  17. package/dist/{editor-CvnzrM9J.js → editor-B_VOz3Q0.js} +2 -2
  18. package/dist/{editor-CvnzrM9J.js.map → editor-B_VOz3Q0.js.map} +1 -1
  19. package/dist/editor-caches-DXYXgWeD.js +63 -0
  20. package/dist/editor-caches-DXYXgWeD.js.map +1 -0
  21. package/dist/error/index.d.ts +1 -1
  22. package/dist/error/index.js +2 -2
  23. package/dist/{error-CFzvnLCf.js → error-E3T5CS3m.js} +13 -12
  24. package/dist/error-E3T5CS3m.js.map +1 -0
  25. package/dist/{index-DfIvkht8.d.ts → index-BC9hY7dI.d.ts} +5 -5
  26. package/dist/{index-CqsS207F.d.ts → index-D-Dfv65w.d.ts} +3 -2
  27. package/dist/index.d.ts +4 -4
  28. package/dist/index.js +5 -5
  29. package/dist/preview/index.d.ts +1 -1
  30. package/dist/preview/index.js +1 -1
  31. package/dist/{preview-B05MkVIQ.js → preview-DFb1pOqJ.js} +30 -11
  32. package/dist/preview-DFb1pOqJ.js.map +1 -0
  33. package/dist/provider/index.js +1 -1
  34. package/dist/{sections-DhH_aVf5.js → sections-CnG7OLr8.js} +3 -3
  35. package/dist/{sections-DhH_aVf5.js.map → sections-CnG7OLr8.js.map} +1 -1
  36. package/dist/states-Ci1AvoQ9.js.map +1 -1
  37. package/dist/style.css +74 -74
  38. package/dist/ui-kit/index.js +2 -2
  39. package/dist/ui-kit/style.css +22 -6
  40. package/dist/{update-BuYQJqjq.js → update-DnX2ibYn.js} +162 -43
  41. package/dist/update-DnX2ibYn.js.map +1 -0
  42. package/dist/{use-dynamic-tailwind-B3l1qZRW.js → use-dynamic-tailwind-CjXeHjsr.js} +115 -40
  43. package/dist/use-dynamic-tailwind-CjXeHjsr.js.map +1 -0
  44. package/dist/{use-field-state-iQGW0vc6.js → use-field-state-B1VLhWfr.js} +2 -2
  45. package/dist/{use-field-state-iQGW0vc6.js.map → use-field-state-B1VLhWfr.js.map} +1 -1
  46. package/dist/utils/ast/index.js +1 -86
  47. package/dist/utils/index.js +3 -4
  48. package/package.json +3 -4
  49. package/dist/boundary-CySnDUs2.js.map +0 -1
  50. package/dist/cn-CAnad2vL.js.map +0 -1
  51. package/dist/compile-my0AjsCw.js +0 -136
  52. package/dist/compile-my0AjsCw.js.map +0 -1
  53. package/dist/constants-st9gnJ5g.js +0 -55
  54. package/dist/constants-st9gnJ5g.js.map +0 -1
  55. package/dist/context-Bjq0CvBE.js.map +0 -1
  56. package/dist/core-BNVuH8-l.js.map +0 -1
  57. package/dist/detect-typescript-CMF0FVKD.js +0 -10
  58. package/dist/detect-typescript-CMF0FVKD.js.map +0 -1
  59. package/dist/dnd-BvjEDo-x.js.map +0 -1
  60. package/dist/document-uHqw0mvr.js.map +0 -1
  61. package/dist/editor-caches-ClMvdcQF.js +0 -23
  62. package/dist/editor-caches-ClMvdcQF.js.map +0 -1
  63. package/dist/error-CFzvnLCf.js.map +0 -1
  64. package/dist/preview-B05MkVIQ.js.map +0 -1
  65. package/dist/update-BuYQJqjq.js.map +0 -1
  66. package/dist/use-dynamic-tailwind-B3l1qZRW.js.map +0 -1
  67. package/dist/utils/ast/index.js.map +0 -1
package/README.ko.md CHANGED
@@ -117,6 +117,14 @@ export default function Example() {
117
117
  | `@jbpark/live-editor/ui-kit/style.css` | ui-kit 진입점용 ui-kit 스타일시트 |
118
118
  | `@jbpark/live-editor/style.css` | 컴파일된 스타일시트 (필수) |
119
119
 
120
+ ## Next.js와 서버 렌더링
121
+
122
+ 컴포넌트는 클라이언트 컴포넌트입니다. Next.js App Router에서는 `'use client'`로 시작하는 파일에서 렌더링합니다.
123
+
124
+ - 서버 사전 렌더링은 안전합니다. `Live.Editor`와 `frame` 없는 `Live.Preview`는 서버에서 렌더링되고, `Live.Dnd`의 섹션과 `frame`이 있는 `Live.Preview`는 hydration 뒤에 나타납니다.
125
+ - `frame` 없는 `Live.Preview`는 사전 렌더링 중 서버에서 미리보기 코드를 컴파일하고 실행하며, 서버 권한으로 실행됩니다([보안 참고사항](#보안-참고사항) 참고). 피하려면 `frame`을 주거나 `next/dynamic`의 `ssr: false`로 불러옵니다.
126
+ - `./utils`, `./utils/ast`, `./utils/tailwind`에는 React 컴포넌트가 없어 Server Component와 일반 Node에서 쓸 수 있습니다.
127
+
120
128
  ## 보안 참고사항
121
129
 
122
130
  - 프리뷰 `<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 자체는 사용자 코드를 실행하지 않습니다.
package/README.md CHANGED
@@ -118,6 +118,14 @@ can avoid pulling in the rest (e.g. no CodeMirror in a preview-only build) — s
118
118
  | `@jbpark/live-editor/ui-kit/style.css` | ui-kit's stylesheet, for the ui-kit entry |
119
119
  | `@jbpark/live-editor/style.css` | Compiled stylesheet (required) |
120
120
 
121
+ ## Next.js and server rendering
122
+
123
+ The components are client components: in the Next.js App Router, render them from a file that starts with `'use client'`.
124
+
125
+ - Pre-rendering is safe. `Live.Editor` and a `Live.Preview` without `frame` render on the server; `Live.Dnd`'s sections and a framed `Live.Preview` appear once the page hydrates.
126
+ - A `Live.Preview` without `frame` compiles and runs the previewed code on the server while pre-rendering, with the server's privileges (see [Security Notes](#security-notes)). Give it a `frame`, or load it with `next/dynamic` and `ssr: false`, to avoid that.
127
+ - `./utils`, `./utils/ast` and `./utils/tailwind` hold no React components and work in Server Components and plain Node.
128
+
121
129
  ## Security Notes
122
130
 
123
131
  - 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.
@@ -1,13 +1,13 @@
1
1
  import { i as useLiveMessages, t as MessagesContext } from "./messages-DrINcRdG.js";
2
- import { t as cn } from "./cn-CAnad2vL.js";
3
2
  import React from "react";
4
3
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
5
4
  //#region src/components/error/error.tsx
6
- const Error = ({ message, title, className, style, onReset }) => {
5
+ const Error = ({ message, title, className, style, onReset, ...divProps }) => {
7
6
  const messages = useLiveMessages();
8
7
  if (!message) return null;
9
8
  return /* @__PURE__ */ jsxs("div", {
10
- className: cn(className),
9
+ ...divProps,
10
+ className,
11
11
  style: {
12
12
  padding: "16px",
13
13
  backgroundColor: "#fee2e2",
@@ -97,4 +97,4 @@ var ErrorBoundary = class extends React.Component {
97
97
  //#endregion
98
98
  export { Error as n, ErrorBoundary as t };
99
99
 
100
- //# sourceMappingURL=boundary-CySnDUs2.js.map
100
+ //# sourceMappingURL=boundary-CLMNMZFa.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"boundary-CLMNMZFa.js","names":[],"sources":["../src/components/error/error.tsx","../src/components/error/boundary.tsx"],"sourcesContent":["import { useLiveMessages } from '~/components/context/messages';\n\nexport interface Props extends Omit<\n React.ComponentPropsWithRef<'div'>,\n // The component renders its own content.\n 'children' | 'dangerouslySetInnerHTML'\n> {\n message?: string | null;\n title?: string;\n onReset?: () => void;\n}\n\nconst Error = ({\n message,\n title,\n className,\n style,\n onReset,\n ...divProps\n}: Props) => {\n const messages = useLiveMessages();\n\n if (!message) {\n return null;\n }\n\n return (\n <div\n {...divProps}\n className={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":";;;;AAYA,MAAM,SAAS,EACb,SACA,OACA,WACA,OACA,SACA,GAAG,eACQ;CACX,MAAM,WAAW,gBAAgB;CAEjC,IAAI,CAAC,SACH,OAAO;CAGT,OACE,qBAAC,OAAD;EACE,GAAI;EACO;EACX,OAAO;GACL,SAAS;GACT,iBAAiB;GACjB,QAAQ;GACR,cAAc;GACd,OAAO;GACP,GAAG;EACL;EAVF,UAAA;GAYE,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;;;AC1CA,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,3 +1,7 @@
1
+ import { a as CONFIG, n as registerEditorCache } from "./editor-caches-DXYXgWeD.js";
2
+ import { t as createBoundedCache } from "./cache-B0-XcIxJ.js";
3
+ import React from "react";
4
+ import * as Babel from "@babel/standalone";
1
5
  //#region node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
2
6
  function r(e) {
3
7
  var t, f, n = "";
@@ -3399,6 +3403,135 @@ function cn(...inputs) {
3399
3403
  return twMerge(clsx(inputs));
3400
3404
  }
3401
3405
  //#endregion
3402
- export { cn as t };
3406
+ //#region src/utils/scripts.ts
3407
+ const pins = /* @__PURE__ */ new Map();
3408
+ const deferredRevocations = /* @__PURE__ */ new Map();
3409
+ const revoke = (src, blobUrl) => {
3410
+ if (!pins.has(src)) {
3411
+ URL.revokeObjectURL(blobUrl);
3412
+ return;
3413
+ }
3414
+ deferredRevocations.set(src, [...deferredRevocations.get(src) ?? [], blobUrl]);
3415
+ };
3416
+ const pin = (src) => {
3417
+ pins.set(src, (pins.get(src) ?? 0) + 1);
3418
+ };
3419
+ const unpin = (src) => {
3420
+ const count = (pins.get(src) ?? 0) - 1;
3421
+ if (count > 0) {
3422
+ pins.set(src, count);
3423
+ return;
3424
+ }
3425
+ pins.delete(src);
3426
+ deferredRevocations.get(src)?.forEach((blobUrl) => {
3427
+ URL.revokeObjectURL(blobUrl);
3428
+ });
3429
+ deferredRevocations.delete(src);
3430
+ };
3431
+ const scriptCache = createBoundedCache(CONFIG.SCRIPT_CACHE_LIMIT, revoke);
3432
+ const loadingScriptCache = /* @__PURE__ */ new Map();
3433
+ const getCachedScriptBlob = async (src) => {
3434
+ const cached = scriptCache.get(src);
3435
+ if (cached) return cached;
3436
+ const existing = loadingScriptCache.get(src);
3437
+ if (existing) return existing;
3438
+ const promise = fetch(src).then((res) => {
3439
+ if (!res.ok) throw new Error(`Failed to load script ${src}: HTTP ${res.status}`);
3440
+ return res.text();
3441
+ }).then((text) => {
3442
+ const blob = new Blob([text], { type: "application/javascript" });
3443
+ const blobUrl = URL.createObjectURL(blob);
3444
+ scriptCache.set(src, blobUrl);
3445
+ return blobUrl;
3446
+ }).finally(() => {
3447
+ loadingScriptCache.delete(src);
3448
+ });
3449
+ loadingScriptCache.set(src, promise);
3450
+ return promise;
3451
+ };
3452
+ const withScriptBlobs = async (scripts, inject) => {
3453
+ scripts.forEach(pin);
3454
+ try {
3455
+ inject(await Promise.all(scripts.map(getCachedScriptBlob)));
3456
+ } finally {
3457
+ scripts.forEach(unpin);
3458
+ }
3459
+ };
3460
+ const preloadScripts = (scripts) => {
3461
+ scripts.forEach((src) => {
3462
+ getCachedScriptBlob(src).catch(() => {});
3463
+ });
3464
+ };
3465
+ const clearScriptCache = () => {
3466
+ scriptCache.clear();
3467
+ loadingScriptCache.clear();
3468
+ };
3469
+ registerEditorCache(clearScriptCache);
3470
+ //#endregion
3471
+ //#region src/utils/compile.ts
3472
+ const compilationKeys = /* @__PURE__ */ new Set();
3473
+ const compilationCache = createBoundedCache(CONFIG.CACHE_LIMIT, (key) => compilationKeys.delete(key));
3474
+ const createCacheKey = (code, modules) => ({
3475
+ code,
3476
+ modules: Object.keys(modules).sort().map((name) => [name, modules[name]])
3477
+ });
3478
+ const compile = (code, modules) => {
3479
+ const candidate = createCacheKey(code, modules);
3480
+ for (const key of compilationKeys) if (key.code === candidate.code && key.modules.length === candidate.modules.length && key.modules.every(([name, value], index) => name === candidate.modules[index][0] && Object.is(value, candidate.modules[index][1]))) return compilationCache.get(key);
3481
+ const result = compileModule(code, Object.fromEntries(candidate.modules));
3482
+ compilationCache.set(candidate, result);
3483
+ compilationKeys.add(candidate);
3484
+ return result;
3485
+ };
3486
+ const clearCompilationCache = () => {
3487
+ compilationCache.clear();
3488
+ };
3489
+ const REACT_PRESET = ["react", { runtime: "classic" }];
3490
+ const FILENAME = "preview.tsx";
3491
+ const transformCode = (code) => Babel.transform(code, {
3492
+ filename: FILENAME,
3493
+ presets: ["typescript", REACT_PRESET],
3494
+ sourceType: "module",
3495
+ plugins: [Babel.availablePlugins["transform-modules-commonjs"]]
3496
+ })?.code || "";
3497
+ const describeTransformError = (e) => (e instanceof Error ? e.message : String(e)).replace(/^\/?preview\.tsx: /, "");
3498
+ const compileModule = (code, modules) => {
3499
+ let transformed;
3500
+ try {
3501
+ transformed = transformCode(code);
3502
+ } catch (e) {
3503
+ return {
3504
+ exports: {},
3505
+ error: describeTransformError(e)
3506
+ };
3507
+ }
3508
+ let render;
3509
+ try {
3510
+ render = new Function("exports", "require", "module", "React", transformed);
3511
+ } catch (e) {
3512
+ return {
3513
+ exports: {},
3514
+ error: e instanceof Error ? e.message : "Syntax error"
3515
+ };
3516
+ }
3517
+ const module = { exports: {} };
3518
+ const customRequire = (name) => {
3519
+ if (name === "react") return React;
3520
+ if (name in modules) return modules[name];
3521
+ throw new Error(`Module not found: ${name}`);
3522
+ };
3523
+ try {
3524
+ render(module.exports, customRequire, module, React);
3525
+ } catch (e) {
3526
+ return {
3527
+ exports: {},
3528
+ error: e instanceof Error ? e.message : "Runtime error"
3529
+ };
3530
+ }
3531
+ return module;
3532
+ };
3533
+ registerEditorCache(clearCompilationCache);
3534
+ //#endregion
3535
+ export { cn as a, withScriptBlobs as i, compile as n, preloadScripts as r, clearCompilationCache as t };
3403
3536
 
3404
- //# sourceMappingURL=cn-CAnad2vL.js.map
3537
+ //# sourceMappingURL=compile-BWZ5U59y.js.map