diff --git a/package.json b/package.json index ac31ba9919..a50089f1f2 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "@makotot/ghostui": "^2.0.0", "@stackblitz/sdk": "^1.9.0", "@swc/core": "1.4.2", + "@types/hash-sum": "^1.0.0", "@types/hast": "^2.3.5", "@types/mdast": "^3.0.12", "@umijs/bundler-utils": "^4.0.84", @@ -96,10 +97,12 @@ "dumi-afx-deps": "^1.0.0-alpha.19", "dumi-assets-types": "2.0.0-alpha.0", "enhanced-resolve": "^5.15.0", + "es-module-lexer": "^1.5.0", "estree-util-to-js": "^1.2.0", "estree-util-visit": "^1.2.1", "file-system-cache": "^2.4.3", "github-slugger": "^1.5.0", + "hash-sum": "^2.0.0", "hast-util-is-element": "^2.1.3", "hast-util-raw": "^8.0.0", "hast-util-to-estree": "^2.3.3", @@ -168,6 +171,7 @@ "eslint": "^8.46.0", "fast-glob": "^3.3.1", "father": "^4.3.0", + "happy-dom": "^14.3.9", "highlight-words-core": "^1.2.2", "husky": "^8.0.3", "lint-staged": "^13.2.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4187285831..c64c077497 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,9 +1,5 @@ lockfileVersion: '6.0' -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - importers: .: @@ -20,6 +16,9 @@ importers: '@swc/core': specifier: 1.4.2 version: 1.4.2 + '@types/hash-sum': + specifier: ^1.0.0 + version: 1.0.0 '@types/hast': specifier: ^2.3.5 version: 2.3.5 @@ -62,6 +61,9 @@ importers: enhanced-resolve: specifier: ^5.15.0 version: 5.15.0 + es-module-lexer: + specifier: ^1.5.0 + version: 1.5.0 estree-util-to-js: specifier: ^1.2.0 version: 1.2.0 @@ -74,6 +76,9 @@ importers: github-slugger: specifier: ^1.5.0 version: 1.5.0 + hash-sum: + specifier: ^2.0.0 + version: 2.0.0 hast-util-is-element: specifier: ^2.1.3 version: 2.1.3 @@ -273,6 +278,9 @@ importers: father: specifier: ^4.3.0 version: 4.3.0(@types/node@18.17.1)(styled-components@6.1.8)(webpack@5.89.0) + happy-dom: + specifier: ^14.3.9 + version: 14.3.9 highlight-words-core: specifier: ^1.2.2 version: 1.2.2 @@ -308,7 +316,7 @@ importers: version: 5.0.4 vitest: specifier: ^0.33.0 - version: 0.33.0(sass@1.64.1) + version: 0.33.0(happy-dom@14.3.9)(sass@1.64.1) assets-types: {} @@ -876,7 +884,7 @@ packages: peerDependencies: react: '>=16.9.0' dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.23.8 classnames: 2.3.2 json2mq: 0.2.0 react: 18.2.0 @@ -2937,7 +2945,7 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.23.8 rc-util: 5.38.1(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -2946,7 +2954,7 @@ packages: resolution: {integrity: sha512-jS4E7T9Li2GuYwI6PyiVXmxTiM6b07rlD9Ge8uGZSCz3WlzcG5ZK7g5bbuKNeZ9pgUuPK/5guV781ujdVpm4HQ==} engines: {node: '>=8.x'} dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.23.8 /@rc-component/mutate-observer@1.1.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-QjrOsDXQusNwGZPf4/qRQasg7UFEj06XiCJ8iuiq/Io7CrHrgVi6Uuetw60WAMG1799v+aM8kyc+1L/GBbHSlw==} @@ -2955,7 +2963,7 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.23.8 classnames: 2.3.2 rc-util: 5.38.1(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 @@ -3038,6 +3046,7 @@ packages: rc-util: 5.38.1(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) + dev: false /@rc-component/trigger@1.18.2(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-jRLYgFgjLEPq3MvS87fIhcfuywFSRDaDrYw1FLku7Cm4esszvzTbA0JBsyacAyLrK9rF3TiHFcvoEDMzoD3CTA==} @@ -4425,7 +4434,7 @@ packages: axios: 0.27.2 babel-plugin-import: 1.13.8 dayjs: 1.11.10 - dva-core: 2.0.4(redux@4.2.1) + dva-core: 2.0.4(redux@3.7.2) dva-immer: 1.0.1(dva@2.5.0-beta.2) dva-loading: 3.0.24(dva-core@2.0.4) event-emitter: 0.3.5 @@ -4545,7 +4554,7 @@ packages: react-refresh: 0.14.0 schema-utils: 3.3.0 source-map: 0.7.4 - webpack: 5.89.0(@swc/core@1.4.2) + webpack: 5.89.0(@swc/core@1.4.2)(esbuild@0.19.11) /@umijs/renderer-react@4.0.84(react-dom@18.1.0)(react@18.1.0): resolution: {integrity: sha512-0SDMuLsBpXmdNzubwke0ihq1tvlhDumZn0BJ0JC7xavOmx9bx6jWo3BRrBn1BfkUoCJC8E4C8ZmDJMKrmQ7BUA==} @@ -6607,7 +6616,7 @@ packages: postcss-modules-values: 4.0.0(postcss@8.4.33) postcss-value-parser: 4.2.0 semver: 7.5.4 - webpack: 5.89.0(@swc/core@1.4.2) + webpack: 5.89.0(@swc/core@1.4.2)(esbuild@0.19.11) /css-prefers-color-scheme@6.0.3(postcss@8.4.29): resolution: {integrity: sha512-4BqMbZksRkJQx2zAjrokiGMd07RqOa2IxIrrN10lyBe9xhn9DEvjUK79J6jkeiv9D9hQFXKb6g1jwU62jziJZA==} @@ -7104,7 +7113,7 @@ packages: warning: 3.0.0 dev: true - /dva-core@2.0.4(redux@4.2.1): + /dva-core@2.0.4(redux@3.7.2): resolution: {integrity: sha512-Zh39llFyItu9HKXKfCZVf9UFtDTcypdAjGBew1S+wK8BGVzFpm1GPTdd6uIMeg7O6STtCvt2Qv+RwUut1GFynA==} peerDependencies: redux: 4.x @@ -7114,7 +7123,7 @@ packages: global: 4.4.0 invariant: 2.2.4 is-plain-object: 2.0.4 - redux: 4.2.1 + redux: 3.7.2 redux-saga: 0.16.2 warning: 3.0.0 dev: true @@ -7135,7 +7144,7 @@ packages: dva-core: ^1.1.0 || ^1.5.0-0 || ^1.6.0-0 dependencies: '@babel/runtime': 7.23.1 - dva-core: 2.0.4(redux@4.2.1) + dva-core: 2.0.4(redux@3.7.2) dev: true /dva@2.5.0-beta.2(react-dom@18.2.0)(react@18.2.0): @@ -7362,12 +7371,8 @@ packages: iterator.prototype: 1.1.2 safe-array-concat: 1.0.1 - /es-module-lexer@1.3.1: - resolution: {integrity: sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==} - dev: true - - /es-module-lexer@1.4.1: - resolution: {integrity: sha512-cXLGjP0c4T3flZJKQSuziYoq7MlT+rnvfZjfp7h+I7K9BNX54kP9nyWvdbwjQ4u1iWbOL4u96fgeZLToQlZC7w==} + /es-module-lexer@1.5.0: + resolution: {integrity: sha512-pqrTKmwEIgafsYZAGw9kszYzmagcE/n4dbgwGWLEXg7J4QFJVQRBld8j3Q3GNez79jzxZshq0bcT962QHOghjw==} /es-set-tostringtag@2.0.1: resolution: {integrity: sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==} @@ -8353,7 +8358,7 @@ packages: semver: 7.5.4 tapable: 2.2.1 typescript: 5.0.4 - webpack: 5.89.0(@swc/core@1.4.2) + webpack: 5.89.0(@swc/core@1.4.2)(esbuild@0.19.11) /fork-ts-checker-webpack-plugin@8.0.0(typescript@5.3.3)(webpack@5.89.0): resolution: {integrity: sha512-mX3qW3idpueT2klaQXBzrIM/pHw+T0B/V9KHEvNrqijTq9NFnMZU6oreVxDYcf33P8a5cW+67PjodNHthGnNVg==} @@ -8759,6 +8764,15 @@ packages: /handle-thing@2.0.1: resolution: {integrity: sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==} + /happy-dom@14.3.9: + resolution: {integrity: sha512-0kPQchwthekcYpYN8CvCiq+/z5bqFYDLbTxZ+yDLwT8AFRVJDFadShHRxp3VAZRy7a5isOZ1j/LzsU1dtAIZMQ==} + engines: {node: '>=16.0.0'} + dependencies: + entities: 4.5.0 + webidl-conversions: 7.0.0 + whatwg-mimetype: 3.0.0 + dev: true + /hard-rejection@2.1.0: resolution: {integrity: sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==} engines: {node: '>=6'} @@ -9111,7 +9125,7 @@ packages: lodash: 4.17.21 pretty-error: 4.0.0 tapable: 2.2.1 - webpack: 5.89.0(@swc/core@1.4.2) + webpack: 5.89.0(@swc/core@1.4.2)(esbuild@0.19.11) dev: false /html2sketch@1.0.2: @@ -12846,6 +12860,11 @@ packages: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} engines: {node: '>=6'} + /punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + dev: true + /q@1.5.1: resolution: {integrity: sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==} engines: {node: '>=0.6.0', teleport: '>=0.2.0'} @@ -12934,7 +12953,7 @@ packages: dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 - webpack: 5.89.0(@swc/core@1.4.2) + webpack: 5.89.0(@swc/core@1.4.2)(esbuild@0.19.11) dev: false /rc-align@4.0.15(react-dom@18.2.0)(react@18.2.0): @@ -12989,7 +13008,7 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.23.8 classnames: 2.3.2 rc-util: 5.38.1(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 @@ -13029,7 +13048,7 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.23.8 '@rc-component/portal': 1.1.2(react-dom@18.2.0)(react@18.2.0) classnames: 2.3.2 rc-motion: 2.9.0(react-dom@18.2.0)(react@18.2.0) @@ -13310,7 +13329,7 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.23.8 classnames: 2.3.2 rc-motion: 2.9.0(react-dom@18.2.0)(react@18.2.0) rc-util: 5.38.1(react-dom@18.2.0)(react@18.2.0) @@ -13376,8 +13395,8 @@ packages: moment: optional: true dependencies: - '@babel/runtime': 7.23.2 - '@rc-component/trigger': 1.18.1(react-dom@18.2.0)(react@18.2.0) + '@babel/runtime': 7.23.8 + '@rc-component/trigger': 1.18.2(react-dom@18.2.0)(react@18.2.0) classnames: 2.3.2 dayjs: 1.11.10 rc-util: 5.38.1(react-dom@18.2.0)(react@18.2.0) @@ -13390,7 +13409,7 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.23.8 classnames: 2.3.2 rc-util: 5.38.1(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 @@ -13403,7 +13422,7 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.23.8 classnames: 2.3.2 rc-util: 5.38.1(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 @@ -13456,7 +13475,7 @@ packages: react: '>=16.0.0' react-dom: '>=16.0.0' dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.23.8 classnames: 2.3.2 rc-motion: 2.9.0(react-dom@18.2.0)(react@18.2.0) rc-util: 5.38.1(react-dom@18.2.0)(react@18.2.0) @@ -13534,7 +13553,7 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.23.8 classnames: 2.3.2 rc-util: 5.38.1(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 @@ -13546,7 +13565,7 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.23.8 classnames: 2.3.2 rc-util: 5.38.1(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 @@ -13748,7 +13767,7 @@ packages: react: '*' react-dom: '*' dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.23.8 classnames: 2.3.2 rc-motion: 2.9.0(react-dom@18.2.0)(react@18.2.0) rc-util: 5.38.1(react-dom@18.2.0)(react@18.2.0) @@ -13837,7 +13856,7 @@ packages: react: '*' react-dom: '*' dependencies: - '@babel/runtime': 7.23.2 + '@babel/runtime': 7.23.8 classnames: 2.3.2 rc-resize-observer: 1.4.0(react-dom@18.2.0)(react@18.2.0) rc-util: 5.38.1(react-dom@18.2.0)(react@18.2.0) @@ -15545,31 +15564,6 @@ packages: serialize-javascript: 6.0.2 terser: 5.27.0 webpack: 5.89.0(@swc/core@1.4.2)(esbuild@0.19.11) - dev: false - - /terser-webpack-plugin@5.3.10(@swc/core@1.4.2)(webpack@5.89.0): - resolution: {integrity: sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==} - engines: {node: '>= 10.13.0'} - peerDependencies: - '@swc/core': '*' - esbuild: '*' - uglify-js: '*' - webpack: ^5.1.0 - peerDependenciesMeta: - '@swc/core': - optional: true - esbuild: - optional: true - uglify-js: - optional: true - dependencies: - '@jridgewell/trace-mapping': 0.3.22 - '@swc/core': 1.4.2 - jest-worker: 27.5.1 - schema-utils: 3.3.0 - serialize-javascript: 6.0.2 - terser: 5.27.0 - webpack: 5.89.0(@swc/core@1.4.2) /terser@5.20.0: resolution: {integrity: sha512-e56ETryaQDyebBwJIWYB2TT6f2EZ0fL0sW/JRXNMN26zZdKi2u/E/5my5lG6jNxym6qsrVXfFRmOdV42zlAgLQ==} @@ -15724,7 +15718,7 @@ packages: /tr46@1.0.1: resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} dependencies: - punycode: 2.3.0 + punycode: 2.3.1 dev: true /transformation-matrix@2.15.0: @@ -16252,7 +16246,7 @@ packages: engines: {node: '>=14.19.0'} dependencies: '@rollup/pluginutils': 5.0.4 - es-module-lexer: 1.3.1 + es-module-lexer: 1.5.0 magic-string: 0.30.3 unplugin: 1.5.0 transitivePeerDependencies: @@ -16602,7 +16596,7 @@ packages: fsevents: 2.3.3 dev: true - /vitest@0.33.0(sass@1.64.1): + /vitest@0.33.0(happy-dom@14.3.9)(sass@1.64.1): resolution: {integrity: sha512-1CxaugJ50xskkQ0e969R/hW47za4YXDUfWJDxip1hwbnhUjYolpfUn2AMOulqG/Dtd9WYAtkHmM/m3yKVrEejQ==} engines: {node: '>=v14.18.0'} hasBin: true @@ -16646,6 +16640,7 @@ packages: cac: 6.7.14 chai: 4.3.9 debug: 4.3.4 + happy-dom: 14.3.9 local-pkg: 0.4.3 magic-string: 0.30.3 pathe: 1.1.1 @@ -16813,6 +16808,11 @@ packages: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} dev: true + /webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + dev: true + /webpack-sources@3.2.3: resolution: {integrity: sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==} engines: {node: '>=10.13.0'} @@ -16821,45 +16821,6 @@ packages: resolution: {integrity: sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw==} dev: true - /webpack@5.89.0(@swc/core@1.4.2): - resolution: {integrity: sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==} - engines: {node: '>=10.13.0'} - hasBin: true - peerDependencies: - webpack-cli: '*' - peerDependenciesMeta: - webpack-cli: - optional: true - dependencies: - '@types/eslint-scope': 3.7.7 - '@types/estree': 1.0.5 - '@webassemblyjs/ast': 1.11.6 - '@webassemblyjs/wasm-edit': 1.11.6 - '@webassemblyjs/wasm-parser': 1.11.6 - acorn: 8.11.3 - acorn-import-assertions: 1.9.0(acorn@8.11.3) - browserslist: 4.22.2 - chrome-trace-event: 1.0.3 - enhanced-resolve: 5.15.0 - es-module-lexer: 1.4.1 - eslint-scope: 5.1.1 - events: 3.3.0 - glob-to-regexp: 0.4.1 - graceful-fs: 4.2.11 - json-parse-even-better-errors: 2.3.1 - loader-runner: 4.3.0 - mime-types: 2.1.35 - neo-async: 2.6.2 - schema-utils: 3.3.0 - tapable: 2.2.1 - terser-webpack-plugin: 5.3.10(@swc/core@1.4.2)(webpack@5.89.0) - watchpack: 2.4.0 - webpack-sources: 3.2.3 - transitivePeerDependencies: - - '@swc/core' - - esbuild - - uglify-js - /webpack@5.89.0(@swc/core@1.4.2)(esbuild@0.19.11): resolution: {integrity: sha512-qyfIC10pOr70V+jkmud8tMfajraGCZMBWJtrmuBymQKCrLTRejBI8STDp1MCyZu/QTdZSeacCQYpYNQVOzX5kw==} engines: {node: '>=10.13.0'} @@ -16880,7 +16841,7 @@ packages: browserslist: 4.22.2 chrome-trace-event: 1.0.3 enhanced-resolve: 5.15.0 - es-module-lexer: 1.4.1 + es-module-lexer: 1.5.0 eslint-scope: 5.1.1 events: 3.3.0 glob-to-regexp: 0.4.1 @@ -16898,12 +16859,16 @@ packages: - '@swc/core' - esbuild - uglify-js - dev: false /whatwg-fetch@3.6.20: resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} dev: true + /whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + dev: true + /whatwg-url@7.1.0: resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} dependencies: @@ -17140,3 +17105,7 @@ packages: /zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} dev: false + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false diff --git a/src/client/misc/reactDemoCompiler.ts b/src/client/misc/reactDemoCompiler.ts index 25eff521be..5fd335175f 100644 --- a/src/client/misc/reactDemoCompiler.ts +++ b/src/client/misc/reactDemoCompiler.ts @@ -1,10 +1,12 @@ -import { transform } from 'sucrase'; +import { transform, type Transform } from 'sucrase'; import type { IDemoCompileFn } from '../theme-api/types'; -const compile: IDemoCompileFn = async (code) => { - return transform(code, { - transforms: ['typescript', 'jsx', 'imports'], - }).code; +const compile: IDemoCompileFn = async (code, { modules }) => { + const transforms: Transform[] = ['typescript', 'jsx']; + if (modules === 'cjs') { + transforms.push('imports'); + } + return transform(code, { transforms }).code; }; export default compile; diff --git a/src/client/pages/Demo/index.ts b/src/client/pages/Demo/index.ts index c90b96f5c9..8b2b67ccce 100644 --- a/src/client/pages/Demo/index.ts +++ b/src/client/pages/Demo/index.ts @@ -1,13 +1,15 @@ import { useDemo, useLiveDemo, useParams } from 'dumi'; -import { ComponentType, createElement, useEffect, type FC } from 'react'; +import { createElement, useEffect, type ComponentType } from 'react'; import { useRenderer } from '../../theme-api/useRenderer'; import './index.less'; -const DemoRenderPage: FC = () => { - const { id } = useParams(); - const demo = useDemo(id!); +const DemoRenderPage = () => { + const params = useParams(); + const id = params.id!; - const canvasRef = useRenderer(demo!); + const demo = useDemo(id)!; + + const canvasRef = useRenderer(Object.assign(demo, { id })); const { component, renderOpts } = demo || {}; @@ -15,7 +17,7 @@ const DemoRenderPage: FC = () => { node: liveDemoNode, setSource, error: liveDemoError, - } = useLiveDemo(id!); + } = useLiveDemo(id); const finalNode = liveDemoNode || diff --git a/src/client/theme-api/__tests__/sandbox.spec.ts b/src/client/theme-api/__tests__/sandbox.spec.ts new file mode 100644 index 0000000000..618a5568fd --- /dev/null +++ b/src/client/theme-api/__tests__/sandbox.spec.ts @@ -0,0 +1,43 @@ +// @vitest-environment happy-dom +import * as lexer from 'es-module-lexer'; +import { rewriteExports } from '../sandbox'; + +async function getModule(iife: string) { + const s = iife.indexOf('blob:'); + const e = iife.indexOf("');", s); + const blob = iife.substring(s, e); + return await (await fetch(blob)).text(); +} + +// happy-dom/jsdom cannot test Sandbox because neither of them implements the sandbox and srcdoc attributes of iframe. +// Currently only sandbox is being implemented: https://github.com/capricorn86/happy-dom/pull/1375 +// After the srcdoc attribute is implemented, complete tests will be added + +describe('esm sandbox', () => { + describe('rewriteExports', () => { + beforeAll(async () => { + await lexer.init; + }); + it('should assign the local name to the constant', async () => { + const iife = rewriteExports('m1', 'export default 1;', 'sandbox'); + expect(await getModule(iife)).toMatchInlineSnapshot(` + "const m1 = 1; + window.___modules___['m1'] = { 'default': m1 }; + parent.postMessage({ type: 'sandbox.esm.done' }, '*');" + `); + }); + + it('should remove the local name in the export statement', async () => { + const iife = rewriteExports( + 'm2', + 'export { xxx as default };', + 'sandbox', + ); + expect(await getModule(iife)).toMatchInlineSnapshot(` + "export { }; + window.___modules___['m2'] = { 'default': xxx }; + parent.postMessage({ type: 'sandbox.esm.done' }, '*');" + `); + }); + }); +}); diff --git a/src/client/theme-api/sandbox.ts b/src/client/theme-api/sandbox.ts new file mode 100644 index 0000000000..ad89f7ff27 --- /dev/null +++ b/src/client/theme-api/sandbox.ts @@ -0,0 +1,353 @@ +import * as lexer from 'es-module-lexer'; +import hashId from 'hash-sum'; + +const srcdoc = ` + + + + + + + + + +`; +interface ImportMap { + imports?: Record; + scopes?: Record; +} + +export interface ExtendedImportMap extends ImportMap { + builtins?: Record; +} + +const MODULE_STORE = '___modules___'; + +type CodeSandboxWindow = Window & { + ___modules___: Record; +}; + +function createBlob(source: string) { + return URL.createObjectURL(new Blob([source], { type: 'text/javascript' })); +} + +const DEFAULT_RESTRICTIONS = [ + 'allow-forms', + 'allow-modals', + 'allow-pointer-lock', + 'allow-popups', + 'allow-same-origin', + 'allow-scripts', + 'allow-top-navigation-by-user-activation', +]; + +export function rewriteExports( + moduleId: string, + esm: string, + eventPrefix: string, +) { + const [, exports] = lexer.parse(esm); + for (const e of exports) { + if (e.n === 'default') { + let rewriteEsm = ''; + let defaultSymbol = ''; + // export default or export { s as default } + if (e.ln) { + rewriteEsm = esm.substring(0, e.ls) + esm.substring(e.e); + defaultSymbol = e.ln; + } else { + const exportStatementStart = esm.lastIndexOf('export', e.s); + if (exportStatementStart >= 0) { + rewriteEsm = + esm.substring(0, exportStatementStart) + + `const ${moduleId} = ` + + esm.substring(e.e); + defaultSymbol = moduleId; + } + } + if (rewriteEsm && defaultSymbol) { + const url = createBlob( + rewriteEsm + + `\nwindow.${MODULE_STORE}['${moduleId}'] = { 'default': ${defaultSymbol} };` + + `\nparent.postMessage({ type: '${eventPrefix}.esm.done' }, '*');`, + ); + return `(async function() { + try { + await import('${url}'); + } catch(e) { + parent.postMessage({ type: '${eventPrefix}.esm.error', error: e }); + } + })()`; + } + } + } + return esm; +} + +class IFrameContainer { + private iframe!: HTMLIFrameElement; + private iframeWindow!: CodeSandboxWindow; + constructor( + private restrictions: string[], + private moduleCache: Record, + private eventPrefix: string, + ) {} + async create(srcdoc: string) { + return new Promise((resolve, reject) => { + const iframe = document.createElement('iframe'); + iframe.sandbox.add(...this.restrictions); + const iframeLoaded = () => { + this.iframeWindow = iframe.contentWindow as CodeSandboxWindow; + this.iframeWindow[MODULE_STORE] = this.moduleCache; + resolve(this); + }; + iframe.addEventListener( + 'load', + () => { + if (iframe.contentWindow) { + iframeLoaded(); + } else { + reject('missing contentWindow'); + } + }, + false, + ); + + iframe.addEventListener( + 'error', + (e) => { + reject(e.error); + }, + false, + ); + + iframe.style.width = '0'; + iframe.style.height = '0'; + iframe.style.border = 'none'; + iframe.style.visibility = 'hidden'; + iframe.srcdoc = srcdoc; + (document.body ?? document.head).appendChild(iframe); + this.iframe = iframe; + if (iframe.contentWindow) { + (iframe.contentWindow as any)[MODULE_STORE] = this.moduleCache; + } + }); + } + + exec(moduleId: string, esm: string) { + return new Promise((execResolve, execReject) => { + const handleMessage = (e: MessageEvent) => { + const { data } = e; + if (data.type?.startsWith(`${this.eventPrefix}.esm.done`)) { + window.removeEventListener('message', handleMessage); + execResolve(this.moduleCache[moduleId]); + } else if (data.type?.startsWith(`${this.eventPrefix}.esm.error`)) { + execReject(data.error); + } + }; + window.addEventListener('message', handleMessage, false); + this.iframeWindow.postMessage({ esm }, '*'); + }); + } + + destroy() { + this.moduleCache = {}; + this.iframe.remove(); + } +} + +export interface SandboxParams { + importMap?: ImportMap; + restrictions?: string[]; + eventPrefix?: string; +} + +/** + * A sandbox for executing ES modules + * + * Supports importmap, and can even inject module instances into the sandbox + * (requires "allow-same-origin" permission) + * + * @example + * ```ts + * const sandbox = await Sandbox.create({ + * importMap: { + * builtins: { 'vue': Module }, + * imports: { + * "lodash": "https://esm.sh/lodash-es", + * }, + * scopes: {}, + * } + * }); + * await sandbox.exec('import vue from "vue";\nimport lodash from "lodash"'); + * await sandbox.updateImportMap({}); + * ``` + */ +export class Sandbox { + private codeSandbox!: Promise; + private srcdoc: string = srcdoc; + + private moduleCache: Record = {}; + private restrictions: string[] = DEFAULT_RESTRICTIONS; + private eventPrefix: string = 'sandbox'; + + static create(params?: SandboxParams) { + return new Sandbox().init(params || {}); + } + + private contentLoadedListener?: () => void; + + private async init({ importMap, restrictions, eventPrefix }: SandboxParams) { + if (importMap) { + this.updateSrcDoc(importMap); + } + if (restrictions) { + this.restrictions = restrictions; + } + if (eventPrefix) { + this.eventPrefix = eventPrefix; + } + this.codeSandbox = (async () => { + await lexer.init; + return new Promise((resolve, reject) => { + const initCodeSandbox = () => { + const container = new IFrameContainer( + this.restrictions, + this.moduleCache, + this.eventPrefix, + ); + container.create(this.srcdoc).then(resolve).catch(reject); + }; + if (document.readyState === 'loading') { + this.contentLoadedListener = initCodeSandbox; + document.addEventListener( + 'DOMContentLoaded', + this.contentLoadedListener, + false, + ); + } else { + initCodeSandbox(); + } + }); + })(); + + await this.codeSandbox; + return this; + } + + async destory() { + if (this.contentLoadedListener) { + document.removeEventListener( + 'DOMContentLoaded', + this.contentLoadedListener, + false, + ); + } + return this.codeSandbox.then((codeSandbox) => { + return codeSandbox.destroy(); + }); + } + + private updateSrcDoc({ imports, builtins, scopes }: ExtendedImportMap) { + const realImportMap: ImportMap = {}; + if (imports) { + realImportMap.imports = { + ...imports, + }; + } + if (scopes) { + realImportMap.scopes = { + ...scopes, + }; + } + if (builtins) { + realImportMap.imports = Object.entries(builtins).reduce( + (acc, [identifier, module]) => { + let source = + `const modules = window.${MODULE_STORE} || {};` + + `\nconst m = modules['${identifier}'] || {}`; + let hasDefault = false; + Object.keys(module).forEach((member) => { + if (member === 'default') { + hasDefault = true; + } + source += + member === 'default' + ? `\nexport default m['${member}']` + : `\nexport const ${member} = m['${member}'];`; + }); + if (!hasDefault) { + source += `\nexport default m;`; + } + const blob = createBlob(source); + acc[identifier] = blob; + this.moduleCache[identifier] = module; + return acc; + }, + realImportMap.imports ?? {}, + ); + } + + const replacements: Record = { + importmap: JSON.stringify(realImportMap), + eventPrefix: this.eventPrefix, + }; + this.srcdoc = srcdoc.replace( + /\{(\w+)\}/gi, + (_, key) => replacements[key] || '', + ); + } + + async updateImportMap(importMap: ExtendedImportMap) { + this.codeSandbox = this.codeSandbox + .then((codeSandbox) => { + codeSandbox.destroy(); + this.updateSrcDoc(importMap); + }) + .then(() => { + const container = new IFrameContainer( + this.restrictions, + this.moduleCache, + this.eventPrefix, + ); + return container.create(this.srcdoc); + }); + await this.codeSandbox; + } + + async exec(esm: string) { + const moduleId = `module_${hashId(esm)}`; + const result = this.moduleCache[moduleId]; + if (result) return result; + const codeSandbox = await this.codeSandbox; + return codeSandbox.exec( + moduleId, + rewriteExports(moduleId, esm, this.eventPrefix), + ); + } +} diff --git a/src/client/theme-api/types.ts b/src/client/theme-api/types.ts index f637f7165d..58f2471520 100644 --- a/src/client/theme-api/types.ts +++ b/src/client/theme-api/types.ts @@ -247,9 +247,11 @@ export type AgnosticComponentType = | Promise | AgnosticComponentModule; +export type ModuleType = 'esm' | 'cjs'; + export type IDemoCompileFn = ( code: string, - opts: { filename: string }, + opts: { filename: string; modules?: ModuleType }, ) => Promise; export type IDemoCancelableFn = ( diff --git a/src/client/theme-api/useImportMap.ts b/src/client/theme-api/useImportMap.ts new file mode 100644 index 0000000000..8ac473a6a7 --- /dev/null +++ b/src/client/theme-api/useImportMap.ts @@ -0,0 +1,57 @@ +import { useMemo, useState } from 'react'; +import { ExtendedImportMap } from './sandbox'; +import { IDemoData } from './types'; +import { SUPPORTED_MODULE } from './utils'; + +export const useImportMap = (demo: IDemoData) => { + const { context, asset } = demo; + + const [internalImportMap, setImportMap] = useState({ + builtins: context || {}, + }); + + const importMap = useMemo(() => { + if (SUPPORTED_MODULE === 'cjs') return null; + return Object.assign( + { + imports: {}, + scopes: {}, + }, + internalImportMap, + { + builtins: Object.keys(internalImportMap.builtins).reduce( + (builtins, dep) => { + builtins[dep] = asset.dependencies[dep]?.value; + return builtins; + }, + {} as NonNullable, + ), + }, + ) as ExtendedImportMap; + }, [internalImportMap]); + + async function updateImportMap(modifiedImportMap: ExtendedImportMap) { + const map: ExtendedImportMap = {}; + if (modifiedImportMap.imports) { + map.imports = modifiedImportMap.imports; + } + if (modifiedImportMap.scopes) { + map.scopes = modifiedImportMap.scopes; + } + setImportMap(() => ({ + ...internalImportMap, + ...map, + })); + } + return { + /** + * Provided to sandbox + */ + internalImportMap, + /** + * provided to editor + */ + importMap, + updateImportMap, + }; +}; diff --git a/src/client/theme-api/useLiveDemo.ts b/src/client/theme-api/useLiveDemo.ts index b3f6e1b38c..cdd95a4fbc 100644 --- a/src/client/theme-api/useLiveDemo.ts +++ b/src/client/theme-api/useLiveDemo.ts @@ -11,25 +11,13 @@ import { } from 'react'; import DemoErrorBoundary from './DumiDemo/DemoErrorBoundary'; import type { AgnosticComponentType } from './types'; +import { useImportMap } from './useImportMap'; import { useRenderer } from './useRenderer'; +import { useSandbox } from './useSandbox'; +import { SUPPORTED_MODULE } from './utils'; const THROTTLE_WAIT = 500; -type CommonJSContext = { - module: any; - exports: { - default?: any; - }; - require: any; -}; - -function evalCommonJS( - js: string, - { module, exports, require }: CommonJSContext, -) { - new Function('module', 'exports', 'require', js)(module, exports, require); -} - export const useLiveDemo = ( id: string, opts?: { containerRef?: RefObject; iframe?: boolean }, @@ -54,6 +42,11 @@ export const useLiveDemo = ( const [demoNode, setDemoNode] = useState(); const [error, setError] = useState(null); + + const { importMap, internalImportMap, updateImportMap } = useImportMap(demo); + + const sandbox = useSandbox(internalImportMap, SUPPORTED_MODULE); + const setSource = useCallback( throttle( async (source: Record) => { @@ -82,7 +75,7 @@ export const useLiveDemo = ( value: { err: null | Error }; }>, ) => { - if (ev.data.type.startsWith('dumi.liveDemo.compileDone')) { + if (ev.data.type?.startsWith('dumi.liveDemo.compileDone')) { iframeWindow.removeEventListener('message', handler); setError(ev.data.value.err); resolve(); @@ -99,18 +92,17 @@ export const useLiveDemo = ( const entryFileName = Object.keys(asset.dependencies).find( (k) => asset.dependencies[k].type === 'FILE', )!; - const require = (v: string) => { - if (v in context!) return context![v]; - throw new Error(`Cannot find module: ${v}`); - }; const token = (taskToken.current = Math.random()); let entryFileCode = source[entryFileName]; + sandbox.init(); + if (renderOpts?.compile) { try { entryFileCode = await renderOpts.compile(entryFileCode, { filename: entryFileName, + modules: SUPPORTED_MODULE, }); } catch (error: any) { setError(error); @@ -121,13 +113,9 @@ export const useLiveDemo = ( if (renderOpts?.renderer && renderOpts?.compile) { try { - const exports: AgnosticComponentType = {}; - const module = { exports }; - evalCommonJS(entryFileCode, { - exports, - module, - require, - }); + const exports: AgnosticComponentType = await sandbox.exec( + entryFileCode, + ); setComponent(exports); setDemoNode(createElement('div', { ref })); setError(null); @@ -146,17 +134,9 @@ export const useLiveDemo = ( // skip current task if another task is running if (token !== taskToken.current) return; - - const exports: { default?: ComponentType } = {}; - const module = { exports }; - - // initial component with fake runtime - evalCommonJS(entryFileCode, { - exports, - module, - require, - }); - + const exports: { default?: ComponentType } = await sandbox.exec( + entryFileCode, + ); const newDemoNode = createElement( DemoErrorBoundary, null, @@ -188,5 +168,12 @@ export const useLiveDemo = ( [context, asset, renderOpts], ); - return { node: demoNode, loading, error, setSource }; + return { + node: demoNode, + loading, + error, + setSource, + updateImportMap, + importMap, + }; }; diff --git a/src/client/theme-api/useSandbox.ts b/src/client/theme-api/useSandbox.ts new file mode 100644 index 0000000000..2d24ad6152 --- /dev/null +++ b/src/client/theme-api/useSandbox.ts @@ -0,0 +1,80 @@ +import { useEffect, useRef } from 'react'; +import type { ExtendedImportMap, Sandbox } from './sandbox'; +import type { ModuleType } from './types'; + +type CommonJSContext = { + module: any; + exports: { + default?: any; + }; + require: any; +}; + +function evalCommonJS( + cjs: string, + { module, exports, require }: CommonJSContext, +) { + new Function('module', 'exports', 'require', cjs)(module, exports, require); +} + +async function createSandbox( + importMap: ExtendedImportMap, + modules: ModuleType, +) { + if (modules === 'esm') { + return import('./sandbox').then(({ Sandbox }) => + Sandbox.create({ importMap }), + ); + } + const require = (v: string) => { + if (v in importMap.builtins!) return importMap.builtins![v]; + throw new Error(`Cannot find module: ${v}`); + }; + return Promise.resolve({ + destory() {}, + exec(cjs: string) { + const exports = {}; + evalCommonJS(cjs, { + exports, + module: { exports }, + require, + }); + return exports; + }, + } as Sandbox); +} + +export function useSandbox( + importMap: ExtendedImportMap, + modules: ModuleType = 'esm', +) { + const sandbox = useRef>(); + const lastImportMap = useRef(importMap); + function destorySandbox() { + sandbox.current?.then((s) => s.destory()); + } + if (lastImportMap.current !== importMap) { + lastImportMap.current = importMap; + sandbox.current = sandbox.current + ? sandbox.current + .then((s) => s.destory()) + .then(() => createSandbox(importMap, modules)) + : createSandbox(importMap, modules); + } + + useEffect(() => () => destorySandbox(), []); + + return { + init() { + if (sandbox.current) return; + sandbox.current = createSandbox(importMap, modules); + }, + async exec(esm: string) { + if (!sandbox.current) { + throw new Error('Please execute init first'); + } + const box = await sandbox.current; + return box.exec(esm); + }, + }; +} diff --git a/src/client/theme-api/utils.ts b/src/client/theme-api/utils.ts index 76dec4dab9..2d6782ad84 100644 --- a/src/client/theme-api/utils.ts +++ b/src/client/theme-api/utils.ts @@ -7,6 +7,7 @@ import type { IRouteMeta, IRoutesById, IUserNavValue, + ModuleType, } from './types'; import { useLocale } from './useLocale'; @@ -146,3 +147,15 @@ export const pickRouteSortMeta = ( export function getLocaleNav(nav: IUserNavValue | INav, locale: ILocale) { return Array.isArray(nav) ? nav : nav[locale.id]; } + +export const supports = + HTMLScriptElement.supports || + function (type: string) { + if (type === 'module') { + return 'noModule' in HTMLScriptElement.prototype; + } + return false; + }; + +export const SUPPORTED_MODULE: ModuleType = + supports('importmap') && supports('module') ? 'esm' : 'cjs'; diff --git a/src/client/theme-default/builtins/Previewer/index.tsx b/src/client/theme-default/builtins/Previewer/index.tsx index 6ce407a40b..516f80e2b1 100644 --- a/src/client/theme-default/builtins/Previewer/index.tsx +++ b/src/client/theme-default/builtins/Previewer/index.tsx @@ -15,6 +15,8 @@ const Previewer: FC = (props) => { error: liveDemoError, loading: liveDemoLoading, setSource: setLiveDemoSource, + importMap, + updateImportMap, } = useLiveDemo(props.asset.id, { iframe: Boolean(props.iframe || props._live_in_iframe), containerRef: demoContainer, @@ -76,6 +78,8 @@ const Previewer: FC = (props) => { )} svg { width: 16px; @@ -91,6 +93,18 @@ fill: lighten(@c-border-dark, 20%); } } + .@{prefix}-tabs-tabpane:not(:hover) & { + opacity: 0; + visibility: hidden; + } + } + + &-editor-tip-btn { + position: absolute; + top: 9px; + right: 42px; + padding: 8px 12px; + cursor: help; &[data-readonly] { > span { @@ -104,15 +118,50 @@ transform: rotate(45deg) translate(-50%, 120%); } } + } - .@{prefix}-tabs-tabpane:not(:hover) & { - opacity: 0; - visibility: hidden; + &-importmap-btn { + position: absolute; + top: 9px; + right: 72px; + padding: 8px 12px; + line-height: 16px; + cursor: pointer; + } + + &-json-ops { + position: absolute; + top: 9px; + right: 46px; + z-index: 3; + + > button { + padding: 8px; + cursor: pointer; + } + } + + &-json-editor { + position: absolute; + top: 0; + left: 0; + z-index: 3; + width: 100%; + height: 100%; + + > div { + height: 100%; + + > div { + height: 100%; + } } } } .@{prefix}-tabs { + position: relative; + &-top { flex-direction: column; @@ -274,6 +323,7 @@ min-width: 30px; margin-bottom: 8px; box-sizing: border-box; + z-index: 5; &-hidden { display: none; diff --git a/src/client/theme-default/slots/PreviewerActions/index.tsx b/src/client/theme-default/slots/PreviewerActions/index.tsx index d6c32cd867..374ce80817 100644 --- a/src/client/theme-default/slots/PreviewerActions/index.tsx +++ b/src/client/theme-default/slots/PreviewerActions/index.tsx @@ -1,6 +1,9 @@ +import type { ExtendedImportMap } from '@/client/theme-api/sandbox'; import { ReactComponent as IconCheck } from '@ant-design/icons-svg/inline-svg/outlined/check.svg'; +import { ReactComponent as IconClose } from '@ant-design/icons-svg/inline-svg/outlined/close-circle.svg'; import { ReactComponent as IconCodeSandbox } from '@ant-design/icons-svg/inline-svg/outlined/code-sandbox.svg'; import { ReactComponent as IconEdit } from '@ant-design/icons-svg/inline-svg/outlined/edit.svg'; +import { ReactComponent as IconSave } from '@ant-design/icons-svg/inline-svg/outlined/save.svg'; import { ReactComponent as IconSketch } from '@ant-design/icons-svg/inline-svg/outlined/sketch.svg'; import { ReactComponent as IconStackBlitz } from '@ant-design/icons-svg/inline-svg/outlined/thunderbolt.svg'; import classNames from 'classnames'; @@ -15,7 +18,9 @@ import { } from 'dumi'; import SourceCode from 'dumi/theme/builtins/SourceCode'; import PreviewerActionsExtra from 'dumi/theme/slots/PreviewerActionsExtra'; -import SourceCodeEditor from 'dumi/theme/slots/SourceCodeEditor'; +import SourceCodeEditor, { + type SourceCodeEditorMethods, +} from 'dumi/theme/slots/SourceCodeEditor'; import Tabs from 'rc-tabs'; import RcTooltip from 'rc-tooltip'; import type { TooltipProps as RcTooltipProps } from 'rc-tooltip/lib/Tooltip'; @@ -39,22 +44,134 @@ const Tooltip: FC = (props) => { ); }; -export interface IPreviewerActionsProps extends IPreviewerProps { - /** - * disabled actions - */ - disabledActions?: ('CSB' | 'STACKBLITZ' | 'EXTERNAL' | 'HTML2SKETCH')[]; - extra?: ReactNode; - forceShowCode?: boolean; - demoContainer: HTMLDivElement | HTMLIFrameElement; - onSourceTranspile?: ( - args: - | { err: Error; source?: null } - | { err?: null; source: Record }, - ) => void; - onSourceChange?: (source: Record) => void; +function checkImportMap( + importmap: ExtendedImportMap, + originImportMap: ExtendedImportMap, +) { + if ( + JSON.stringify(originImportMap?.builtins) !== + JSON.stringify(importmap.builtins) + ) { + throw new Error('Builtin dependencies cannot be changed online!'); + } + if (importmap.imports && importmap.builtins) { + for (const i of Object.keys(importmap.imports)) { + if (importmap.builtins[i]) { + throw new Error( + 'Dependencies already in `builtins` cannot be introduced in `imports`', + ); + } + } + } } +export interface UseImportMapEditorProps { + importMap?: ExtendedImportMap | null; + onImportMapChange?: (importMap: ExtendedImportMap) => void; +} + +function useImportMapEditor(props: UseImportMapEditorProps) { + const [visible, setVisible] = useState(false); + const originImportMap = useRef(props.importMap); + const [source, setSource] = useState( + props.importMap && JSON.stringify(props.importMap, null, 2), + ); + + function close() { + setSource(JSON.stringify(originImportMap.current, null, 2)); + setVisible(false); + } + + function toggle() { + setVisible(!visible); + } + + function saveImportMap() { + if (!source) return; + try { + const result = JSON.parse(source) as ExtendedImportMap; + checkImportMap(result, originImportMap.current!); + props.onImportMapChange?.(result); + originImportMap.current = result; + close(); + } catch (error: any) { + alert(error.toString()); + } + } + + return { + visible, + toggle, + close, + saveImportMap, + source, + setSource, + }; +} + +const ImportMapEditor = (props: ReturnType) => { + const intl = useIntl(); + if (!props.source) return null; + return ( +
+ { + props.setSource(code); + }} + lang="json" + extra={ +
+ + + + + + +
+ } + /> +
+ ); +}; + +export type IPreviewerActionsProps = IPreviewerProps & + UseImportMapEditorProps & { + /** + * disabled actions + */ + disabledActions?: ('CSB' | 'STACKBLITZ' | 'EXTERNAL' | 'HTML2SKETCH')[]; + extra?: ReactNode; + forceShowCode?: boolean; + demoContainer: HTMLDivElement | HTMLIFrameElement; + onSourceTranspile?: ( + args: + | { err: Error; source?: null } + | { err?: null; source: Record }, + ) => void; + onSourceChange?: (source: Record) => void; + }; + const IconCode: FC = () => ( @@ -86,6 +203,18 @@ const PreviewerActions: FC = (props) => { ); const copyTimer = useRef(); const [isCopied, setIsCopied] = useState(false); + + const codeEditor = useRef(null); + + const mapEditor = useImportMapEditor({ + importMap: props.importMap, + onImportMapChange: (importMap) => { + props.onImportMapChange?.(importMap); + // After the Import Map is updated, code needs to be recompiled. + codeEditor.current?.triggerChange(); + }, + }); + const isSingleFile = files.length === 1; const lang = (files[activeKey][0].match(/\.([^.]+)$/)?.[1] || 'text') as any; @@ -218,31 +347,46 @@ const PreviewerActions: FC = (props) => { // only support to edit entry file currently children: i === 0 && renderOpts?.compile ? ( - { - props.onSourceChange?.({ [files[i][0]]: code }); - // FIXME: remove before publish - props.onSourceTranspile?.({ - source: { [files[i][0]]: code }, - }); - }} - extra={ - - - - } - /> + <> + { + props.onSourceChange?.({ [files[i][0]]: code }); + // FIXME: remove before publish + props.onSourceTranspile?.({ + source: { [files[i][0]]: code }, + }); + }} + extra={ + <> + + + + {props.importMap && ( + + )} + + } + /> + {mapEditor.visible && } + ) : ( void; } +export interface SourceCodeEditorMethods { + triggerChange: () => void; +} + /** * simple source code editor based on textarea */ -const SourceCodeEditor: FC = (props) => { +const SourceCodeEditor = forwardRef< + SourceCodeEditorMethods, + ISourceCodeEditorProps +>((props, ref) => { const elm = useRef(null); const [style, setStyle] = useState(); const [code, setCode] = useState(props.initialValue); + function handleChange(code: string) { + setCode(code); + props.onChange?.(code); + // FIXME: remove before publish + props.onTranspile?.({ err: null, code }); + } + + useImperativeHandle(ref, () => ({ + triggerChange: () => { + handleChange(code); + }, + })); + // generate style from pre element, for adapting to the custom theme useEffect(() => { const pre = elm.current?.querySelector('pre'); @@ -93,6 +114,6 @@ const SourceCodeEditor: FC = (props) => { ); -}; +}); export default SourceCodeEditor; diff --git a/src/client/typings.d.ts b/src/client/typings.d.ts index a727659158..d2a1c4e703 100644 --- a/src/client/typings.d.ts +++ b/src/client/typings.d.ts @@ -7,3 +7,5 @@ declare module '*.svg' { const src: string; export default src; } + +declare module '*.html'; diff --git a/suites/preset-vue/lib/compiler.mjs b/suites/preset-vue/lib/compiler.mjs index 029c67b5da..46d697691b 100644 --- a/suites/preset-vue/lib/compiler.mjs +++ b/suites/preset-vue/lib/compiler.mjs @@ -1,5 +1,5 @@ +import * as md from '@babel/standalone'; import { parse, compileScript, rewriteDefault, compileTemplate, compileStyle } from 'vue/compiler-sfc'; -import * as Td from '@babel/standalone'; var Sd=Object.create;var ko=Object.defineProperty;var xd=Object.getOwnPropertyDescriptor;var Pd=Object.getOwnPropertyNames;var Ed=Object.getPrototypeOf,gd=Object.prototype.hasOwnProperty;var A=(t,e)=>()=>(e||t((e={exports:{}}).exports,e),e.exports);var Ad=(t,e,r,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of Pd(e))!gd.call(t,s)&&s!==r&&ko(t,s,{get:()=>e[s],enumerable:!(i=xd(e,s))||i.enumerable});return t};var Ce=(t,e,r)=>(r=t!=null?Sd(Ed(t)):{},Ad(e||!t||!t.__esModule?ko(r,"default",{value:t,enumerable:!0}):r,t));var Dr=A(Xi=>{Object.defineProperty(Xi,"__esModule",{value:!0});Xi.default=vd;function vd(t,e){let r=Object.keys(e);for(let i of r)if(t[i]!==e[i])return !1;return !0}});var Vt=A(Ji=>{Object.defineProperty(Ji,"__esModule",{value:!0});Ji.default=Id;var _o=new Set;function Id(t,e,r=""){if(_o.has(t))return;_o.add(t);let{internal:i,trace:s}=wd(1,2);i||console.warn(`${r}\`${t}\` has been deprecated, please migrate to \`${e}\` ${s}`);}function wd(t,e){let{stackTraceLimit:r,prepareStackTrace:i}=Error,s;if(Error.stackTraceLimit=1+t+e,Error.prepareStackTrace=function(n,o){s=o;},new Error().stack,Error.stackTraceLimit=r,Error.prepareStackTrace=i,!s)return {internal:!1,trace:""};let a=s.slice(1+t,1+t+e);return {internal:/[\\/]@babel[\\/]/.test(a[1].getFileName()),trace:a.map(n=>` at ${n}`).join(` @@ -50,17 +50,17 @@ ${e}`,i}}}});var yp=A(Ie=>{Object.defineProperty(Ie,"__esModule",{value:!0});Ie. function ${u.name}(s) { return typeof s === 'function' || (Object.prototype.toString.call(s) === '[object Object]' && !${p.name}.isVNode(s)); } - `,v=e.get("body").filter(I=>I.isVariableDeclaration()&&I.node.declarations.some(N=>{var M;return ((M=N.id)==null?void 0:M.name)===n.name})).pop();return v&&v.insertAfter(S),u});}let{opts:{pragma:s=""},file:a}=r;if(s&&r.set("createVNode",()=>he.identifier(s)),a.ast.comments)for(let n of a.ast.comments){let o=h_.exec(n.value);o&&r.set("createVNode",()=>he.identifier(o[1]));}}},exit(e){let r=e.get("body"),i=new Map;r.filter(a=>he.isImportDeclaration(a.node)&&a.node.source.value==="vue").forEach(a=>{let{specifiers:n}=a.node,o=!1;n.forEach(l=>{!l.loc&&he.isImportSpecifier(l)&&he.isIdentifier(l.imported)&&(i.set(l.imported.name,l),o=!0);}),o&&a.remove();});let s=[...i.keys()].map(a=>i.get(a));s.length&&e.unshiftContainer("body",he.importDeclaration(s,he.stringLiteral("vue")));}}})});var md=Ce(hd());function No(t){let[,e,r]=t.match(/([^.]+)\.([^.]+)$/)||[];return {basename:e,lang:r}}var lt="__sfc__";function yd({babel:t,availablePlugins:e={},availablePresets:r={}}){function i(l){return t.transformSync(l,{presets:[[r.env??"env",{modules:"cjs"}]]})}function s(l,u,p={}){let{lang:S,plugins:E=[],presets:v=[]}=p;if(S==="ts"||S==="tsx"){let M=S==="tsx";v.push([r.typescript??"typescript",{isTSX:M,allExtensions:M,onlyRemoveTypeImports:!0}]);}(S==="tsx"||S==="jsx")&&E.push(e["vue-jsx"]??"vue-jsx");let{basename:I}=No(u);return t.transformSync(l,{filename:I+"."+(S||"ts"),presets:v,plugins:E})?.code||""}function a(l,u,p,S){let{filename:E,template:v,scriptSetup:I,script:N}=u,M=v?.content,j=!!I,R=S==="ts"||S==="tsx",k=[];R&&k.push("typescript"),(S==="jsx"||S==="tsx")&&k.push("jsx");let $="";if(N||I)try{let{content:oe}=compileScript(u,{id:l,inlineTemplate:j,templateOptions:{compilerOptions:{expressionPlugins:k}}});$=s(rewriteDefault(oe,lt,k),E,{lang:S});}catch(oe){return [oe]}else $=`const ${lt} = {};`;if(!j&&M){let{code:oe,errors:ne}=compileTemplate({id:l,filename:E,source:M,scoped:p,compilerOptions:{expressionPlugins:k}});if(ne.length)return ne;oe=s(oe,E,{lang:S}),$+=` + `,v=e.get("body").filter(I=>I.isVariableDeclaration()&&I.node.declarations.some(N=>{var M;return ((M=N.id)==null?void 0:M.name)===n.name})).pop();return v&&v.insertAfter(S),u});}let{opts:{pragma:s=""},file:a}=r;if(s&&r.set("createVNode",()=>he.identifier(s)),a.ast.comments)for(let n of a.ast.comments){let o=h_.exec(n.value);o&&r.set("createVNode",()=>he.identifier(o[1]));}}},exit(e){let r=e.get("body"),i=new Map;r.filter(a=>he.isImportDeclaration(a.node)&&a.node.source.value==="vue").forEach(a=>{let{specifiers:n}=a.node,o=!1;n.forEach(l=>{!l.loc&&he.isImportSpecifier(l)&&he.isIdentifier(l.imported)&&(i.set(l.imported.name,l),o=!0);}),o&&a.remove();});let s=[...i.keys()].map(a=>i.get(a));s.length&&e.unshiftContainer("body",he.importDeclaration(s,he.stringLiteral("vue")));}}})});var Td=Ce(hd());function No(t){let[,e,r]=t.match(/([^.]+)\.([^.]+)$/)||[];return {basename:e,lang:r}}var lt="__sfc__";function yd({babel:t,availablePlugins:e={},availablePresets:r={}}){function i(l){return t.transformSync(l,{presets:[[r.env??"env",{modules:"cjs"}]]})}function s(l,u,p={}){let{lang:S,plugins:E=[],presets:v=[]}=p;if(S==="ts"||S==="tsx"){let M=S==="tsx";v.push([r.typescript??"typescript",{isTSX:M,allExtensions:M,onlyRemoveTypeImports:!0}]);}(S==="tsx"||S==="jsx")&&E.push(e["vue-jsx"]??"vue-jsx");let{basename:I}=No(u);return t.transformSync(l,{filename:I+"."+(S||"ts"),presets:v,plugins:E})?.code||""}function a(l,u,p,S){let{filename:E,template:v,scriptSetup:I,script:N}=u,M=v?.content,j=!!I,R=S==="ts"||S==="tsx",k=[];R&&k.push("typescript"),(S==="jsx"||S==="tsx")&&k.push("jsx");let $="";if(N||I)try{let{content:oe}=compileScript(u,{id:l,inlineTemplate:j,templateOptions:{compilerOptions:{expressionPlugins:k}}});$=s(rewriteDefault(oe,lt,k),E,{lang:S});}catch(oe){return [oe]}else $=`const ${lt} = {};`;if(!j&&M){let{code:oe,errors:ne}=compileTemplate({id:l,filename:E,source:M,scoped:p,compilerOptions:{expressionPlugins:k}});if(ne.length)return ne;oe=s(oe,E,{lang:S}),$+=` ${oe.replace(/\nexport (function|const) render/,"$1 render")} ${lt}.render = render;`;}return $}function n(l,u){let{filename:p}=u,S=[];for(let E of u.styles){let{code:v,errors:I}=compileStyle({source:E.content,filename:p,id:l,scoped:E.scoped});if(I.length)return I;S.push(v);}return S.join(` `)}function o(l){let{id:u,code:p,filename:S}=l,{descriptor:E,errors:v}=parse(p,{filename:S});if(v.length)return v;let I="",N=!1;(E.styles.some($=>$.lang)||E.template&&E.template.lang)&&(N=!0,I+=` console.warn("Custom preprocessors for