From 6d63caadc118a2b177eee4f7b6fd579a927ce0c4 Mon Sep 17 00:00:00 2001 From: jeffwcx Date: Thu, 28 Sep 2023 00:43:01 +0800 Subject: [PATCH 01/32] feat: add vue tech stack support --- .eslintignore | 1 + assets-types/typings/example.d.ts | 4 + examples/normal/src/Foo/index.md | 3 +- examples/vue/.dumirc.ts | 42 + examples/vue/.eslintrc.js | 14 + examples/vue/.fatherrc.ts | 14 + examples/vue/README.md | 1 + .../vue/docs/framework-test/external/App.vue | 299 + examples/vue/docs/framework-test/index.md | 96 + examples/vue/docs/index.md | 20 + examples/vue/package.json | 32 + examples/vue/src/Button/button.less | 12 + examples/vue/src/Button/demos/Demo.tsx | 19 + examples/vue/src/Button/demos/demo.less | 4 + examples/vue/src/Button/index.md | 83 + examples/vue/src/Button/index.tsx | 42 + examples/vue/src/Foo/demos/sfc-demo.vue | 21 + examples/vue/src/Foo/index.md | 65 + examples/vue/src/Foo/index.tsx | 14 + examples/vue/src/index.ts | 2 + examples/vue/src/test.svg | 1 + examples/vue/tsconfig.json | 12 + package.json | 7 +- packages/babel-plugin-iife/.fatherrc.ts | 8 + packages/babel-plugin-iife/.gitignore | 3 + packages/babel-plugin-iife/README.md | 39 + packages/babel-plugin-iife/package.json | 45 + .../__snapshots__/integration.test.ts.snap | 43 + .../__snapshots__/snapshot.test.ts.snap | 69 + .../src/__tests__/integration.test.ts | 52 + .../src/__tests__/snapshot.test.ts | 48 + .../babel-plugin-iife/src/__tests__/utils.ts | 22 + packages/babel-plugin-iife/src/index.ts | 80 + packages/babel-plugin-iife/src/utils.ts | 79 + packages/babel-plugin-iife/tsconfig.json | 8 + packages/dumi-plugin-vue/.fatherrc.ts | 5 + packages/dumi-plugin-vue/.gitignore | 3 + packages/dumi-plugin-vue/README.md | 52 + packages/dumi-plugin-vue/package.json | 51 + .../runtime/getPreviewerData.ts | 238 + .../runtime/globalInject.ts.tpl | 11 + packages/dumi-plugin-vue/runtime/render.ts | 25 + packages/dumi-plugin-vue/runtime/runtime.tsx | 19 + packages/dumi-plugin-vue/src/genRuntimeApi.ts | 45 + packages/dumi-plugin-vue/src/index.ts | 57 + packages/dumi-plugin-vue/src/requireHook.ts | 48 + .../dumi-plugin-vue/src/techStack/index.ts | 18 + packages/dumi-plugin-vue/src/techStack/jsx.ts | 34 + .../__snapshots__/compile.test.ts.snap | 178 + .../techStack/sfc/__tests__/compile.test.ts | 175 + .../src/techStack/sfc/compile.ts | 180 + .../src/techStack/sfc/index.ts | 45 + .../src/vueBabelLoaderCustomize.ts | 20 + .../dumi-plugin-vue/src/webpack/config.ts | 110 + packages/dumi-plugin-vue/src/webpack/index.ts | 23 + packages/dumi-plugin-vue/tsconfig.json | 11 + pnpm-lock.yaml | 8133 ++++++++++------- pnpm-workspace.yaml | 1 + src/assetParsers/block.ts | 48 +- src/client/pages/Demo/index.ts | 13 +- src/client/theme-api/DumiDemo.tsx | 27 +- src/client/theme-api/context.ts | 19 +- src/client/theme-api/index.ts | 18 +- src/client/theme-api/reactDemoRuntimeApi.ts | 162 + src/client/theme-api/types.ts | 14 + src/client/theme-api/useRenderer.ts | 36 + .../builtins/SourceCode/index.tsx | 5 +- .../slots/PreviewerActions/index.tsx | 9 +- src/constants.ts | 2 + src/features/compile/index.ts | 2 +- src/index.ts | 4 +- src/loaders/markdown/index.ts | 8 +- .../transformer/fixtures/demo/expect.ts | 2 +- src/loaders/markdown/transformer/index.ts | 5 + .../markdown/transformer/rehypeDemo.ts | 19 +- src/types.ts | 24 +- 76 files changed, 7636 insertions(+), 3567 deletions(-) create mode 100644 examples/vue/.dumirc.ts create mode 100644 examples/vue/.eslintrc.js create mode 100644 examples/vue/.fatherrc.ts create mode 100644 examples/vue/README.md create mode 100644 examples/vue/docs/framework-test/external/App.vue create mode 100644 examples/vue/docs/framework-test/index.md create mode 100644 examples/vue/docs/index.md create mode 100644 examples/vue/package.json create mode 100644 examples/vue/src/Button/button.less create mode 100644 examples/vue/src/Button/demos/Demo.tsx create mode 100644 examples/vue/src/Button/demos/demo.less create mode 100644 examples/vue/src/Button/index.md create mode 100644 examples/vue/src/Button/index.tsx create mode 100644 examples/vue/src/Foo/demos/sfc-demo.vue create mode 100644 examples/vue/src/Foo/index.md create mode 100644 examples/vue/src/Foo/index.tsx create mode 100644 examples/vue/src/index.ts create mode 100644 examples/vue/src/test.svg create mode 100644 examples/vue/tsconfig.json create mode 100644 packages/babel-plugin-iife/.fatherrc.ts create mode 100644 packages/babel-plugin-iife/.gitignore create mode 100644 packages/babel-plugin-iife/README.md create mode 100644 packages/babel-plugin-iife/package.json create mode 100644 packages/babel-plugin-iife/src/__tests__/__snapshots__/integration.test.ts.snap create mode 100644 packages/babel-plugin-iife/src/__tests__/__snapshots__/snapshot.test.ts.snap create mode 100644 packages/babel-plugin-iife/src/__tests__/integration.test.ts create mode 100644 packages/babel-plugin-iife/src/__tests__/snapshot.test.ts create mode 100644 packages/babel-plugin-iife/src/__tests__/utils.ts create mode 100644 packages/babel-plugin-iife/src/index.ts create mode 100644 packages/babel-plugin-iife/src/utils.ts create mode 100644 packages/babel-plugin-iife/tsconfig.json create mode 100644 packages/dumi-plugin-vue/.fatherrc.ts create mode 100644 packages/dumi-plugin-vue/.gitignore create mode 100644 packages/dumi-plugin-vue/README.md create mode 100644 packages/dumi-plugin-vue/package.json create mode 100644 packages/dumi-plugin-vue/runtime/getPreviewerData.ts create mode 100644 packages/dumi-plugin-vue/runtime/globalInject.ts.tpl create mode 100644 packages/dumi-plugin-vue/runtime/render.ts create mode 100644 packages/dumi-plugin-vue/runtime/runtime.tsx create mode 100644 packages/dumi-plugin-vue/src/genRuntimeApi.ts create mode 100644 packages/dumi-plugin-vue/src/index.ts create mode 100644 packages/dumi-plugin-vue/src/requireHook.ts create mode 100644 packages/dumi-plugin-vue/src/techStack/index.ts create mode 100644 packages/dumi-plugin-vue/src/techStack/jsx.ts create mode 100644 packages/dumi-plugin-vue/src/techStack/sfc/__tests__/__snapshots__/compile.test.ts.snap create mode 100644 packages/dumi-plugin-vue/src/techStack/sfc/__tests__/compile.test.ts create mode 100644 packages/dumi-plugin-vue/src/techStack/sfc/compile.ts create mode 100644 packages/dumi-plugin-vue/src/techStack/sfc/index.ts create mode 100644 packages/dumi-plugin-vue/src/vueBabelLoaderCustomize.ts create mode 100644 packages/dumi-plugin-vue/src/webpack/config.ts create mode 100644 packages/dumi-plugin-vue/src/webpack/index.ts create mode 100644 packages/dumi-plugin-vue/tsconfig.json create mode 100644 src/client/theme-api/reactDemoRuntimeApi.ts create mode 100644 src/client/theme-api/useRenderer.ts diff --git a/.eslintignore b/.eslintignore index 8b3073efc7..67718eb6b7 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,3 +1,4 @@ /dist /compiled /theme-default +/runtime diff --git a/assets-types/typings/example.d.ts b/assets-types/typings/example.d.ts index fa33f9bf33..45add911d0 100644 --- a/assets-types/typings/example.d.ts +++ b/assets-types/typings/example.d.ts @@ -64,6 +64,10 @@ export interface ExampleBlockAsset extends ExampleBaseAsset { value: string; } >; + /** + * Entry file name, you can find the relevant entry file content from `dependencies` + */ + entry: string; } /** diff --git a/examples/normal/src/Foo/index.md b/examples/normal/src/Foo/index.md index f10b5b17a2..38999bf04a 100644 --- a/examples/normal/src/Foo/index.md +++ b/examples/normal/src/Foo/index.md @@ -1,5 +1,6 @@ 组件路由测试 ```jsx -export default () => 'Hello Foo!'; +import { Foo } from '@examples/normal'; +export default () => ; ``` diff --git a/examples/vue/.dumirc.ts b/examples/vue/.dumirc.ts new file mode 100644 index 0000000000..64a4733fa6 --- /dev/null +++ b/examples/vue/.dumirc.ts @@ -0,0 +1,42 @@ +// import AutoImport from 'unplugin-auto-import/webpack'; +// import Components from 'unplugin-vue-components/webpack'; +// import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'; + +export default { + mfsu: false, + apiParser: {}, + resolve: { + entryFile: './src/index.ts', + }, + plugins: ['dumi-plugin-vue'], + themeConfig: { + nav: [ + { title: 'SFC', link: '/components/foo' }, + { title: 'JSX', link: '/components/button' }, + { title: '3rd party framework', link: '/framework-test' }, + ], + // vue: { + // globalInject: { + // imports: ` + // import ElementPlus from 'element-plus'; + // import 'element-plus/dist/index.css'; + // `, + // statements: `app.use(ElementPlus, { size: 'small', zIndex: 3000 });` + // }, + // }, + }, + chainWebpack(memo: any) { + memo.plugin('unplugin-element-plus').use( + require('unplugin-element-plus/webpack')({ + useSource: true, + }), + ); + // memo.plugin('auto-import').use(AutoImport( { + // resolvers: [ElementPlusResolver()], + // })); + // memo.plugin('components').use(Components({ + // resolvers: [ElementPlusResolver()], + // })); + return memo; + }, +}; diff --git a/examples/vue/.eslintrc.js b/examples/vue/.eslintrc.js new file mode 100644 index 0000000000..f2ace260ca --- /dev/null +++ b/examples/vue/.eslintrc.js @@ -0,0 +1,14 @@ +module.exports = { + root: true, + parser: 'vue-eslint-parser', + parserOptions: { + parser: '@typescript-eslint/parser', + sourceType: 'module', + ecmaVersion: 2020, + ecmaFeatures: { + jsx: true, + }, + }, + extends: ['plugin:vue/vue3-recommended'], + rules: {}, +}; diff --git a/examples/vue/.fatherrc.ts b/examples/vue/.fatherrc.ts new file mode 100644 index 0000000000..029ca01d93 --- /dev/null +++ b/examples/vue/.fatherrc.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'father'; + +export default defineConfig({ + extraBabelPlugins: [ + [ + '@babel/plugin-transform-typescript', + { isTSX: true, allExtensions: true }, + ], + '@vue/babel-plugin-jsx', + ], + esm: { + transformer: 'babel', + }, +}); diff --git a/examples/vue/README.md b/examples/vue/README.md new file mode 100644 index 0000000000..f3be33510e --- /dev/null +++ b/examples/vue/README.md @@ -0,0 +1 @@ +# @exmaples/vue diff --git a/examples/vue/docs/framework-test/external/App.vue b/examples/vue/docs/framework-test/external/App.vue new file mode 100644 index 0000000000..cc3dbb9fcd --- /dev/null +++ b/examples/vue/docs/framework-test/external/App.vue @@ -0,0 +1,299 @@ + + + diff --git a/examples/vue/docs/framework-test/index.md b/examples/vue/docs/framework-test/index.md new file mode 100644 index 0000000000..b5cc9e6775 --- /dev/null +++ b/examples/vue/docs/framework-test/index.md @@ -0,0 +1,96 @@ +# Element Plus + +暂不支持组件的全局导入,但支持按需导入 + +轻量级 Demo 只支持组件代码的导入,需自行导入样式 + +```vue + + + +``` + +外置组件可以通过配置 webpack 来实现组件的自动导入或是按需导入 + + + +这里使用手动按需导入 + +```ts +// import AutoImport from 'unplugin-auto-import/webpack'; +// import Components from 'unplugin-vue-components/webpack'; +// import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'; + +export default { + chainWebpack(memo) { + memo.plugin('unplugin-element-plus').use( + require('unplugin-element-plus/webpack')({ + useSource: true, + }), + ); + // memo.plugin('auto-import').use(AutoImport( { + // resolvers: [ElementPlusResolver()], + // })); + // memo.plugin('components').use(Components({ + // resolvers: [ElementPlusResolver()], + // })); + return memo; + }, +}; +``` + +## TSX + +```tsx +import { ElBreadcrumb, ElBreadcrumbItem } from 'element-plus'; +import 'element-plus/es/components/breadcrumb/style/css'; +import { defineComponent } from 'vue'; + +const tableData = [ + { + date: '2016-05-03', + name: 'Tom', + address: 'No. 189, Grove St, Los Angeles', + }, + { + date: '2016-05-02', + name: 'Tom', + address: 'No. 189, Grove St, Los Angeles', + }, + { + date: '2016-05-04', + name: 'Tom', + address: 'No. 189, Grove St, Los Angeles', + }, + { + date: '2016-05-01', + name: 'Tom', + address: 'No. 189, Grove St, Los Angeles', + }, +]; +export default defineComponent({ + setup() { + return () => ( + + homepage + + promotion management + + promotion list + promotion detail + + ); + }, +}); +``` diff --git a/examples/vue/docs/index.md b/examples/vue/docs/index.md new file mode 100644 index 0000000000..379095afa7 --- /dev/null +++ b/examples/vue/docs/index.md @@ -0,0 +1,20 @@ +--- +hero: + title: Vue.js Support + description: A demo about Vue SFC and JSX + actions: + - text: SFC + link: /components/foo + - text: JSX + link: /components/button +features: + - title: SFC + emoji: 🤟 + description: Put hello description here + - title: JSX + emoji: ⚛️ + description: Put world description here + - title: TSX + emoji: ⚛️ + description: Put ! description here +--- diff --git a/examples/vue/package.json b/examples/vue/package.json new file mode 100644 index 0000000000..67be395537 --- /dev/null +++ b/examples/vue/package.json @@ -0,0 +1,32 @@ +{ + "name": "@exmaples/vue", + "description": "A Vue3 component library", + "license": "MIT", + "scripts": { + "build": "node ../../bin/dumi.js build", + "dev": "node ../../bin/dumi.js dev", + "preview": "node ../../bin/dumi.js preview", + "setup": "node ../../bin/dumi.js setup", + "start": "npm run dev" + }, + "dependencies": { + "@babel/plugin-transform-typescript": "^7.21.3", + "@vue/babel-plugin-jsx": "^1.1.1", + "dayjs": "^1.11.10", + "element-plus": "^2.3.14", + "pinia": "^2.1.6", + "react": "^18.2.0", + "vue": "^3.3.4" + }, + "devDependencies": { + "@typescript-eslint/parser": "^6.7.2", + "dumi-plugin-vue": "workspace:*", + "eslint-plugin-vue": "^9.17.0", + "typescript": "~4.7.4", + "unplugin-auto-import": "^0.16.6", + "unplugin-element-plus": "^0.8.0", + "unplugin-vue-components": "^0.25.2", + "vue-loader": "^17.0.1" + }, + "authors": [] +} diff --git a/examples/vue/src/Button/button.less b/examples/vue/src/Button/button.less new file mode 100644 index 0000000000..a03f01ba0e --- /dev/null +++ b/examples/vue/src/Button/button.less @@ -0,0 +1,12 @@ +.btn { + --btn-color: rgb(93, 93, 245); + + padding: 10px; + min-width: 100px; + height: 50px; + border: 1px solid var(--btn-color); + border-radius: 50px; + font-size: 16px; + background: var(--btn-color); + color: #fff; +} diff --git a/examples/vue/src/Button/demos/Demo.tsx b/examples/vue/src/Button/demos/Demo.tsx new file mode 100644 index 0000000000..893110dfef --- /dev/null +++ b/examples/vue/src/Button/demos/Demo.tsx @@ -0,0 +1,19 @@ +import { Button } from '@exmaples/vue'; +import { defineComponent, ref } from 'vue'; +import './demo.less'; + +export default defineComponent({ + setup() { + const count = ref(0); + const handleClick = () => { + count.value++; + }; + return () => ( +
+ +
+ ); + }, +}); diff --git a/examples/vue/src/Button/demos/demo.less b/examples/vue/src/Button/demos/demo.less new file mode 100644 index 0000000000..958e882960 --- /dev/null +++ b/examples/vue/src/Button/demos/demo.less @@ -0,0 +1,4 @@ +.demo { + padding: 100px; + background: aliceblue; +} diff --git a/examples/vue/src/Button/index.md b/examples/vue/src/Button/index.md new file mode 100644 index 0000000000..2f61935393 --- /dev/null +++ b/examples/vue/src/Button/index.md @@ -0,0 +1,83 @@ +# JSX Support + +## Lightweight Demo + Composition API + JSX + +```jsx +/** + * title: jsx + */ +import { defineComponent } from 'vue'; +import { Button } from '@exmaples/vue'; + +export default defineComponent({ + setup() { + function handleClick() { + alert('Using defineComponent API'); + } + return () => ; + }, +}); +``` + +## Lightweight Inline Demo + Options API + JSX + +```jsx | inline +import { Button } from '@exmaples/vue'; + +export default { + data() { + return { + msg: 'Using Options API', + }; + }, + methods: { + handleClick() { + alert(this.msg); + }, + }, + render() { + return ; + }, +}; +``` + +## External Demo + TSX + + + +## Functional Component + +```tsx +interface ArticleProps { + title?: string; + desc?: string; +} + +function Article(props: ArticleProps) { + return ( +
+

{props.title}

+

{props.desc}

+
+ ); +} + +Article.props = { + title: { + type: String, + required: false, + default: 'Functional Component Demo', + }, + desc: { + type: String, + required: false, + default: 'No Desc here', + }, +}; + +export default Article; +``` + +## The API table is not supported yet + + diff --git a/examples/vue/src/Button/index.tsx b/examples/vue/src/Button/index.tsx new file mode 100644 index 0000000000..40f21d790d --- /dev/null +++ b/examples/vue/src/Button/index.tsx @@ -0,0 +1,42 @@ +import { defineComponent, PropType } from 'vue'; +import './button.less'; + +export const props = { + /** + * @description 按钮文字左侧的图标 + */ + icon: { + type: String, + default: '', + }, + /** + * @description 点击事件 + */ + onClick: { + type: [Function] as PropType<(e: MouseEvent) => void>, + default: () => {}, + }, +}; + +export default defineComponent({ + inheritAttrs: false, + props, + setup(props, { emit, slots, attrs }) { + function handleClick(e: MouseEvent) { + emit('click', e); + } + return () => { + const buttonProps = { + ...attrs, + class: 'btn', + onClick: handleClick, + }; + const { icon } = props; + return ( + + ); + }; + }, +}); diff --git a/examples/vue/src/Foo/demos/sfc-demo.vue b/examples/vue/src/Foo/demos/sfc-demo.vue new file mode 100644 index 0000000000..6c0b5bb83b --- /dev/null +++ b/examples/vue/src/Foo/demos/sfc-demo.vue @@ -0,0 +1,21 @@ + + + + + diff --git a/examples/vue/src/Foo/index.md b/examples/vue/src/Foo/index.md new file mode 100644 index 0000000000..cec1ea291c --- /dev/null +++ b/examples/vue/src/Foo/index.md @@ -0,0 +1,65 @@ +# SFC Support + +## Script Setup (Composition) + Scoped + +```vue + + + + + +``` + +## Options API + +```vue + + + + + +``` + +## External Demo + + diff --git a/examples/vue/src/Foo/index.tsx b/examples/vue/src/Foo/index.tsx new file mode 100644 index 0000000000..c63adae5e0 --- /dev/null +++ b/examples/vue/src/Foo/index.tsx @@ -0,0 +1,14 @@ +import { defineComponent } from 'vue'; + +export default defineComponent({ + inheritAttrs: false, + props: { + title: { + type: String, + default: '', + }, + }, + setup({ title }) { + return () =>
{title}
; + }, +}); diff --git a/examples/vue/src/index.ts b/examples/vue/src/index.ts new file mode 100644 index 0000000000..0518018563 --- /dev/null +++ b/examples/vue/src/index.ts @@ -0,0 +1,2 @@ +export { default as Button } from './Button'; +export { default as Foo } from './Foo'; diff --git a/examples/vue/src/test.svg b/examples/vue/src/test.svg new file mode 100644 index 0000000000..bd5ba06292 --- /dev/null +++ b/examples/vue/src/test.svg @@ -0,0 +1 @@ + diff --git a/examples/vue/tsconfig.json b/examples/vue/tsconfig.json new file mode 100644 index 0000000000..fdb8ed05b1 --- /dev/null +++ b/examples/vue/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": "./", + "paths": { + "@exmaples/vue": ["./src"] + }, + "jsx": "preserve", + "jsxImportSource": "vue" + }, + "include": [".dumi/**/*", ".dumirc.ts", "src/**/*", "docs/**/*"] +} diff --git a/package.json b/package.json index dbf27167fe..446c28bee1 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "build": "father build && npm run build:crates", "build:crates": "cargo build --target wasm32-wasi -r --out-dir compiled/crates -Z unstable-options", "build:deps": "node scripts/pre-bundle-worker.js && father prebundle", + "build:packages": "pnpm run --filter=\"./packages/**\" build", "build:suites": "pnpm run --filter=\"./suites/**\" build", "dev": "father dev", "docs:build": "node ./bin/dumi.js build", @@ -47,7 +48,7 @@ "lint": "npm run lint:es && npm run lint:css", "lint:css": "stylelint \"{src,test}/**/*.{css,less}\"", "lint:es": "eslint \"{src,test}/**/*.{js,jsx,ts,tsx}\"", - "prepare": "husky install && npm run build && npm run build:suites && node ./bin/dumi.js setup && npm run docs:sync", + "prepare": "husky install && npm run build && npm run build:packages && npm run build:suites && node ./bin/dumi.js setup && npm run docs:sync", "prepublishOnly": "npm run build", "test": "vitest", "vercel:build": "npm run docs:build", @@ -77,6 +78,9 @@ }, "dependencies": { "@ant-design/icons-svg": "^4.2.1", + "@babel/plugin-transform-typescript": "^7.21.3", + "@babel/preset-env": "^7.21.4", + "@babel/preset-typescript": "^7.21.4", "@makotot/ghostui": "^2.0.0", "@stackblitz/sdk": "^1.9.0", "@swc/core": "1.3.72", @@ -153,6 +157,7 @@ "@types/react-copy-to-clipboard": "^5.0.4", "@umijs/lint": "^4.0.83", "@umijs/plugins": "4.0.32", + "codesandbox-import-utils": "^2.2.3", "dumi-theme-mobile": "workspace:*", "eslint": "^8.46.0", "fast-glob": "^3.3.1", diff --git a/packages/babel-plugin-iife/.fatherrc.ts b/packages/babel-plugin-iife/.fatherrc.ts new file mode 100644 index 0000000000..1cc7a58364 --- /dev/null +++ b/packages/babel-plugin-iife/.fatherrc.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'father'; + +export default defineConfig({ + cjs: {}, + prebundle: { + deps: {}, + }, +}); diff --git a/packages/babel-plugin-iife/.gitignore b/packages/babel-plugin-iife/.gitignore new file mode 100644 index 0000000000..891437fe08 --- /dev/null +++ b/packages/babel-plugin-iife/.gitignore @@ -0,0 +1,3 @@ +/node_modules +/dist +.DS_Store diff --git a/packages/babel-plugin-iife/README.md b/packages/babel-plugin-iife/README.md new file mode 100644 index 0000000000..f863d94fb8 --- /dev/null +++ b/packages/babel-plugin-iife/README.md @@ -0,0 +1,39 @@ +# babel-plugin-iife + +Convert module files into iife execution statements + +## Install + +```bash +$ pnpm i babel-plugin-iife +``` + +## Configuration in babel + +```json +{ + "plugins": [["babel-plugin-iife", {}]] +} +``` + +## Options + +### wrappedByIIFE + +Type: `boolean` + +Default: true + +Whether to use IIFE module, the default is true + +### forceAsync?: boolean + +Type: `boolean` + +Default: `undefined` + +It can be set only when wrappedByIIFE is true, which is used to specify the callee function in IIFE as an asynchronous function. + +## LICENSE + +MIT diff --git a/packages/babel-plugin-iife/package.json b/packages/babel-plugin-iife/package.json new file mode 100644 index 0000000000..98738ad568 --- /dev/null +++ b/packages/babel-plugin-iife/package.json @@ -0,0 +1,45 @@ +{ + "name": "babel-plugin-iife", + "version": "0.0.1", + "description": "Babel plugin for wrapping demo code as iife", + "keywords": [], + "license": "MIT", + "main": "dist/cjs/index.js", + "types": "dist/cjs/index.d.ts", + "files": [ + "dist", + "compiled" + ], + "scripts": { + "build": "father build", + "build:deps": "father prebundle", + "dev": "father dev", + "prepublishOnly": "father doctor && npm run build", + "test": "vitest" + }, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/template": "^7.22.15", + "@babel/traverse": "^7.22.20", + "@babel/types": "^7.22.19" + }, + "devDependencies": { + "@babel/core": "^7.21.4", + "@babel/preset-env": "^7.21.4", + "@types/babel__core": "^7.20.2", + "@types/babel__helper-plugin-utils": "^7.10.1", + "@types/babel__template": "^7.4.2", + "@types/babel__traverse": "^7.20.2", + "@vue/babel-plugin-jsx": "^1.1.1", + "father": "^4.1.9" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + }, + "publishConfig": { + "access": "public" + }, + "authors": [ + "jeffwcx " + ] +} diff --git a/packages/babel-plugin-iife/src/__tests__/__snapshots__/integration.test.ts.snap b/packages/babel-plugin-iife/src/__tests__/__snapshots__/integration.test.ts.snap new file mode 100644 index 0000000000..e8b46ed9da --- /dev/null +++ b/packages/babel-plugin-iife/src/__tests__/__snapshots__/integration.test.ts.snap @@ -0,0 +1,43 @@ +// Vitest Snapshot v1 + +exports[`options test > forceAsync = false 1`] = ` +"(async function () { + const forceAsync = false; + return { + default: forceAsync + }; +})();" +`; + +exports[`options test > wrappedByIIFE = false 1`] = ` +"const { + default: a +} = await import('a'); +return { + default: a +};" +`; + +exports[`use with other plugins 1`] = ` +"(async function () { + const { + createVNode: _createVNode, + createTextVNode: _createTextVNode + } = await import(\\"vue\\"); + const { + defineComponent + } = await import('vue'); + const { + Button + } = await import('@exmaples/vue3'); + return { + default: defineComponent({ + setup() { + return () => _createVNode(Button, null, { + default: () => [_createTextVNode(\\"hello\\")] + }); + } + }) + }; +})();" +`; diff --git a/packages/babel-plugin-iife/src/__tests__/__snapshots__/snapshot.test.ts.snap b/packages/babel-plugin-iife/src/__tests__/__snapshots__/snapshot.test.ts.snap new file mode 100644 index 0000000000..0c50766ee3 --- /dev/null +++ b/packages/babel-plugin-iife/src/__tests__/__snapshots__/snapshot.test.ts.snap @@ -0,0 +1,69 @@ +// Vitest Snapshot v1 + +exports[`babel-plugin-iife > export default \`class\` 1`] = ` +"(function () { + ; + return { + default: class A {} + }; +})();" +`; + +exports[`babel-plugin-iife > export default \`function\` 1`] = ` +"(function () { + return { + default: () => null + }; +})();" +`; + +exports[`babel-plugin-iife > export default \`value\` 1`] = ` +"(function () { + return { + default: a + }; +})();" +`; + +exports[`babel-plugin-iife > export named 1`] = ` +"(function () { + const name1 = 1, + name2 = 2; + ; + ; + return { + x: function x() {}, + A: class A {} + }; +})();" +`; + +exports[`babel-plugin-iife > should wrapped by iife 1`] = ` +"(function () { + const a = 1; +})();" +`; + +exports[`babel-plugin-iife > static import -> dynamic import 1`] = ` +"(async function () { + const { + default: a + } = await import('a'); + const { + b + } = await import('b'); + const { + c1: c + } = await import('c'); + const d = await import('d'); + const { + default: e, + e1, + e2: e3 + } = await import('e'); + const { + createVNode: _createVNode, + createTextVNode: _createTextVNode + } = await import(\\"vue\\"); +})();" +`; diff --git a/packages/babel-plugin-iife/src/__tests__/integration.test.ts b/packages/babel-plugin-iife/src/__tests__/integration.test.ts new file mode 100644 index 0000000000..0b2ba20f19 --- /dev/null +++ b/packages/babel-plugin-iife/src/__tests__/integration.test.ts @@ -0,0 +1,52 @@ +import { describe, expect, test } from 'vitest'; +import iifePlugin from '..'; +import { transpile } from './utils'; + +test('use with other plugins', async () => { + expect( + await transpile( + ` + import { defineComponent } from 'vue'; + import { Button } from '@exmaples/vue3'; + export default defineComponent({ + setup() { + return () => ; + }, + }); + `, + { + plugins: [require.resolve('@vue/babel-plugin-jsx'), iifePlugin], + }, + ), + ).toMatchSnapshot(); +}); + +describe('options test', () => { + test('wrappedByIIFE = false', async () => { + expect( + await transpile( + ` + import a from 'a'; + export default a; + `, + { + plugins: [[iifePlugin, { wrappedByIIFE: false }]], + }, + ), + ).toMatchSnapshot(); + }); + + test('forceAsync = false', async () => { + expect( + await transpile( + ` + const forceAsync = false; + export default forceAsync; + `, + { + plugins: [[iifePlugin, { forceAsync: true }]], + }, + ), + ).toMatchSnapshot(); + }); +}); diff --git a/packages/babel-plugin-iife/src/__tests__/snapshot.test.ts b/packages/babel-plugin-iife/src/__tests__/snapshot.test.ts new file mode 100644 index 0000000000..a1580fe74d --- /dev/null +++ b/packages/babel-plugin-iife/src/__tests__/snapshot.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, test } from 'vitest'; +import { transpile } from './utils'; + +describe('babel-plugin-iife', () => { + [ + { + name: 'should wrapped by iife', + from: 'const a = 1;', + }, + { + name: 'static import -> dynamic import', + from: ` + import a from 'a'; + import { b } from 'b'; + import { c1 as c } from 'c'; + import * as d from 'd'; + import e, { e1, e2 as e3 } from 'e'; + import { createVNode as _createVNode, createTextVNode as _createTextVNode } from "vue"; + `, + }, + { + name: 'export default `value`', + from: 'export default a;', + }, + { + name: 'export default `class`', + from: 'export default class A {};', + }, + { + name: 'export default `function`', + from: 'export default () => null;', + }, + { + name: 'export named', + from: ` + const name1 = 1, name2 = 2; + export { name1, name2 }; + export let name3 = 1; + export function x () {}; + export class A {}; + `, + }, + ].forEach(({ name, from }) => { + test(name, async () => { + expect(await transpile(from)).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/babel-plugin-iife/src/__tests__/utils.ts b/packages/babel-plugin-iife/src/__tests__/utils.ts new file mode 100644 index 0000000000..7151e8f6ce --- /dev/null +++ b/packages/babel-plugin-iife/src/__tests__/utils.ts @@ -0,0 +1,22 @@ +import { transform, TransformOptions } from '@babel/core'; +import plugin from '..'; + +export const transpile = (source: string, options?: TransformOptions) => + new Promise((resolve, reject) => { + transform( + source, + { + filename: '', + presets: null, + plugins: [plugin], + configFile: false, + ...options, + }, + (error, result) => { + if (error) { + return reject(error); + } + resolve(result?.code); + }, + ); + }); diff --git a/packages/babel-plugin-iife/src/index.ts b/packages/babel-plugin-iife/src/index.ts new file mode 100644 index 0000000000..132dd403a1 --- /dev/null +++ b/packages/babel-plugin-iife/src/index.ts @@ -0,0 +1,80 @@ +import { declare } from '@babel/helper-plugin-utils'; +import * as t from '@babel/types'; +import { + createDynamicImport, + createExportObjectProperty, + createIIFE, + isModule, +} from './utils'; + +export interface DemoTransformOptions { + /** + * Whether to use IIFE module, the default is true + */ + wrappedByIIFE?: boolean; + /** + * It can be set only when wrappedByIIFE is true, + * which is used to specify the callee function in IIFE as an asynchronous function. + */ + forceAsync?: boolean; +} + +const ASYNC_KEY = 'async'; +const EXPORTS_KEY = 'exports'; + +export default declare((_, opts: DemoTransformOptions) => { + const { wrappedByIIFE = true, forceAsync } = opts; + return { + name: 'babel-plugin-iife', + visitor: { + Program: { + enter(path) { + if (!isModule(path)) return; + if (wrappedByIIFE) { + this.set(ASYNC_KEY, forceAsync ?? false); + } + this.set(EXPORTS_KEY, []); + }, + exit(path) { + if (!isModule(path)) return; + + const exportList = this.get(EXPORTS_KEY); + if (exportList.length > 0) { + path.pushContainer( + 'body', + t.returnStatement(t.objectExpression(exportList)), + ); + } + + path.replaceWith( + t.program( + wrappedByIIFE + ? [createIIFE(path.node.body, this.get(ASYNC_KEY))] + : path.node.body, + ), + ); + }, + }, + ImportDeclaration: { + enter() { + if (!wrappedByIIFE || forceAsync !== undefined) return; + if (this.get(ASYNC_KEY)) return; + this.set(ASYNC_KEY, true); + }, + exit(path) { + const node = createDynamicImport(path.node); + path.replaceWith(node); + }, + }, + ExportDeclaration: { + exit(path) { + const prop = createExportObjectProperty(path.node); + if (prop) { + this.get(EXPORTS_KEY).push(prop); + } + path.remove(); + }, + }, + }, + }; +}); diff --git a/packages/babel-plugin-iife/src/utils.ts b/packages/babel-plugin-iife/src/utils.ts new file mode 100644 index 0000000000..ba86104a29 --- /dev/null +++ b/packages/babel-plugin-iife/src/utils.ts @@ -0,0 +1,79 @@ +import type * as BabelCore from '@babel/core'; +import * as t from '@babel/types'; + +export function createDynamicImport(importDeclaration: t.ImportDeclaration) { + const content = importDeclaration.specifiers.map((specifier) => { + if (t.isImportNamespaceSpecifier(specifier)) { + return t.identifier(specifier.local.name); + } + if (t.isImportDefaultSpecifier(specifier)) { + return t.objectProperty( + t.identifier('default'), + t.identifier(specifier.local.name), + ); + } + const keyName = t.isStringLiteral(specifier.imported) + ? specifier.imported.value + : specifier.imported.name; + return t.objectProperty( + t.identifier(keyName), + t.identifier(specifier.local.name), + false, + keyName === specifier.local.name, + ); + }); + let id: t.Identifier | t.ObjectPattern; + if (t.isIdentifier(content[0])) { + id = content[0]; + } else { + id = t.objectPattern(content as t.ObjectProperty[]); + } + const vd = t.variableDeclarator( + id, + t.awaitExpression(t.callExpression(t.import(), [importDeclaration.source])), + ); + return t.variableDeclaration('const', [vd]); +} + +export function createExportObjectProperty(node: t.ExportDeclaration) { + if (t.isExportAllDeclaration(node) || !node.declaration) return; + + let { declaration } = node; + if ( + t.isTSDeclareFunction(declaration) || + t.isVariableDeclaration(declaration) + ) + return; + + let name = 'default'; + if ( + t.isClassDeclaration(declaration) || + t.isFunctionDeclaration(declaration) + ) { + if (declaration.id && t.isExportNamedDeclaration(node)) { + name = declaration.id.name; + } + declaration = t.toExpression(declaration); + } + if (!t.isExpression(declaration)) return; + return t.objectProperty(t.identifier(name), declaration); +} + +export function createIIFE(statements: t.Statement[], async: boolean) { + return t.expressionStatement( + t.callExpression( + t.functionExpression( + null, + [], + t.blockStatement(statements), + false, + async, + ), + [], + ), + ); +} + +export function isModule(path: BabelCore.NodePath) { + return path.node.sourceType === 'module'; +} diff --git a/packages/babel-plugin-iife/tsconfig.json b/packages/babel-plugin-iife/tsconfig.json new file mode 100644 index 0000000000..c10487a267 --- /dev/null +++ b/packages/babel-plugin-iife/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "strict": true, + "declaration": true, + "skipLibCheck": true, + "baseUrl": "./" + } +} diff --git a/packages/dumi-plugin-vue/.fatherrc.ts b/packages/dumi-plugin-vue/.fatherrc.ts new file mode 100644 index 0000000000..b468752a15 --- /dev/null +++ b/packages/dumi-plugin-vue/.fatherrc.ts @@ -0,0 +1,5 @@ +import { defineConfig } from 'father'; + +export default defineConfig({ + cjs: { output: 'dist' }, +}); diff --git a/packages/dumi-plugin-vue/.gitignore b/packages/dumi-plugin-vue/.gitignore new file mode 100644 index 0000000000..891437fe08 --- /dev/null +++ b/packages/dumi-plugin-vue/.gitignore @@ -0,0 +1,3 @@ +/node_modules +/dist +.DS_Store diff --git a/packages/dumi-plugin-vue/README.md b/packages/dumi-plugin-vue/README.md new file mode 100644 index 0000000000..0209ca1186 --- /dev/null +++ b/packages/dumi-plugin-vue/README.md @@ -0,0 +1,52 @@ +# dumi-plugin-vue + +dumi Vue3 technology stack support + +## Features + +[x] Supports both Single File Component and JSX/TSX + +[x] Inline demo and external demo support + +[x] Support CodeSandbox and StackBlitz preview + +[x] Webpack processing + +## Install + +``` +npm i dumi-plugin-vue +``` + +## Options + +### globalInject + +Global script injection + +Vue’s UI framework basically has the usage of global import. Users can use this option to achieve global import. The following configuration can import `ElementPlus`. + +```js +themeConfig: { + vue: { + globalInject: { + imports: ` + import ElementPlus from 'element-plus'; + import 'element-plus/dist/index.css'; + `, + statements: `app.use(ElementPlus, { size: 'small', zIndex: 3000 });` + }, + }, +}, +``` + +The plugin will insert relevant statements into the following templates + +```ts +import { App } from 'vue'; +// imports here + +export function globalInject(app: App) { + // statements here +} +``` diff --git a/packages/dumi-plugin-vue/package.json b/packages/dumi-plugin-vue/package.json new file mode 100644 index 0000000000..a993080160 --- /dev/null +++ b/packages/dumi-plugin-vue/package.json @@ -0,0 +1,51 @@ +{ + "name": "dumi-plugin-vue", + "version": "0.0.1", + "description": "vue3 support plugin for dumi", + "keywords": [], + "license": "MIT", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist", + "compiled", + "runtime" + ], + "scripts": { + "build": "father build", + "build:deps": "father prebundle", + "dev": "father dev", + "prepublishOnly": "father doctor && npm run build" + }, + "dependencies": { + "@babel/core": "^7.21.4", + "@babel/plugin-syntax-typescript": "^7.22.5", + "@babel/preset-env": "^7.21.4", + "@babel/preset-typescript": "^7.21.4", + "@umijs/bundler-webpack": "^4.0.81", + "@vue/babel-plugin-jsx": "^1.1.1", + "babel-plugin-iife": "workspace:*", + "hash-sum": "^2.0.0", + "sucrase": "^3.34.0", + "vue": "^3.2.47", + "vue-loader": "^17.0.1", + "vue-template-compiler": "^2.7.14" + }, + "devDependencies": { + "@stackblitz/sdk": "^1.9.0", + "@types/babel__core": "^7.20.0", + "@types/hash-sum": "^1.0.0", + "codesandbox-import-utils": "^2.2.3", + "dumi": "workspace:*", + "father": "^4.1.9" + }, + "peerDependencies": { + "dumi": "^2.0.0" + }, + "publishConfig": { + "access": "public" + }, + "authors": [ + "jeffwcx " + ] +} diff --git a/packages/dumi-plugin-vue/runtime/getPreviewerData.ts b/packages/dumi-plugin-vue/runtime/getPreviewerData.ts new file mode 100644 index 0000000000..91981e20b6 --- /dev/null +++ b/packages/dumi-plugin-vue/runtime/getPreviewerData.ts @@ -0,0 +1,238 @@ +import StackBlitzSDK, { Project } from '@stackblitz/sdk'; +import { IFiles, getParameters } from 'codesandbox-import-utils/lib/api/define'; +import { IPreviewerProps } from 'dumi'; + +const defaultTitle = 'vue demo'; +const defaultDesc = 'An auto-generated vue demo by dumi'; + +const CSB_API_ENDPOINT = 'https://codesandbox.io/api/v1/sandboxes/define'; + +const genIndexHtml = ( + title: string, + description: string, + entryFile: string, +) => { + return ` + + + + + + + ${title} + + +
+ + + +`; +}; + +const genViteConfig = (tsx: boolean, sourceDir: string) => { + return ` +import { fileURLToPath, URL } from 'node:url'; + +import { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; +${tsx ? `import vueJsx from '@vitejs/plugin-vue-jsx';` : ''} + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + vue(),${tsx ? 'vueJsx()' : ''} + ], + resolve: { + alias: { + '@': fileURLToPath(new URL('./${sourceDir}', import.meta.url)) + } + } +});`; +}; + +const genRenderCode = (mainFileName: string) => ` +import { createApp } from 'vue'; +import App from './${mainFileName}'; + +const app = createApp(App); +app.config.errorHandler = (err) => console.error(err); +app.mount('#app'); +`; + +const tsconfig = ` +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], + "exclude": ["src/**/__tests__/*"], + "compilerOptions": { + "composite": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +}`; + +// the corresponding preprocessor map +const extDepMap: Record = { + less: ['less', '^4.2.0'], + scss: ['sass', '^1.68.0'], + sass: ['sass', '^1.68.0'], + styl: ['stylus', '^0.60.0'], +}; + +function resolve(filename: string) { + const match = /(.+)\.(\w+)$/i.exec(filename); + if (!match) return match; + const [, name, ext] = match; + return { name, ext }; +} + +//entryFileName can be index.tsx, index.vue, index.jsx +//The configuration of index.vue is processed as a TypeScript project +function getVueApp({ + asset, + title = defaultTitle, + description = defaultDesc, +}: IPreviewerProps) { + let entryFileName = asset.entry; + const result = resolve(entryFileName); + if (!result) return {}; + const { name, ext } = result; + const isVue = ext === 'vue'; + let isTs = isVue || ext === 'tsx'; + const sourceDir = 'src/'; + + const files: IFiles = { + [`vite.config.${isTs ? 'ts' : 'js'}`]: { + content: genViteConfig(isTs, sourceDir), + isBinary: false, + }, + }; + + if (isTs) { + files['tsconfig.json'] = { content: tsconfig, isBinary: false }; + } + + const deps: Record = {}; + const devDeps: Record = { + '@vitejs/plugin-vue': '~4.0.0', + vite: '~4.0.0', + }; + + const mainFileName = name === 'index' ? `App.${ext}` : entryFileName; + + Object.entries(asset.dependencies).forEach(([name, { type, value }]) => { + if (type === 'NPM') { + // generate dependencies + deps[name] = value; + } else { + // append other imported local files + files[ + name === entryFileName + ? `${sourceDir}${mainFileName}` + : `${sourceDir}${name}` + ] = { + content: value, + isBinary: false, + }; + } + // Add style preprocessor dependency + const result = resolve(name); + if (!result) return; + const dep = extDepMap[result.ext]; + if (!dep) return; + const [depName, verison] = dep; + devDeps[depName] = verison; + }); + + deps['vue'] ??= '^3.3'; + deps['vue-router'] ??= '^4.2'; + + const previewerEntryFileName = isTs + ? `${sourceDir}index.ts` + : `${sourceDir}index.js`; + + if (isTs) { + Object.assign(devDeps, { + '@tsconfig/node18': '~18.2.2', + '@types/node': '~18.17.17', + '@vitejs/plugin-vue-jsx': '~3.0.2', + '@vue/tsconfig': '~0.4.0', + typescript: '~5.2.0', + 'vue-tsc': '~1.8.11', + }); + } + + // append package.json + files['package.json'] = { + content: JSON.stringify( + { + description, + main: previewerEntryFileName, + dependencies: deps, + scripts: { + dev: 'vite', + build: 'vite build', + preview: 'vite preview', + }, + browserslist: ['> 0.2%', 'not dead'], + // add TypeScript dependency if required, must in devDeps to avoid csb compile error + devDependencies: devDeps, + }, + null, + 2, + ), + isBinary: false, + }; + + files['index.html'] = { + content: genIndexHtml(title, description, previewerEntryFileName), + isBinary: false, + }; + files[previewerEntryFileName] = { + content: genRenderCode(mainFileName), + isBinary: false, + }; + + return files; +} + +export function openCodeSandbox(props: IPreviewerProps) { + const form = document.createElement('form'); + const input = document.createElement('input'); + const CSBData = { files: getVueApp(props) }; + form.method = 'POST'; + form.target = '_blank'; + form.style.display = 'none'; + form.action = props?.api || CSB_API_ENDPOINT; + form.appendChild(input); + form.setAttribute('data-demo', props.assets?.id || ''); + + input.name = 'parameters'; + input.value = getParameters(CSBData); + + document.body.appendChild(form); + + form.submit(); + form.remove(); +} + +export function openStackBlitz(props: IPreviewerProps) { + const { title, description } = props; + const config: Project = { + title: title || defaultTitle, + description, + template: 'node', + files: {}, + dependencies: {}, + }; + + const files = getVueApp(props); + + config.files = Object.entries(files).reduce((acc, [k, v]) => { + acc[k] = v.content; + return acc; + }, {} as Record); + StackBlitzSDK.openProject(config); +} diff --git a/packages/dumi-plugin-vue/runtime/globalInject.ts.tpl b/packages/dumi-plugin-vue/runtime/globalInject.ts.tpl new file mode 100644 index 0000000000..4e9f920bc0 --- /dev/null +++ b/packages/dumi-plugin-vue/runtime/globalInject.ts.tpl @@ -0,0 +1,11 @@ +import { App } from 'vue'; +{{#globalInject.imports}} + {{{globalInject.imports}}} +{{/globalInject.imports}} + +export function globalInject (app: App) { + // do something here +{{#globalInject.statements}} + {{{globalInject.statements}}} +{{/globalInject.statements}} +} diff --git a/packages/dumi-plugin-vue/runtime/render.ts b/packages/dumi-plugin-vue/runtime/render.ts new file mode 100644 index 0000000000..bda4a12500 --- /dev/null +++ b/packages/dumi-plugin-vue/runtime/render.ts @@ -0,0 +1,25 @@ +import { createApp } from 'vue'; +import { globalInject } from './globalInject'; + +export async function renderToCanvas(canvas: Element, component: any) { + if (component.__css__) { + setTimeout(() => { + document + .querySelectorAll(`style[css-${component.__id__}]`) + .forEach((el) => el.remove()); + document.head.insertAdjacentHTML( + 'beforeend', + ``, + ); + }, 1); + } + const app = createApp(component); + + globalInject(app); + + app.config.errorHandler = (e) => console.error(e); + app.mount(canvas); + return () => { + app.unmount(); + }; +} diff --git a/packages/dumi-plugin-vue/runtime/runtime.tsx b/packages/dumi-plugin-vue/runtime/runtime.tsx new file mode 100644 index 0000000000..37c6f91d3c --- /dev/null +++ b/packages/dumi-plugin-vue/runtime/runtime.tsx @@ -0,0 +1,19 @@ +import { TechStackRuntimeContext } from 'dumi'; +import React from 'react'; +import { openCodeSandbox, openStackBlitz } from './getPreviewerData'; +import { renderToCanvas } from './render'; + +const vueTechStackRuntimeApi = { + techStackName: 'vue3', + openCodeSandbox, + openStackBlitz, + renderToCanvas, +}; + +export function rootContainer(container: React.ReactNode) { + return ( + + {container} + + ); +} diff --git a/packages/dumi-plugin-vue/src/genRuntimeApi.ts b/packages/dumi-plugin-vue/src/genRuntimeApi.ts new file mode 100644 index 0000000000..4d85b971a6 --- /dev/null +++ b/packages/dumi-plugin-vue/src/genRuntimeApi.ts @@ -0,0 +1,45 @@ +import { extname, join } from 'path'; +import type { IApi } from 'umi'; +import { Mustache, fsExtra, winPath } from 'umi/plugin-utils'; + +export default function (api: IApi) { + // Runtime configuration + // 1. Provide the previewer override methods + // 2. Give the rendering method of non-React framework in React framework + + api.addRuntimePlugin(() => { + return [ + winPath( + join(api.paths.absTmpPath, `plugin-${api.plugin.key}`, 'runtime.tsx'), + ), + ]; + }); + + api.addRuntimePluginKey(() => { + return ['useTechStackRuntimeContext']; + }); + + api.onGenerateFiles(() => { + const runtimePath = join(__dirname, '../runtime'); + const files = fsExtra.readdirSync(runtimePath, { + withFileTypes: true, + }); + + files.forEach((dirent) => { + if (!dirent.isFile()) return; + let content = fsExtra.readFileSync( + join(runtimePath, dirent.name), + 'utf8', + ); + const ext = extname(dirent.name); + if (ext === '.tpl') { + const options = api.userConfig.themeConfig?.vue; + content = Mustache.render(content, options); + } + api.writeTmpFile({ + path: dirent.name.replace('.tpl', ''), + content, + }); + }); + }); +} diff --git a/packages/dumi-plugin-vue/src/index.ts b/packages/dumi-plugin-vue/src/index.ts new file mode 100644 index 0000000000..ae3aa5e41f --- /dev/null +++ b/packages/dumi-plugin-vue/src/index.ts @@ -0,0 +1,57 @@ +import type { IApi } from 'dumi'; +import genRuntimeApi from './genRuntimeApi'; +import './requireHook'; +import registerTechStack from './techStack'; +import modifyWebpackConfig from './webpack'; + +const PLUGIN_KEY = 'dumi:vue'; + +export default (api: IApi) => { + api.describe({ + key: PLUGIN_KEY, + config: { + schema(joi) { + return joi.object({ + globalInject: joi + .object({ + imports: joi.string().optional(), + statements: joi.string().optional(), + }) + .optional(), + }); + }, + }, + }); + + api.modifyBabelPresetOpts((memo) => { + memo.presetTypeScript = { + allExtensions: true, + isTSX: true, + }; + return memo; + }); + + modifyWebpackConfig(api); + + api.modifyConfig((memo) => { + memo.babelLoaderCustomize = require.resolve('./vueBabelLoaderCustomize'); + memo.resolveDemoModule = { + '.vue': { loader: 'tsx', transform: 'html' }, + }; + return memo; + }); + + api.modifyDefaultConfig((config) => { + // feature flags https://link.vuejs.org/feature-flags. + config.define = { + ...config.define, + __VUE_OPTIONS_API__: true, + __VUE_PROD_DEVTOOLS__: false, + }; + return config; + }); + + genRuntimeApi(api); + + registerTechStack(api); +}; diff --git a/packages/dumi-plugin-vue/src/requireHook.ts b/packages/dumi-plugin-vue/src/requireHook.ts new file mode 100644 index 0000000000..8a0a1da223 --- /dev/null +++ b/packages/dumi-plugin-vue/src/requireHook.ts @@ -0,0 +1,48 @@ +// MIT: copy from https://github.com/vercel/next.js/blob/canary/packages/next/build/webpack/require-hook.ts +// sync injects a hook for webpack and webpack/... requires to use the internal ncc webpack version +// this is in order for userland plugins to attach to the same webpack instance as umi +// the individual compiled modules are as defined for the compilation in bundles/webpack/packages/* + +// TODO 之后走预打包处理 +// @ts-ignore +import deepImports from '@umijs/bundler-webpack/compiled/webpack/deepImports.json'; + +const hookPropertyMap = new Map([ + ['webpack', '@umijs/bundler-webpack/compiled/webpack'], + ['webpack/package', '@umijs/bundler-webpack/compiled/webpack/package'], + ['webpack/package.json', '@umijs/bundler-webpack/compiled/webpack/package'], + ['webpack/lib/webpack', '@umijs/bundler-webpack/compiled/webpack'], + ['webpack/lib/webpack.js', '@umijs/bundler-webpack/compiled/webpack'], + ['tapable', '@umijs/bundler-utils/compiled/tapable'], +]); + +deepImports.forEach((item: string) => { + const name = item.split('/').pop(); + + hookPropertyMap.set(item, `@umijs/bundler-webpack/compiled/webpack/${name}`); + hookPropertyMap.set( + `${item}.js`, + `@umijs/bundler-webpack/compiled/webpack/${name}`, + ); +}); + +const mod = require('module'); + +const resolveFilename = mod._resolveFilename; +mod._resolveFilename = function ( + request: string, + parent: any, + isMain: boolean, + options: any, +) { + const hookResolved = hookPropertyMap.get(request); + return resolveFilename.call( + mod, + hookResolved ?? request, + parent, + isMain, + options, + ); +}; + +export { hookPropertyMap }; diff --git a/packages/dumi-plugin-vue/src/techStack/index.ts b/packages/dumi-plugin-vue/src/techStack/index.ts new file mode 100644 index 0000000000..672a310425 --- /dev/null +++ b/packages/dumi-plugin-vue/src/techStack/index.ts @@ -0,0 +1,18 @@ +import type { IApi } from 'umi'; + +import VueJSXTechStack from './jsx'; +import VueSfcTechStack from './sfc'; + +export default function registerTechStack(api: IApi) { + api.register({ + key: 'registerTechStack', + stage: 0, + fn: () => new VueJSXTechStack(), + }); + + api.register({ + key: 'registerTechStack', + stage: 1, + fn: () => new VueSfcTechStack(), + }); +} diff --git a/packages/dumi-plugin-vue/src/techStack/jsx.ts b/packages/dumi-plugin-vue/src/techStack/jsx.ts new file mode 100644 index 0000000000..e653c14819 --- /dev/null +++ b/packages/dumi-plugin-vue/src/techStack/jsx.ts @@ -0,0 +1,34 @@ +import type { PluginItem } from '@babel/core'; +import { transformSync } from '@babel/core'; +import type { IDumiTechStack } from 'dumi'; + +export default class VueJSXTechStack implements IDumiTechStack { + name = 'vue3-tsx'; + + isSupported(...[, lang]: Parameters) { + return ['jsx', 'tsx'].includes(lang); + } + + transformCode(...[raw, opts]: Parameters) { + if (opts.type === 'code-block') { + const isTSX = opts.fileAbsPath.endsWith('.tsx'); + const plugins: PluginItem[] = []; + if (isTSX) { + plugins.push([ + '@babel/plugin-transform-typescript', + { isTSX, allExtensions: isTSX }, + ]); + } + plugins.push( + require.resolve('@vue/babel-plugin-jsx'), + require.resolve('babel-plugin-iife'), + ); + const result = transformSync(raw, { + filename: opts.fileAbsPath, + plugins, + }); + return (result?.code || '').replace(/;$/g, ''); + } + return raw; + } +} diff --git a/packages/dumi-plugin-vue/src/techStack/sfc/__tests__/__snapshots__/compile.test.ts.snap b/packages/dumi-plugin-vue/src/techStack/sfc/__tests__/__snapshots__/compile.test.ts.snap new file mode 100644 index 0000000000..f78da15017 --- /dev/null +++ b/packages/dumi-plugin-vue/src/techStack/sfc/__tests__/__snapshots__/compile.test.ts.snap @@ -0,0 +1,178 @@ +// Vitest Snapshot v1 + +exports[`Vue SFC compilation test > custom preprocessors are not supported 1`] = ` +{ + "css": "", + "js": " +console.warn(\\"Custom preprocessors for