diff --git a/.eslintrc.js b/.eslintrc.js index 5487bda8a0..917518ca30 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,11 +1,15 @@ module.exports = { root: true, - parser: '@babel/eslint-parser', + parser: '@typescript-eslint/parser', parserOptions: { sourceType: 'module' }, - extends: 'standard', + extends: [ + 'plugin:@typescript-eslint/recommended', + 'standard' + ], plugins: [ + '@typescript-eslint', 'html', 'jest' ], @@ -27,10 +31,18 @@ module.exports = { getCurrentPages: 'readonly' }, rules: { + '@typescript-eslint/ban-ts-comment': 0, + '@typescript-eslint/no-empty-function': 0, + '@typescript-eslint/no-this-alias': 0, + '@typescript-eslint/no-var-requires': 0, + '@typescript-eslint/explicit-module-boundary-types': 0, 'no-cond-assign': 0, camelcase: 0 }, env: { - 'jest/globals': true + 'jest/globals': true, + es6: true, + browser: true, + node: true } } diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index b97ed05560..c3d25b06ce 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -22,6 +22,7 @@ jobs: - name: generate docs file if: env.GIT_DIFF run: | + cd docs-vueporess npm i npm run docs:build diff --git a/.gitignore b/.gitignore index 124586563b..4050ceff10 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,7 @@ package-lock.json pnpm-lock.yaml yarn.lock .DS_Store -docs-vuepress/.vuepress/dist +docs-vuepress/docs/.vuepress/dist elevate/ +.yarn/ +packages/**/dist/ diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000000..a8a5904397 --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +registry = https://registry.npmjs.org/ +auto-install-peers = true \ No newline at end of file diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000000..50cf8e67e4 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v14.21.1 \ No newline at end of file diff --git a/docs-vuepress/.vuepress/config.js b/docs-vuepress/docs/.vuepress/config.js similarity index 100% rename from docs-vuepress/.vuepress/config.js rename to docs-vuepress/docs/.vuepress/config.js diff --git a/docs-vuepress/.vuepress/enhanceApp.js b/docs-vuepress/docs/.vuepress/enhanceApp.js similarity index 100% rename from docs-vuepress/.vuepress/enhanceApp.js rename to docs-vuepress/docs/.vuepress/enhanceApp.js diff --git a/docs-vuepress/.vuepress/headerMdPlugin.js b/docs-vuepress/docs/.vuepress/headerMdPlugin.js similarity index 100% rename from docs-vuepress/.vuepress/headerMdPlugin.js rename to docs-vuepress/docs/.vuepress/headerMdPlugin.js diff --git a/docs-vuepress/.vuepress/public/favicon.ico b/docs-vuepress/docs/.vuepress/public/favicon.ico similarity index 100% rename from docs-vuepress/.vuepress/public/favicon.ico rename to docs-vuepress/docs/.vuepress/public/favicon.ico diff --git a/docs-vuepress/.vuepress/public/logo.png b/docs-vuepress/docs/.vuepress/public/logo.png similarity index 100% rename from docs-vuepress/.vuepress/public/logo.png rename to docs-vuepress/docs/.vuepress/public/logo.png diff --git a/docs-vuepress/.vuepress/public/manifest.webmanifest b/docs-vuepress/docs/.vuepress/public/manifest.webmanifest similarity index 100% rename from docs-vuepress/.vuepress/public/manifest.webmanifest rename to docs-vuepress/docs/.vuepress/public/manifest.webmanifest diff --git a/docs-vuepress/.vuepress/styles/index.styl b/docs-vuepress/docs/.vuepress/styles/index.styl similarity index 100% rename from docs-vuepress/.vuepress/styles/index.styl rename to docs-vuepress/docs/.vuepress/styles/index.styl diff --git a/docs-vuepress/.vuepress/styles/pallete.styl b/docs-vuepress/docs/.vuepress/styles/pallete.styl similarity index 100% rename from docs-vuepress/.vuepress/styles/pallete.styl rename to docs-vuepress/docs/.vuepress/styles/pallete.styl diff --git a/docs-vuepress/.vuepress/theme/components/AlgoliaSearchBox.vue b/docs-vuepress/docs/.vuepress/theme/components/AlgoliaSearchBox.vue similarity index 100% rename from docs-vuepress/.vuepress/theme/components/AlgoliaSearchBox.vue rename to docs-vuepress/docs/.vuepress/theme/components/AlgoliaSearchBox.vue diff --git a/docs-vuepress/.vuepress/theme/components/CodeList.vue b/docs-vuepress/docs/.vuepress/theme/components/CodeList.vue similarity index 100% rename from docs-vuepress/.vuepress/theme/components/CodeList.vue rename to docs-vuepress/docs/.vuepress/theme/components/CodeList.vue diff --git a/docs-vuepress/.vuepress/theme/components/MobileSwiper.vue b/docs-vuepress/docs/.vuepress/theme/components/MobileSwiper.vue similarity index 100% rename from docs-vuepress/.vuepress/theme/components/MobileSwiper.vue rename to docs-vuepress/docs/.vuepress/theme/components/MobileSwiper.vue diff --git a/docs-vuepress/.vuepress/theme/components/MobileView.vue b/docs-vuepress/docs/.vuepress/theme/components/MobileView.vue similarity index 100% rename from docs-vuepress/.vuepress/theme/components/MobileView.vue rename to docs-vuepress/docs/.vuepress/theme/components/MobileView.vue diff --git a/docs-vuepress/.vuepress/theme/components/Navbar.vue b/docs-vuepress/docs/.vuepress/theme/components/Navbar.vue similarity index 100% rename from docs-vuepress/.vuepress/theme/components/Navbar.vue rename to docs-vuepress/docs/.vuepress/theme/components/Navbar.vue diff --git a/docs-vuepress/.vuepress/theme/components/Popover.vue b/docs-vuepress/docs/.vuepress/theme/components/Popover.vue similarity index 100% rename from docs-vuepress/.vuepress/theme/components/Popover.vue rename to docs-vuepress/docs/.vuepress/theme/components/Popover.vue diff --git a/docs-vuepress/.vuepress/theme/components/SlideItem.vue b/docs-vuepress/docs/.vuepress/theme/components/SlideItem.vue similarity index 100% rename from docs-vuepress/.vuepress/theme/components/SlideItem.vue rename to docs-vuepress/docs/.vuepress/theme/components/SlideItem.vue diff --git a/docs-vuepress/.vuepress/theme/components/Swiper.vue b/docs-vuepress/docs/.vuepress/theme/components/Swiper.vue similarity index 100% rename from docs-vuepress/.vuepress/theme/components/Swiper.vue rename to docs-vuepress/docs/.vuepress/theme/components/Swiper.vue diff --git a/docs-vuepress/.vuepress/theme/components/SwiperImg.vue b/docs-vuepress/docs/.vuepress/theme/components/SwiperImg.vue similarity index 100% rename from docs-vuepress/.vuepress/theme/components/SwiperImg.vue rename to docs-vuepress/docs/.vuepress/theme/components/SwiperImg.vue diff --git a/docs-vuepress/.vuepress/theme/enhanceApp.js b/docs-vuepress/docs/.vuepress/theme/enhanceApp.js similarity index 100% rename from docs-vuepress/.vuepress/theme/enhanceApp.js rename to docs-vuepress/docs/.vuepress/theme/enhanceApp.js diff --git a/docs-vuepress/.vuepress/theme/global-components/Content.vue b/docs-vuepress/docs/.vuepress/theme/global-components/Content.vue similarity index 100% rename from docs-vuepress/.vuepress/theme/global-components/Content.vue rename to docs-vuepress/docs/.vuepress/theme/global-components/Content.vue diff --git a/docs-vuepress/.vuepress/theme/global-components/Footer.vue b/docs-vuepress/docs/.vuepress/theme/global-components/Footer.vue similarity index 100% rename from docs-vuepress/.vuepress/theme/global-components/Footer.vue rename to docs-vuepress/docs/.vuepress/theme/global-components/Footer.vue diff --git a/docs-vuepress/.vuepress/theme/global-components/Header.vue b/docs-vuepress/docs/.vuepress/theme/global-components/Header.vue similarity index 100% rename from docs-vuepress/.vuepress/theme/global-components/Header.vue rename to docs-vuepress/docs/.vuepress/theme/global-components/Header.vue diff --git a/docs-vuepress/.vuepress/theme/index.js b/docs-vuepress/docs/.vuepress/theme/index.js similarity index 100% rename from docs-vuepress/.vuepress/theme/index.js rename to docs-vuepress/docs/.vuepress/theme/index.js diff --git a/docs-vuepress/.vuepress/theme/layouts/GlobalLayout.vue b/docs-vuepress/docs/.vuepress/theme/layouts/GlobalLayout.vue similarity index 100% rename from docs-vuepress/.vuepress/theme/layouts/GlobalLayout.vue rename to docs-vuepress/docs/.vuepress/theme/layouts/GlobalLayout.vue diff --git a/docs-vuepress/.vuepress/theme/layouts/HomepageLayout.vue b/docs-vuepress/docs/.vuepress/theme/layouts/HomepageLayout.vue similarity index 100% rename from docs-vuepress/.vuepress/theme/layouts/HomepageLayout.vue rename to docs-vuepress/docs/.vuepress/theme/layouts/HomepageLayout.vue diff --git a/docs-vuepress/.vuepress/theme/layouts/Layout.vue b/docs-vuepress/docs/.vuepress/theme/layouts/Layout.vue similarity index 100% rename from docs-vuepress/.vuepress/theme/layouts/Layout.vue rename to docs-vuepress/docs/.vuepress/theme/layouts/Layout.vue diff --git a/docs-vuepress/.vuepress/theme/vuepress-format-header-slug-plugin.js b/docs-vuepress/docs/.vuepress/theme/vuepress-format-header-slug-plugin.js similarity index 100% rename from docs-vuepress/.vuepress/theme/vuepress-format-header-slug-plugin.js rename to docs-vuepress/docs/.vuepress/theme/vuepress-format-header-slug-plugin.js diff --git a/docs-vuepress/README.md b/docs-vuepress/docs/README.md similarity index 100% rename from docs-vuepress/README.md rename to docs-vuepress/docs/README.md diff --git a/docs-vuepress/api/ApiIndex.vue b/docs-vuepress/docs/api/ApiIndex.vue similarity index 100% rename from docs-vuepress/api/ApiIndex.vue rename to docs-vuepress/docs/api/ApiIndex.vue diff --git a/docs-vuepress/api/app-config.md b/docs-vuepress/docs/api/app-config.md similarity index 100% rename from docs-vuepress/api/app-config.md rename to docs-vuepress/docs/api/app-config.md diff --git a/docs-vuepress/api/builtIn.md b/docs-vuepress/docs/api/builtIn.md similarity index 100% rename from docs-vuepress/api/builtIn.md rename to docs-vuepress/docs/api/builtIn.md diff --git a/docs-vuepress/api/compile.md b/docs-vuepress/docs/api/compile.md similarity index 100% rename from docs-vuepress/api/compile.md rename to docs-vuepress/docs/api/compile.md diff --git a/docs-vuepress/api/composition-api.md b/docs-vuepress/docs/api/composition-api.md similarity index 100% rename from docs-vuepress/api/composition-api.md rename to docs-vuepress/docs/api/composition-api.md diff --git a/docs-vuepress/api/directives.md b/docs-vuepress/docs/api/directives.md similarity index 100% rename from docs-vuepress/api/directives.md rename to docs-vuepress/docs/api/directives.md diff --git a/docs-vuepress/api/extend.md b/docs-vuepress/docs/api/extend.md similarity index 100% rename from docs-vuepress/api/extend.md rename to docs-vuepress/docs/api/extend.md diff --git a/docs-vuepress/api/global-api.md b/docs-vuepress/docs/api/global-api.md similarity index 100% rename from docs-vuepress/api/global-api.md rename to docs-vuepress/docs/api/global-api.md diff --git a/docs-vuepress/api/index.md b/docs-vuepress/docs/api/index.md similarity index 100% rename from docs-vuepress/api/index.md rename to docs-vuepress/docs/api/index.md diff --git a/docs-vuepress/api/instance-api.md b/docs-vuepress/docs/api/instance-api.md similarity index 100% rename from docs-vuepress/api/instance-api.md rename to docs-vuepress/docs/api/instance-api.md diff --git a/docs-vuepress/api/reactivity-api.md b/docs-vuepress/docs/api/reactivity-api.md similarity index 100% rename from docs-vuepress/api/reactivity-api.md rename to docs-vuepress/docs/api/reactivity-api.md diff --git a/docs-vuepress/api/store-api.md b/docs-vuepress/docs/api/store-api.md similarity index 100% rename from docs-vuepress/api/store-api.md rename to docs-vuepress/docs/api/store-api.md diff --git a/docs-vuepress/articles/1.0.md b/docs-vuepress/docs/articles/1.0.md similarity index 100% rename from docs-vuepress/articles/1.0.md rename to docs-vuepress/docs/articles/1.0.md diff --git a/docs-vuepress/articles/2.0.md b/docs-vuepress/docs/articles/2.0.md similarity index 100% rename from docs-vuepress/articles/2.0.md rename to docs-vuepress/docs/articles/2.0.md diff --git a/docs-vuepress/articles/2.7-release.md b/docs-vuepress/docs/articles/2.7-release.md similarity index 100% rename from docs-vuepress/articles/2.7-release.md rename to docs-vuepress/docs/articles/2.7-release.md diff --git a/docs-vuepress/articles/2.8-release.md b/docs-vuepress/docs/articles/2.8-release.md similarity index 100% rename from docs-vuepress/articles/2.8-release.md rename to docs-vuepress/docs/articles/2.8-release.md diff --git a/docs-vuepress/articles/index.md b/docs-vuepress/docs/articles/index.md similarity index 100% rename from docs-vuepress/articles/index.md rename to docs-vuepress/docs/articles/index.md diff --git a/docs-vuepress/articles/mpx-cli-next.md b/docs-vuepress/docs/articles/mpx-cli-next.md similarity index 100% rename from docs-vuepress/articles/mpx-cli-next.md rename to docs-vuepress/docs/articles/mpx-cli-next.md diff --git a/docs-vuepress/articles/mpx1.md b/docs-vuepress/docs/articles/mpx1.md similarity index 100% rename from docs-vuepress/articles/mpx1.md rename to docs-vuepress/docs/articles/mpx1.md diff --git a/docs-vuepress/articles/mpx2.md b/docs-vuepress/docs/articles/mpx2.md similarity index 100% rename from docs-vuepress/articles/mpx2.md rename to docs-vuepress/docs/articles/mpx2.md diff --git a/docs-vuepress/articles/performance.md b/docs-vuepress/docs/articles/performance.md similarity index 100% rename from docs-vuepress/articles/performance.md rename to docs-vuepress/docs/articles/performance.md diff --git a/docs-vuepress/articles/size-control.md b/docs-vuepress/docs/articles/size-control.md similarity index 100% rename from docs-vuepress/articles/size-control.md rename to docs-vuepress/docs/articles/size-control.md diff --git a/docs-vuepress/articles/ts-derivation.md b/docs-vuepress/docs/articles/ts-derivation.md similarity index 100% rename from docs-vuepress/articles/ts-derivation.md rename to docs-vuepress/docs/articles/ts-derivation.md diff --git a/docs-vuepress/articles/unit-test.md b/docs-vuepress/docs/articles/unit-test.md similarity index 100% rename from docs-vuepress/articles/unit-test.md rename to docs-vuepress/docs/articles/unit-test.md diff --git a/docs-vuepress/assets/images/1666074957603.jpg b/docs-vuepress/docs/assets/images/1666074957603.jpg similarity index 100% rename from docs-vuepress/assets/images/1666074957603.jpg rename to docs-vuepress/docs/assets/images/1666074957603.jpg diff --git a/docs-vuepress/assets/images/cloud.png b/docs-vuepress/docs/assets/images/cloud.png similarity index 100% rename from docs-vuepress/assets/images/cloud.png rename to docs-vuepress/docs/assets/images/cloud.png diff --git a/docs-vuepress/assets/images/select-ts-version.png b/docs-vuepress/docs/assets/images/select-ts-version.png similarity index 100% rename from docs-vuepress/assets/images/select-ts-version.png rename to docs-vuepress/docs/assets/images/select-ts-version.png diff --git a/docs-vuepress/assets/images/start-tips1.png b/docs-vuepress/docs/assets/images/start-tips1.png similarity index 100% rename from docs-vuepress/assets/images/start-tips1.png rename to docs-vuepress/docs/assets/images/start-tips1.png diff --git a/docs-vuepress/assets/images/start-tips2.png b/docs-vuepress/docs/assets/images/start-tips2.png similarity index 100% rename from docs-vuepress/assets/images/start-tips2.png rename to docs-vuepress/docs/assets/images/start-tips2.png diff --git a/docs-vuepress/desc.md b/docs-vuepress/docs/desc.md similarity index 100% rename from docs-vuepress/desc.md rename to docs-vuepress/docs/desc.md diff --git a/docs-vuepress/guide/advance/ability-compatible.md b/docs-vuepress/docs/guide/advance/ability-compatible.md similarity index 100% rename from docs-vuepress/guide/advance/ability-compatible.md rename to docs-vuepress/docs/guide/advance/ability-compatible.md diff --git a/docs-vuepress/guide/advance/async-subpackage.md b/docs-vuepress/docs/guide/advance/async-subpackage.md similarity index 100% rename from docs-vuepress/guide/advance/async-subpackage.md rename to docs-vuepress/docs/guide/advance/async-subpackage.md diff --git a/docs-vuepress/guide/advance/custom-output-path.md b/docs-vuepress/docs/guide/advance/custom-output-path.md similarity index 100% rename from docs-vuepress/guide/advance/custom-output-path.md rename to docs-vuepress/docs/guide/advance/custom-output-path.md diff --git a/docs-vuepress/guide/advance/dll-plugin.md b/docs-vuepress/docs/guide/advance/dll-plugin.md similarity index 100% rename from docs-vuepress/guide/advance/dll-plugin.md rename to docs-vuepress/docs/guide/advance/dll-plugin.md diff --git a/docs-vuepress/guide/advance/i18n.md b/docs-vuepress/docs/guide/advance/i18n.md similarity index 100% rename from docs-vuepress/guide/advance/i18n.md rename to docs-vuepress/docs/guide/advance/i18n.md diff --git a/docs-vuepress/guide/advance/image-process.md b/docs-vuepress/docs/guide/advance/image-process.md similarity index 100% rename from docs-vuepress/guide/advance/image-process.md rename to docs-vuepress/docs/guide/advance/image-process.md diff --git a/docs-vuepress/guide/advance/mixin.md b/docs-vuepress/docs/guide/advance/mixin.md similarity index 100% rename from docs-vuepress/guide/advance/mixin.md rename to docs-vuepress/docs/guide/advance/mixin.md diff --git a/docs-vuepress/guide/advance/npm.md b/docs-vuepress/docs/guide/advance/npm.md similarity index 100% rename from docs-vuepress/guide/advance/npm.md rename to docs-vuepress/docs/guide/advance/npm.md diff --git a/docs-vuepress/guide/advance/pinia.md b/docs-vuepress/docs/guide/advance/pinia.md similarity index 100% rename from docs-vuepress/guide/advance/pinia.md rename to docs-vuepress/docs/guide/advance/pinia.md diff --git a/docs-vuepress/guide/advance/platform.md b/docs-vuepress/docs/guide/advance/platform.md similarity index 100% rename from docs-vuepress/guide/advance/platform.md rename to docs-vuepress/docs/guide/advance/platform.md diff --git a/docs-vuepress/guide/advance/plugin.md b/docs-vuepress/docs/guide/advance/plugin.md similarity index 100% rename from docs-vuepress/guide/advance/plugin.md rename to docs-vuepress/docs/guide/advance/plugin.md diff --git a/docs-vuepress/guide/advance/progressive.md b/docs-vuepress/docs/guide/advance/progressive.md similarity index 100% rename from docs-vuepress/guide/advance/progressive.md rename to docs-vuepress/docs/guide/advance/progressive.md diff --git a/docs-vuepress/guide/advance/resource-resolve.md b/docs-vuepress/docs/guide/advance/resource-resolve.md similarity index 100% rename from docs-vuepress/guide/advance/resource-resolve.md rename to docs-vuepress/docs/guide/advance/resource-resolve.md diff --git a/docs-vuepress/guide/advance/size-report.md b/docs-vuepress/docs/guide/advance/size-report.md similarity index 100% rename from docs-vuepress/guide/advance/size-report.md rename to docs-vuepress/docs/guide/advance/size-report.md diff --git a/docs-vuepress/guide/advance/store.md b/docs-vuepress/docs/guide/advance/store.md similarity index 100% rename from docs-vuepress/guide/advance/store.md rename to docs-vuepress/docs/guide/advance/store.md diff --git a/docs-vuepress/guide/advance/subpackage.md b/docs-vuepress/docs/guide/advance/subpackage.md similarity index 100% rename from docs-vuepress/guide/advance/subpackage.md rename to docs-vuepress/docs/guide/advance/subpackage.md diff --git a/docs-vuepress/guide/basic/class-style-binding.md b/docs-vuepress/docs/guide/basic/class-style-binding.md similarity index 100% rename from docs-vuepress/guide/basic/class-style-binding.md rename to docs-vuepress/docs/guide/basic/class-style-binding.md diff --git a/docs-vuepress/guide/basic/component.md b/docs-vuepress/docs/guide/basic/component.md similarity index 100% rename from docs-vuepress/guide/basic/component.md rename to docs-vuepress/docs/guide/basic/component.md diff --git a/docs-vuepress/guide/basic/conditional-render.md b/docs-vuepress/docs/guide/basic/conditional-render.md similarity index 100% rename from docs-vuepress/guide/basic/conditional-render.md rename to docs-vuepress/docs/guide/basic/conditional-render.md diff --git a/docs-vuepress/guide/basic/css.md b/docs-vuepress/docs/guide/basic/css.md similarity index 100% rename from docs-vuepress/guide/basic/css.md rename to docs-vuepress/docs/guide/basic/css.md diff --git a/docs-vuepress/guide/basic/event.md b/docs-vuepress/docs/guide/basic/event.md similarity index 100% rename from docs-vuepress/guide/basic/event.md rename to docs-vuepress/docs/guide/basic/event.md diff --git a/docs-vuepress/guide/basic/ide.md b/docs-vuepress/docs/guide/basic/ide.md similarity index 100% rename from docs-vuepress/guide/basic/ide.md rename to docs-vuepress/docs/guide/basic/ide.md diff --git a/docs-vuepress/guide/basic/intro.md b/docs-vuepress/docs/guide/basic/intro.md similarity index 100% rename from docs-vuepress/guide/basic/intro.md rename to docs-vuepress/docs/guide/basic/intro.md diff --git a/docs-vuepress/guide/basic/list-render.md b/docs-vuepress/docs/guide/basic/list-render.md similarity index 100% rename from docs-vuepress/guide/basic/list-render.md rename to docs-vuepress/docs/guide/basic/list-render.md diff --git a/docs-vuepress/guide/basic/reactive.md b/docs-vuepress/docs/guide/basic/reactive.md similarity index 100% rename from docs-vuepress/guide/basic/reactive.md rename to docs-vuepress/docs/guide/basic/reactive.md diff --git a/docs-vuepress/guide/basic/refs.md b/docs-vuepress/docs/guide/basic/refs.md similarity index 100% rename from docs-vuepress/guide/basic/refs.md rename to docs-vuepress/docs/guide/basic/refs.md diff --git a/docs-vuepress/guide/basic/single-file.md b/docs-vuepress/docs/guide/basic/single-file.md similarity index 100% rename from docs-vuepress/guide/basic/single-file.md rename to docs-vuepress/docs/guide/basic/single-file.md diff --git a/docs-vuepress/guide/basic/start.md b/docs-vuepress/docs/guide/basic/start.md similarity index 100% rename from docs-vuepress/guide/basic/start.md rename to docs-vuepress/docs/guide/basic/start.md diff --git a/docs-vuepress/guide/basic/template.md b/docs-vuepress/docs/guide/basic/template.md similarity index 100% rename from docs-vuepress/guide/basic/template.md rename to docs-vuepress/docs/guide/basic/template.md diff --git a/docs-vuepress/guide/basic/two-way-binding.md b/docs-vuepress/docs/guide/basic/two-way-binding.md similarity index 100% rename from docs-vuepress/guide/basic/two-way-binding.md rename to docs-vuepress/docs/guide/basic/two-way-binding.md diff --git a/docs-vuepress/guide/composition-api/composition-api.md b/docs-vuepress/docs/guide/composition-api/composition-api.md similarity index 100% rename from docs-vuepress/guide/composition-api/composition-api.md rename to docs-vuepress/docs/guide/composition-api/composition-api.md diff --git a/docs-vuepress/guide/composition-api/reactive-api.md b/docs-vuepress/docs/guide/composition-api/reactive-api.md similarity index 100% rename from docs-vuepress/guide/composition-api/reactive-api.md rename to docs-vuepress/docs/guide/composition-api/reactive-api.md diff --git a/docs-vuepress/guide/extend/api-proxy.md b/docs-vuepress/docs/guide/extend/api-proxy.md similarity index 100% rename from docs-vuepress/guide/extend/api-proxy.md rename to docs-vuepress/docs/guide/extend/api-proxy.md diff --git a/docs-vuepress/guide/extend/fetch.md b/docs-vuepress/docs/guide/extend/fetch.md similarity index 100% rename from docs-vuepress/guide/extend/fetch.md rename to docs-vuepress/docs/guide/extend/fetch.md diff --git a/docs-vuepress/guide/extend/index.md b/docs-vuepress/docs/guide/extend/index.md similarity index 100% rename from docs-vuepress/guide/extend/index.md rename to docs-vuepress/docs/guide/extend/index.md diff --git a/docs-vuepress/guide/extend/mock.md b/docs-vuepress/docs/guide/extend/mock.md similarity index 100% rename from docs-vuepress/guide/extend/mock.md rename to docs-vuepress/docs/guide/extend/mock.md diff --git a/docs-vuepress/docs/guide/extend/request.md b/docs-vuepress/docs/guide/extend/request.md new file mode 100644 index 0000000000..e49d226a90 --- /dev/null +++ b/docs-vuepress/docs/guide/extend/request.md @@ -0,0 +1,131 @@ +# 网络请求 + + +Mpx 提供了网络请求库 fetch,抹平了微信,阿里等平台请求参数及响应数据的差异;同时支持请求拦截器,请求取消等 + + +## 使用说明 + +```js +import mpx from '@mpxjs/core' +import mpxFetch from '@mpxjs/fetch' +mpx.use(mpxFetch) +// 第一种访问形式 +mpx.xfetch.fetch({ + url: 'http://xxx.com' +}).then(res => { + console.log(res.data) +}) + +mpx.createApp({ + onLaunch() { + // 第二种访问形式 + this.$xfetch.fetch({url: 'http://test.com'}) + } +}) +``` + +## 导出说明 + +mpx-fetch提供了一个实例 **xfetch** ,该实例包含以下api + +- fetch(config), 正常的promisify风格的请求方法 +- CancelToken,实例属性,用于创建一个取消请求的凭证。 +- interceptors,实例属性,用于添加拦截器,包含两个属性,request & response + +## 请求拦截器 + +```js +mpx.xfetch.interceptors.request.use(function(config) { + console.log(config) + // 也可以返回promise + return config +}) +mpx.xfetch.interceptors.response.use(function(res) { + console.log(res) + // 也可以返回promise + return res +}) +``` + +## 请求中断 + +```js +const cancelToken = new mpx.xfetch.CancelToken() +mpx.xfetch.fetch({ + url: 'http://xxx.com', + data: { + name: 'test' + }, + cancelToken: cancelToken.token +}) +cancelToken.exec('手动取消请求') // 执行后请求中断,返回abort fail +``` + +## 设置请求参数 +```js +mpx.xfetch.fetch({ + url: 'http://xxx.com', + params: { + name: 'test' + } +}) + +mpx.xfetch.fetch({ + url: 'http://xxx.com', + method: 'POST', + // params 参数等价于 url query + params: { + age: 10 + }, + data: { + name: 'test' + }, + // 设置参数序列化方式,等价于header = {'content-type': 'application/x-www-form-urlencoded'} + emulateJSON: true +}) +``` + +## 设置请求 timeout + +```js +mpx.xfetch.fetch({ + url: 'http://xxx.com', + method: 'POST', + data: { + name: 'test' + }, + timeout: 10000 // 超时时间 +}) +``` + +## 在组合式 API 中使用 {#composition-api-usage} +在组合式 API 中我们提供了 [useFetch](/api/extend.html#usefetch) 方法来访问 `xfetch` 实例对象 + +```js +// app.mpx +import mpx, { createComponent } from '@mpxjs/core' +import { useFetch } from '@mpxjs/fetch' + +createComponent({ + setup() { + useFetch().fetch({ + url: 'http://xxx.com', + method: 'POST', + params: { + age: 10 + }, + data: { + name: 'test' + }, + emulateJSON: true, + usePre: true, + cacheInvalidationTime: 3000, + ignorePreParamKeys: ['timestamp'] + }).then(res => { + console.log(res.data) + }) + } +}) + +``` diff --git a/docs-vuepress/guide/migrate/2.7.md b/docs-vuepress/docs/guide/migrate/2.7.md similarity index 100% rename from docs-vuepress/guide/migrate/2.7.md rename to docs-vuepress/docs/guide/migrate/2.7.md diff --git a/docs-vuepress/guide/migrate/2.8.md b/docs-vuepress/docs/guide/migrate/2.8.md similarity index 100% rename from docs-vuepress/guide/migrate/2.8.md rename to docs-vuepress/docs/guide/migrate/2.8.md diff --git a/docs-vuepress/guide/migrate/mpx-cli-3.md b/docs-vuepress/docs/guide/migrate/mpx-cli-3.md similarity index 100% rename from docs-vuepress/guide/migrate/mpx-cli-3.md rename to docs-vuepress/docs/guide/migrate/mpx-cli-3.md diff --git a/docs-vuepress/guide/tool/e2e-test.md b/docs-vuepress/docs/guide/tool/e2e-test.md similarity index 100% rename from docs-vuepress/guide/tool/e2e-test.md rename to docs-vuepress/docs/guide/tool/e2e-test.md diff --git a/docs-vuepress/guide/tool/ts.md b/docs-vuepress/docs/guide/tool/ts.md similarity index 100% rename from docs-vuepress/guide/tool/ts.md rename to docs-vuepress/docs/guide/tool/ts.md diff --git a/docs-vuepress/guide/tool/unit-test.md b/docs-vuepress/docs/guide/tool/unit-test.md similarity index 100% rename from docs-vuepress/guide/tool/unit-test.md rename to docs-vuepress/docs/guide/tool/unit-test.md diff --git a/docs-vuepress/guide/understand/compile.md b/docs-vuepress/docs/guide/understand/compile.md similarity index 100% rename from docs-vuepress/guide/understand/compile.md rename to docs-vuepress/docs/guide/understand/compile.md diff --git a/docs-vuepress/guide/understand/runtime.md b/docs-vuepress/docs/guide/understand/runtime.md similarity index 100% rename from docs-vuepress/guide/understand/runtime.md rename to docs-vuepress/docs/guide/understand/runtime.md diff --git a/docs-vuepress/package.json b/docs-vuepress/package.json new file mode 100644 index 0000000000..72362c53e8 --- /dev/null +++ b/docs-vuepress/package.json @@ -0,0 +1,14 @@ +{ + "name": "mpx-docs", + "description": "mpx docs", + "private": true, + "scripts": { + "docs:dev": "vuepress dev docs", + "docs:build": "vuepress build docs" + }, + "devDependencies": { + "@vuepress/plugin-back-to-top": "^1.8.2", + "@vuepress/plugin-pwa": "^1.8.0", + "vuepress": "^1.9.7" + } +} diff --git a/examples/mpx-cloud/babel.config.json b/examples/mpx-cloud/babel.config.json index 6be2f424a9..3aad4a5b59 100644 --- a/examples/mpx-cloud/babel.config.json +++ b/examples/mpx-cloud/babel.config.json @@ -13,7 +13,7 @@ "@babel/transform-runtime", { "corejs": 3, - "version": "^7.12.5" + "version": "^7.20.13" } ], [ diff --git a/examples/mpx-cloud/build/package.json b/examples/mpx-cloud/build/package.json index a84ff3ca30..894f7fbe22 100644 --- a/examples/mpx-cloud/build/package.json +++ b/examples/mpx-cloud/build/package.json @@ -1,5 +1,5 @@ { - "name": "build", + "name": "mpx-cloud-build", "version": "1.0.0", "description": "避免webpack通过分析目录依赖错误地将项目package.json中声明的dependencies作为buildDependencies,请勿删除该文件!" } diff --git a/examples/mpx-cloud/config/package.json b/examples/mpx-cloud/config/package.json index a59b4221e8..c266f7fe89 100644 --- a/examples/mpx-cloud/config/package.json +++ b/examples/mpx-cloud/config/package.json @@ -1,5 +1,5 @@ { - "name": "config", + "name": "mpx-cloud-config", "version": "1.0.0", "description": "避免webpack通过分析目录依赖错误地将项目package.json中声明的dependencies作为buildDependencies,请勿删除该文件!" } diff --git a/examples/mpx-cloud/package.json b/examples/mpx-cloud/package.json index 3e42ce5884..d67a217a74 100644 --- a/examples/mpx-cloud/package.json +++ b/examples/mpx-cloud/package.json @@ -1,5 +1,5 @@ { - "name": "mpx-cloud", + "name": "mpx-cloud-demo", "version": "1.0.0", "description": "A mpx project", "main": "index.js", @@ -19,16 +19,16 @@ "author": "sky-admin <546485299@qq.com>", "license": "ISC", "dependencies": { - "@mpxjs/api-proxy": "^2.7.28", - "@mpxjs/core": "^2.7.26" + "@mpxjs/api-proxy": "workspace:*", + "@mpxjs/core": "workspace:*" }, "devDependencies": { "@babel/core": "7.12.10", "@babel/eslint-parser": "7.16.0", "@babel/plugin-transform-runtime": "7.12.10", "@babel/preset-env": "7.12.11", - "@babel/runtime-corejs3": "7.12.5", - "@mpxjs/webpack-plugin": "^2.7.27", + "@babel/runtime-corejs3": "7.20.13", + "@mpxjs/webpack-plugin": "workspace:*", "@typescript-eslint/eslint-plugin": "^5.2.0", "@typescript-eslint/parser": "^5.2.0", "babel-jest": "^25.3.0", diff --git a/examples/mpx-i18n/babel.config.json b/examples/mpx-i18n/babel.config.json index 6be2f424a9..3aad4a5b59 100644 --- a/examples/mpx-i18n/babel.config.json +++ b/examples/mpx-i18n/babel.config.json @@ -13,7 +13,7 @@ "@babel/transform-runtime", { "corejs": 3, - "version": "^7.12.5" + "version": "^7.20.13" } ], [ diff --git a/examples/mpx-i18n/build/package.json b/examples/mpx-i18n/build/package.json index a84ff3ca30..a89946d72b 100644 --- a/examples/mpx-i18n/build/package.json +++ b/examples/mpx-i18n/build/package.json @@ -1,5 +1,5 @@ { - "name": "build", + "name": "mpx-i18n-build", "version": "1.0.0", "description": "避免webpack通过分析目录依赖错误地将项目package.json中声明的dependencies作为buildDependencies,请勿删除该文件!" } diff --git a/examples/mpx-i18n/config/package.json b/examples/mpx-i18n/config/package.json index a59b4221e8..ae7527e23b 100644 --- a/examples/mpx-i18n/config/package.json +++ b/examples/mpx-i18n/config/package.json @@ -1,5 +1,5 @@ { - "name": "config", + "name": "mpx-i18n-config", "version": "1.0.0", "description": "避免webpack通过分析目录依赖错误地将项目package.json中声明的dependencies作为buildDependencies,请勿删除该文件!" } diff --git a/examples/mpx-i18n/package.json b/examples/mpx-i18n/package.json index 241eeca038..8a1703ed49 100644 --- a/examples/mpx-i18n/package.json +++ b/examples/mpx-i18n/package.json @@ -1,5 +1,5 @@ { - "name": "mpx-subpackage-demo", + "name": "mpx-i18n-demo", "version": "1.0.0", "description": "A mpx project", "main": "index.js", @@ -19,16 +19,16 @@ "author": "sky-admin <546485299@qq.com>", "license": "ISC", "dependencies": { - "@mpxjs/api-proxy": "^2.7.28", - "@mpxjs/core": "^2.7.26" + "@mpxjs/api-proxy": "workspace:*", + "@mpxjs/core": "workspace:*" }, "devDependencies": { "@babel/core": "7.12.10", "@babel/eslint-parser": "7.16.0", "@babel/plugin-transform-runtime": "7.12.10", "@babel/preset-env": "7.12.11", - "@babel/runtime-corejs3": "7.12.5", - "@mpxjs/webpack-plugin": "^2.7.27", + "@babel/runtime-corejs3": "7.20.13", + "@mpxjs/webpack-plugin": "workspace:*", "@typescript-eslint/eslint-plugin": "^5.2.0", "@typescript-eslint/parser": "^5.2.0", "babel-jest": "^25.3.0", @@ -59,7 +59,7 @@ "vue-loader": "^15.9.3", "vue-router": "^3.1.3", "vue-style-loader": "^4.1.2", - "vue-template-compiler": "^2.6.10", + "vue-template-compiler": "~2.6.10", "ts-loader": "9.2.6", "typescript": "^4.1.3", "webpack": "^5.72.0", diff --git a/examples/mpx-progressive/babel.config.json b/examples/mpx-progressive/babel.config.json index 6be2f424a9..3aad4a5b59 100644 --- a/examples/mpx-progressive/babel.config.json +++ b/examples/mpx-progressive/babel.config.json @@ -13,7 +13,7 @@ "@babel/transform-runtime", { "corejs": 3, - "version": "^7.12.5" + "version": "^7.20.13" } ], [ diff --git a/examples/mpx-progressive/build/package.json b/examples/mpx-progressive/build/package.json index a84ff3ca30..c214b883a2 100644 --- a/examples/mpx-progressive/build/package.json +++ b/examples/mpx-progressive/build/package.json @@ -1,5 +1,5 @@ { - "name": "build", + "name": "mpx-progressive-build", "version": "1.0.0", "description": "避免webpack通过分析目录依赖错误地将项目package.json中声明的dependencies作为buildDependencies,请勿删除该文件!" } diff --git a/examples/mpx-progressive/config/package.json b/examples/mpx-progressive/config/package.json index a59b4221e8..ef5dd4236f 100644 --- a/examples/mpx-progressive/config/package.json +++ b/examples/mpx-progressive/config/package.json @@ -1,5 +1,5 @@ { - "name": "config", + "name": "mpx-progressive-config", "version": "1.0.0", "description": "避免webpack通过分析目录依赖错误地将项目package.json中声明的dependencies作为buildDependencies,请勿删除该文件!" } diff --git a/examples/mpx-progressive/package.json b/examples/mpx-progressive/package.json index b1021c92ac..a2a69f58dd 100644 --- a/examples/mpx-progressive/package.json +++ b/examples/mpx-progressive/package.json @@ -1,5 +1,5 @@ { - "name": "mpx-subpackage-demo", + "name": "mpx-progressive-demo", "version": "1.0.0", "description": "A mpx project", "main": "index.js", @@ -19,8 +19,8 @@ "author": "sky-admin <546485299@qq.com>", "license": "ISC", "dependencies": { - "@mpxjs/api-proxy": "^2.7.28", - "@mpxjs/core": "^2.7.26" + "@mpxjs/api-proxy": "workspace:*", + "@mpxjs/core": "workspace:*" }, "devDependencies": { "@babel/core": "7.12.10", @@ -28,8 +28,8 @@ "@babel/plugin-syntax-typescript": "^7.16.7", "@babel/plugin-transform-runtime": "7.12.10", "@babel/preset-env": "7.12.11", - "@babel/runtime-corejs3": "7.12.5", - "@mpxjs/webpack-plugin": "^2.7.27", + "@babel/runtime-corejs3": "7.20.13", + "@mpxjs/webpack-plugin": "workspace:*", "@typescript-eslint/eslint-plugin": "^5.2.0", "@typescript-eslint/parser": "^5.2.0", "babel-jest": "^25.3.0", diff --git a/examples/mpx-subpackage/babel.config.json b/examples/mpx-subpackage/babel.config.json index 6be2f424a9..3aad4a5b59 100644 --- a/examples/mpx-subpackage/babel.config.json +++ b/examples/mpx-subpackage/babel.config.json @@ -13,7 +13,7 @@ "@babel/transform-runtime", { "corejs": 3, - "version": "^7.12.5" + "version": "^7.20.13" } ], [ diff --git a/examples/mpx-subpackage/build/package.json b/examples/mpx-subpackage/build/package.json index a84ff3ca30..aca8f718a9 100644 --- a/examples/mpx-subpackage/build/package.json +++ b/examples/mpx-subpackage/build/package.json @@ -1,5 +1,5 @@ { - "name": "build", + "name": "mpx-subpackage-build", "version": "1.0.0", "description": "避免webpack通过分析目录依赖错误地将项目package.json中声明的dependencies作为buildDependencies,请勿删除该文件!" } diff --git a/examples/mpx-subpackage/config/package.json b/examples/mpx-subpackage/config/package.json index a59b4221e8..a13c9d6ca8 100644 --- a/examples/mpx-subpackage/config/package.json +++ b/examples/mpx-subpackage/config/package.json @@ -1,5 +1,5 @@ { - "name": "config", + "name": "mpx-subpackage-config", "version": "1.0.0", "description": "避免webpack通过分析目录依赖错误地将项目package.json中声明的dependencies作为buildDependencies,请勿删除该文件!" } diff --git a/examples/mpx-subpackage/package.json b/examples/mpx-subpackage/package.json index ec23a2c0ec..8472248f89 100644 --- a/examples/mpx-subpackage/package.json +++ b/examples/mpx-subpackage/package.json @@ -19,16 +19,16 @@ "author": "sky-admin <546485299@qq.com>", "license": "ISC", "dependencies": { - "@mpxjs/api-proxy": "^2.7.28", - "@mpxjs/core": "^2.7.26" + "@mpxjs/api-proxy": "workspace:*", + "@mpxjs/core": "workspace:*" }, "devDependencies": { "@babel/core": "7.12.10", "@babel/eslint-parser": "7.16.0", "@babel/plugin-transform-runtime": "7.12.10", "@babel/preset-env": "7.12.11", - "@babel/runtime-corejs3": "7.12.5", - "@mpxjs/webpack-plugin": "^2.7.27", + "@babel/runtime-corejs3": "7.20.13", + "@mpxjs/webpack-plugin": "workspace:*", "@typescript-eslint/eslint-plugin": "^5.2.0", "@typescript-eslint/parser": "^5.2.0", "babel-jest": "^25.3.0", diff --git a/examples/mpx-todoMVC/babel.config.json b/examples/mpx-todoMVC/babel.config.json index 24f07ee7d1..9ab394b138 100644 --- a/examples/mpx-todoMVC/babel.config.json +++ b/examples/mpx-todoMVC/babel.config.json @@ -13,7 +13,7 @@ "@babel/transform-runtime", { "corejs": 3, - "version": "^7.12.5" + "version": "^7.20.13" } ], "lodash" diff --git a/examples/mpx-todoMVC/build/package.json b/examples/mpx-todoMVC/build/package.json index a84ff3ca30..298b32e405 100644 --- a/examples/mpx-todoMVC/build/package.json +++ b/examples/mpx-todoMVC/build/package.json @@ -1,5 +1,5 @@ { - "name": "build", + "name": "mpx-todoMVC-build", "version": "1.0.0", "description": "避免webpack通过分析目录依赖错误地将项目package.json中声明的dependencies作为buildDependencies,请勿删除该文件!" } diff --git a/examples/mpx-todoMVC/config/package.json b/examples/mpx-todoMVC/config/package.json index a59b4221e8..2a2d17a918 100644 --- a/examples/mpx-todoMVC/config/package.json +++ b/examples/mpx-todoMVC/config/package.json @@ -1,5 +1,5 @@ { - "name": "config", + "name": "mpx-todoMVC-config", "version": "1.0.0", "description": "避免webpack通过分析目录依赖错误地将项目package.json中声明的dependencies作为buildDependencies,请勿删除该文件!" } diff --git a/examples/mpx-todoMVC/package.json b/examples/mpx-todoMVC/package.json index 88a51becf3..f513c27ec5 100644 --- a/examples/mpx-todoMVC/package.json +++ b/examples/mpx-todoMVC/package.json @@ -1,5 +1,5 @@ { - "name": "mpx-subpackage-demo", + "name": "mpx-todo-mvc-demo", "version": "1.0.0", "description": "A mpx project", "main": "index.js", @@ -19,16 +19,16 @@ "author": "sky-admin <546485299@qq.com>", "license": "ISC", "dependencies": { - "@mpxjs/api-proxy": "^2.8.0-beta.2", - "@mpxjs/core": "^2.8.0-beta.3" + "@mpxjs/api-proxy": "workspace:*", + "@mpxjs/core": "workspace:*" }, "devDependencies": { "@babel/core": "7.12.10", "@babel/eslint-parser": "7.16.0", "@babel/plugin-transform-runtime": "7.12.10", "@babel/preset-env": "7.12.11", - "@babel/runtime-corejs3": "7.12.5", - "@mpxjs/webpack-plugin": "^2.8.0-beta.2", + "@babel/runtime-corejs3": "7.20.13", + "@mpxjs/webpack-plugin": "workspace:*", "@typescript-eslint/eslint-plugin": "^5.2.0", "@typescript-eslint/parser": "^5.2.0", "babel-jest": "^25.3.0", diff --git a/examples/mpx-transform-web/.gitignore b/examples/mpx-transform-web/.gitignore new file mode 100644 index 0000000000..53f7466aca --- /dev/null +++ b/examples/mpx-transform-web/.gitignore @@ -0,0 +1,5 @@ +node_modules +.DS_Store +dist +dist-ssr +*.local \ No newline at end of file diff --git a/examples/mpx-transform-web/babel.config.json b/examples/mpx-transform-web/babel.config.json new file mode 100644 index 0000000000..58b76917fa --- /dev/null +++ b/examples/mpx-transform-web/babel.config.json @@ -0,0 +1,33 @@ +{ + "presets": [ + [ + "@babel/env", + { + "modules": false, + "shippedProposals": true + } + ] + ], + "plugins": [ + [ + "@babel/transform-runtime", + { + "corejs": 3, + "version": "^7.10.4" + } + ] + ], + "sourceType": "unambiguous", + "env": { + "test": { + "presets": [ + [ + "@babel/env", + { + "shippedProposals": true + } + ] + ] + } + } +} diff --git a/examples/mpx-transform-web/build/build.js b/examples/mpx-transform-web/build/build.js new file mode 100644 index 0000000000..f2d5e04844 --- /dev/null +++ b/examples/mpx-transform-web/build/build.js @@ -0,0 +1,127 @@ +const rm = require('rimraf') +const chalk = require('chalk') +const webpack = require('webpack') +const program = require('commander') +const { userConf, supportedModes } = require('../config/index') +const getWebpackConf = require('./getWebpackConf') +const { resolveDist, getRootPath } = require('./utils') + +program + .option('-w, --watch', 'watch mode') + .option('-p, --production', 'production release') + .parse(process.argv) + +const env = process.env + +const modeStr = env.npm_config_mode || env.npm_config_modes || '' + +const report = env.npm_config_report + +const modes = modeStr.split(/[,|]/) + .map((mode) => { + const modeArr = mode.split(':') + if (supportedModes.includes(modeArr[0])) { + return { + mode: modeArr[0], + env: modeArr[1] + } + } + }).filter((item) => item) + +if (!modes.length) { + modes.push({ + mode: userConf.srcMode + }) +} + +// 开启子进程 +if (userConf.openChildProcess && modes.length > 1) { + let scriptType = '' + const isProduct = program.production + const isWatch = program.watch + if (!isProduct && isWatch) scriptType = 'watch' + if (isProduct && !isWatch) scriptType = 'build' + if (isProduct && isWatch) scriptType = 'watch:prod' + if (!isProduct && !isWatch) scriptType = 'build:dev' + + const spawn = require('child_process').spawn + while (modes.length > 1) { + const modeObj = modes.pop() + const modeAndEnv = modeObj.env ? `${modeObj.mode}:${modeObj.env}` : modeObj.mode + const ls = spawn('npm', ['run', scriptType, `--modes=${modeAndEnv}`, `--mode=${modeAndEnv}`], { stdio: 'inherit' }) + ls.on('close', (code) => { + process.exitCode = code + }) + } +} + +let webpackConfs = [] + +modes.forEach(({ mode, env }) => { + const options = Object.assign({}, userConf, { + mode, + env, + production: program.production, + watch: program.watch, + report, + subDir: (userConf.isPlugin || userConf.cloudFunc) ? 'miniprogram' : '' + }) + webpackConfs.push(getWebpackConf(options)) +}) + +if (webpackConfs.length === 1) { + webpackConfs = webpackConfs[0] +} + + +try { + modes.forEach(({ mode, env }) => { + rm.sync(resolveDist(getRootPath(mode, env), '*')) + }) +} catch (e) { + console.error(e) + console.log('\n\n删除dist文件夹遇到了一些问题,如果遇到问题请手工删除dist重来\n\n') +} + +if (program.watch) { + webpack(webpackConfs).watch(undefined, callback) +} else { + webpack(webpackConfs, callback) +} + +function callback (err, stats) { + if (err) { + process.exitCode = 1 + return console.error(err) + } + if (Array.isArray(stats.stats)) { + stats.stats.forEach(item => { + console.log(item.compilation.name + '打包结果:') + process.stdout.write(item.toString({ + colors: true, + modules: false, + children: false, + chunks: false, + chunkModules: false, + entrypoints: false + }) + '\n\n') + }) + } else { + process.stdout.write(stats.toString({ + colors: true, + modules: false, + children: false, + chunks: false, + chunkModules: false, + entrypoints: false + }) + '\n\n') + } + + if (stats.hasErrors()) { + console.log(chalk.red(' Build failed with errors.\n')) + } else if (program.watch) { + console.log(chalk.cyan(` Build complete at ${new Date()}.\n Still watching...\n`)) + } else { + console.log(chalk.cyan(' Build complete.\n')) + } +} diff --git a/examples/mpx-transform-web/build/getPlugins.js b/examples/mpx-transform-web/build/getPlugins.js new file mode 100644 index 0000000000..8ce6bdf931 --- /dev/null +++ b/examples/mpx-transform-web/build/getPlugins.js @@ -0,0 +1,44 @@ +let { mpxPluginConf } = require('../config/index') +const MpxWebpackPlugin = require('@mpxjs/web-plugin/webpack') +const { resolveSrc, getConf } = require('./utils') +const HtmlWebpackPlugin = require('html-webpack-plugin') +const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin +const VueLoaderPlugin = require('vue-loader').VueLoaderPlugin +const webpack = require('webpack') +module.exports = function getPlugins (options) { + const { mode, srcMode, env, subDir, production, report } = options + const plugins = [] + const currentMpxPluginConf = getConf(mpxPluginConf, options) + plugins.push(new MpxWebpackPlugin(Object.assign({}, currentMpxPluginConf, { + mode, + srcMode, + env: 'didi', + // webConfig: { + // routeMode: 'history' + // }, + externalClasses: ['list-class'], + }, env && { env }))) + + plugins.push(new webpack.DefinePlugin({ + 'process.env': { + NODE_ENV: production ? '"production"' : '"development"' + } + })) + + if (mode === 'web') { + plugins.push(new VueLoaderPlugin()) + plugins.push(new HtmlWebpackPlugin({ + filename: 'index.html', + template: resolveSrc('index.html', subDir), + inject: true + })) + } + + plugins.push(new webpack.ProgressPlugin()) + + if (report) { + plugins.push(new BundleAnalyzerPlugin()) + } + + return plugins +} diff --git a/examples/mpx-transform-web/build/getRules.js b/examples/mpx-transform-web/build/getRules.js new file mode 100644 index 0000000000..27909baca5 --- /dev/null +++ b/examples/mpx-transform-web/build/getRules.js @@ -0,0 +1,122 @@ +let { mpxLoaderConf } = require('../config/index') +const MpxWebpackPlugin = require('@mpxjs/web-plugin/webpack') +// const MpxWebpackPlugin = require('@mpxjs/webpack-plugin') +const { resolve, getConf } = require('./utils') + +const baseRules = [ + { + test: /\.js$/, + loader: 'babel-loader', + include: [/\.mpx\.js/, resolve('src'), resolve('test'), resolve('node_modules/@mpxjs')] + }, + { + test: /\.json$/, + resourceQuery: /asScript/, + type: 'javascript/auto' + }, + { + test: /\.(wxs|qs|sjs|qjs|jds|dds|filter\.js)$/, + use: [ + MpxWebpackPlugin.wxsPreLoader() + ], + enforce: 'pre' + }, + { + test: /\.(png|jpe?g|gif|svg)$/, + use: [ + MpxWebpackPlugin.urlLoader({ + name: 'img/[name][hash].[ext]' + }) + ] + } +] + +const tsRule = { + test: /\.ts$/, + use: [ + 'babel-loader', + { + loader: 'ts-loader', + options: { + appendTsSuffixTo: [/\.(mpx|vue)$/] + } + } + ] +} + +module.exports = function getRules (options) { + const { mode, tsSupport } = options + + let rules = baseRules.slice() + + if (tsSupport) { + rules.push(tsRule) + } + + const currentMpxLoaderConf = getConf(mpxLoaderConf, options) + + if (mode === 'web') { + rules = rules.concat([ + { + test: /\.mpx$/, + use: [ + { + loader: 'vue-loader', + options: { + transformToRequire: { + 'mpx-image': 'src', + 'mpx-audio': 'src', + 'mpx-video': 'src' + } + } + }, + MpxWebpackPlugin.loader(currentMpxLoaderConf) + ] + }, + { + test: /\.vue$/, + loader: 'vue-loader' + }, + // 如输出web时需要支持其他预编译语言,可以在此添加rule配置 + { + test: /\.styl(us)?$/, + use: [ + 'vue-style-loader', + 'css-loader', + 'stylus-loader' + ] + }, + { + test: /\.css$/, + use: [ + 'vue-style-loader', + 'css-loader' + ] + } + ]) + } else { + rules = rules.concat([ + { + test: /\.mpx$/, + use: MpxWebpackPlugin.loader(currentMpxLoaderConf) + }, + { + test: /\.styl(us)?$/, + use: [ + MpxWebpackPlugin.wxssLoader(), + 'stylus-loader' + ] + }, + { + test: /\.(wxss|acss|css|qss|ttss|jxss|ddss)$/, + use: MpxWebpackPlugin.wxssLoader() + }, + { + test: /\.(wxml|axml|swan|qml|ttml|qxml|jxml|ddml)$/, + use: MpxWebpackPlugin.wxmlLoader() + } + ]) + } + + return rules +} diff --git a/examples/mpx-transform-web/build/getWebpackConf.js b/examples/mpx-transform-web/build/getWebpackConf.js new file mode 100644 index 0000000000..4f7b971ca1 --- /dev/null +++ b/examples/mpx-transform-web/build/getWebpackConf.js @@ -0,0 +1,53 @@ +const webpackBaseConf = require('./webpack.base.conf') +const { mergeWithCustomize, customizeObject } = require('webpack-merge') +const getRules = require('./getRules') +const getPlugins = require('./getPlugins') +const { resolveSrc, resolveDist, getRootPath } = require('./utils') +const MpxWebpackPlugin = require('@mpxjs/web-plugin/webpack') +// const MpxWebpackPlugin = require('@mpxjs/webpack-plugin') + +module.exports = function getWebpackConfs (options) { + const { plugin, subDir, mode, env, production, watch } = options + const entry = plugin + ? { plugin: MpxWebpackPlugin.getPluginEntry(resolveSrc('plugin.json', subDir)) } + : { app: resolveSrc('app.mpx', subDir) } + const rootPath = getRootPath(mode, env) + const output = { + path: resolveDist(rootPath, subDir), + publicPath: '/', + filename: '[name].js', + crossOriginLoading: 'anonymous' + } + const name = plugin ? `${rootPath}-plugin-compiler` : `${rootPath}-compiler` + const rules = getRules(options) + // console.log(rules); + const plugins = getPlugins(options) + const extendConfs = {} + if (production) { + extendConfs.mode = 'production' + } + extendConfs.optimization = { + nodeEnv: production ? 'production' : 'development' + } + if (watch) { + // 仅在watch模式下生产sourcemap + // 百度小程序不开启sourcemap,开启会有模板渲染问题 + if (mode !== 'swan') { + extendConfs.devtool = 'source-map' + } + } + + return mergeWithCustomize({ + customizeObject: customizeObject({ + snapshot: 'replace' + }) + })(webpackBaseConf, { + name, + entry, + output, + module: { + rules + }, + plugins + }, extendConfs) +} diff --git a/examples/mpx-transform-web/build/package.json b/examples/mpx-transform-web/build/package.json new file mode 100644 index 0000000000..b76afc0e66 --- /dev/null +++ b/examples/mpx-transform-web/build/package.json @@ -0,0 +1,5 @@ +{ + "name": "mpx-transform-web-build", + "version": "1.0.0", + "description": "避免webpack通过分析目录依赖错误地将项目package.json中声明的dependencies作为buildDependencies,请勿删除该文件!" +} diff --git a/examples/mpx-transform-web/build/utils.js b/examples/mpx-transform-web/build/utils.js new file mode 100644 index 0000000000..5334f26415 --- /dev/null +++ b/examples/mpx-transform-web/build/utils.js @@ -0,0 +1,39 @@ +const path = require('path') + +function resolveSrc (file, subDir = '') { + return path.join(__dirname, '../src', subDir, file || '') +} + +function resolveDist (platform, subDir = '') { + return path.join(__dirname, '../dist', platform, subDir) +} + +function resolve (file) { + return path.join(__dirname, '..', file || '') +} + +function normalizeArr (arrCfg) { + if (Array.isArray(arrCfg) && arrCfg.length) { + return arrCfg + } else if (arrCfg) { + return [arrCfg] + } + return [] +} + +function getRootPath (...args) { + return args.filter(item => item).join('_') +} + +function getConf (conf, options) { + return typeof conf === 'function' ? conf(options) : conf +} + +module.exports = { + resolve, + resolveSrc, + resolveDist, + normalizeArr, + getRootPath, + getConf +} diff --git a/examples/mpx-transform-web/build/webpack.base.conf.js b/examples/mpx-transform-web/build/webpack.base.conf.js new file mode 100644 index 0000000000..84ea2119c9 --- /dev/null +++ b/examples/mpx-transform-web/build/webpack.base.conf.js @@ -0,0 +1,43 @@ +const { resolve } = require('./utils') + +module.exports = { + performance: { + hints: false + }, + mode: 'none', + resolve: { + extensions: ['.mpx', '.js', '.wxml', '.vue', '.ts'], + modules: ['node_modules'] + }, + // cache: { + // type: 'filesystem', + // buildDependencies: { + // build: [resolve('build/')], + // config: [resolve('config/')] + // }, + // cacheDirectory: resolve('.cache/') + // }, + snapshot: { + // 如果希望修改node_modules下的文件时对应的缓存可以失效,可以将此处的配置改为 managedPaths: [] + managedPaths: [] + }, + optimization: { + minimizer: [ + { + apply: compiler => { + // Lazy load the Terser plugin + const TerserPlugin = require('terser-webpack-plugin') + new TerserPlugin({ + // terserOptions参考 https://github.com/webpack-contrib/terser-webpack-plugin#terseroptions + terserOptions: { + // terser的默认行为会把某些对象方法转为箭头函数,导致ios9等不支持箭头函数的环境白屏,详情见 https://github.com/terser/terser#compress-options + compress: { + arrows: false + } + } + }).apply(compiler) + } + } + ] + } +} diff --git a/examples/mpx-transform-web/config/dll.conf.js b/examples/mpx-transform-web/config/dll.conf.js new file mode 100644 index 0000000000..69a0b8285d --- /dev/null +++ b/examples/mpx-transform-web/config/dll.conf.js @@ -0,0 +1,16 @@ +const { resolve, resolveSrc } = require('../build/utils') +module.exports = { + path: resolve('dll'), + context: resolveSrc(), + groups: { + cacheGroups: [ + { + entries: [resolveSrc('lib/dll')], + name: 'dll', + } + ], + webpackCfg: { + mode: 'none' + } + } +} diff --git a/examples/mpx-transform-web/config/index.js b/examples/mpx-transform-web/config/index.js new file mode 100644 index 0000000000..9f84ab52c3 --- /dev/null +++ b/examples/mpx-transform-web/config/index.js @@ -0,0 +1,22 @@ +const mpxLoaderConf = require('./mpxLoader.conf') +const mpxPluginConf = require('./mpxPlugin.conf') +const userConf = require('./user.conf') + +const supportedModes = ['wx', 'ali', 'swan', 'qq', 'tt', 'qa', 'jd', 'dd'] + +if (userConf.transWeb) { + supportedModes.push('web') +} + +const options = { + userConf, + mpxLoaderConf, + mpxPluginConf, + supportedModes +} + +if (userConf.needDll) { + options.dllConf = require('./dll.conf') +} + +module.exports = options diff --git a/examples/mpx-transform-web/config/mpxLoader.conf.js b/examples/mpx-transform-web/config/mpxLoader.conf.js new file mode 100644 index 0000000000..36bbfe2cf2 --- /dev/null +++ b/examples/mpx-transform-web/config/mpxLoader.conf.js @@ -0,0 +1,3 @@ +// mpx的loader配置在这里传入 +// 配置项文档:https://www.mpxjs.cn/api/compile.html#mpxwebpackplugin-loader +module.exports = {} diff --git a/examples/mpx-transform-web/config/mpxPlugin.conf.js b/examples/mpx-transform-web/config/mpxPlugin.conf.js new file mode 100644 index 0000000000..71e185b0f9 --- /dev/null +++ b/examples/mpx-transform-web/config/mpxPlugin.conf.js @@ -0,0 +1,65 @@ +const resolve = require('../build/utils').resolve +const path = require('path') +// 可以在此配置mpx webpack plugin +// 配置项文档: https://www.mpxjs.cn/api/compile.html#mpxwebpackplugin-options +module.exports = { + mode: 'web', + srcMode: 'wx', + // env为mpx编译的目标环境,需自定义 + // env: 'didi', + // resolve的模式 + resolveMode: 'webpack', // 可选值 webpack / native,默认是webpack,原生迁移建议使用native + + // 当resolveMode为native时可通过该字段指定项目根目录 + // projectRoot: resolve('src'), + + // 可选值 full / changed,不传默认为change,当设置为changed时在watch模式下将只会对内容发生变化的文件进行写入,以提升小程序开发者工具编译性能 + writeMode: 'changed', + + // 是否需要对样式加scope,因为只有ali平台没有样式隔离,只对ali平台生效,提供include和exclude,和webpack的rules规则相同 + // autoScopeRules: { + // include: [resolve('src')] + // }, + + // 批量指定文件mode,用法如下,指定平台,提供include/exclude指定文件,即include的文件会默认被认为是该平台的,include/exclude的规则和webpack的rules的相同 + modeRules: { + // ali: { + // include: [resolve('node_modules/vant-aliapp')] + // } + }, + + // 定义一些全局环境变量,可在JS/模板/样式/JSON中使用 + defs: { + __application_name__: 'dd' + }, + + // 是否转换px到rpx + transRpxRules: [ + { + mode: 'only', + comment: 'use rpx', + include: resolve('src') + } + ], + + // 多语言i18n能力 以下是简单示例,更多详情请参考文档:https://didi.github.io/mpx/i18n.html + i18n: { + locale: 'en-US', + // messages既可以通过对象字面量传入,也可以通过messagesPath指定一个js模块路径,在该模块中定义配置并导出,dateTimeFormats/dateTimeFormatsPath和numberFormats/numberFormatsPath同理 + messagesPath: path.resolve(__dirname, '../src/i18n/index.js') + // messages: { + // 'en-US': { + // message: { + // title: 'test2344', + // hello: '{msg} world' + // } + // }, + // 'zh-CN': { + // message: { + // title: '中文', + // hello: '{msg} 世界' + // } + // } + // } + } +} diff --git a/examples/mpx-transform-web/config/package.json b/examples/mpx-transform-web/config/package.json new file mode 100644 index 0000000000..1c834f8878 --- /dev/null +++ b/examples/mpx-transform-web/config/package.json @@ -0,0 +1,5 @@ +{ + "name": "mpx-transform-web-config", + "version": "1.0.0", + "description": "避免webpack通过分析目录依赖错误地将项目package.json中声明的dependencies作为buildDependencies,请勿删除该文件!" +} diff --git a/examples/mpx-transform-web/config/user.conf.js b/examples/mpx-transform-web/config/user.conf.js new file mode 100644 index 0000000000..34ac93e3a4 --- /dev/null +++ b/examples/mpx-transform-web/config/user.conf.js @@ -0,0 +1,21 @@ +function formatOption (option) { + if (option === 'true') return true + if (option === 'false') return false + return option +} + +// 根据创建项目时的问题生成的 +// 改动需谨慎,有的选项存在互斥关系,比如跨平台,则无法使用云函数 +// 若需修改以启用新的能力,建议试试新建项目按问题生成模板后把这部分内容拷贝过来 +module.exports = { + srcMode: formatOption('wx'), + cross: formatOption('true'), + openChildProcess: formatOption('true'), + transWeb: formatOption('true'), + cloudFunc: formatOption('false'), + isPlugin: formatOption('false'), + tsSupport: formatOption('false'), + needEslint: formatOption('false'), + needDll: formatOption('true'), + needUnitTest: formatOption('false') +} diff --git a/examples/mpx-transform-web/index.html b/examples/mpx-transform-web/index.html new file mode 100644 index 0000000000..352793a15b --- /dev/null +++ b/examples/mpx-transform-web/index.html @@ -0,0 +1,15 @@ + + + + + + + Vite App + + +
+ + + diff --git a/examples/mpx-transform-web/package.json b/examples/mpx-transform-web/package.json new file mode 100644 index 0000000000..410e78ca0b --- /dev/null +++ b/examples/mpx-transform-web/package.json @@ -0,0 +1,96 @@ +{ + "name": "mpx-transform-web-demo", + "version": "1.0.0", + "description": "A mpx project", + "main": "index.js", + "scripts": { + "vite:dev": "vite", + "vite:build": "vite build", + "watch": "node build/build.js -w", + "watch:prod": "node build/build.js -w -p", + "build": "npm run prod --modes=web", + "prod": "node build/build.js -p", + "watch:web:serve": "npx npm-run-all --parallel watch:web serve", + "watch:web": "npm run watch --mode=web", + "serve": "npx http-server dist/web", + "lint": "eslint --ext .js,.ts,.mpx src/" + }, + "author": "yandadaFreedom <525966780@qq.com>", + "license": "ISC", + "dependencies": { + "@mpxjs/api-proxy": "workspace:*", + "@mpxjs/core": "workspace:*", + "@mpxjs/web-plugin": "workspace:*", + "@mpxjs/webpack-plugin": "workspace:*", + "@mpxjs/loaders": "workspace:*", + "@mpxjs/store": "workspace:*", + "mime": "^2.6.0", + "vue": "~2.7.10", + "vue-demi": "^0.13.11", + "vue-i18n": "^8.27.2", + "vue-i18n-bridge": "^9.2.2", + "@intlify/core-base": "9.2.2", + "@intlify/shared": "9.2.2", + "@intlify/vue-devtools": "9.2.2", + "@intlify/devtools-if": "9.2.2", + "@intlify/message-compiler": "9.2.2", + "core-js-pure": "^3.28.0" + }, + "browserslist": [ + "ios >= 9", + "chrome >= 47" + ], + "devDependencies": { + "@babel/core": "^7.10.4", + "@babel/eslint-parser": "^7.16.0", + "@babel/plugin-transform-runtime": "^7.10.4", + "@babel/plugin-transform-typescript": "^7.16.1", + "@babel/preset-env": "^7.10.4", + "@babel/runtime-corejs3": "^7.20.13", + "@better-scroll/core": "^2.2.1", + "@better-scroll/movable": "^2.2.1", + "@better-scroll/observe-dom": "^2.2.1", + "@better-scroll/pull-down": "^2.2.1", + "@better-scroll/slide": "^2.2.1", + "@better-scroll/wheel": "^2.2.1", + "@better-scroll/zoom": "^2.2.1", + "@originjs/vite-plugin-commonjs": "^1.0.3", + "@typescript-eslint/eslint-plugin": "^5.2.0", + "@typescript-eslint/parser": "^5.2.0", + "@vitejs/plugin-legacy": "4.0.0", + "autoprefixer": "^6.3.1", + "babel-jest": "^27.4.5", + "babel-loader": "^8.1.0", + "chalk": "^2.3.2", + "commander": "^6.0.0", + "copy-webpack-plugin": "^9.0.1", + "css-loader": "^0.28.11", + "eslint": "^7.32.0", + "html-loader": "^3.0.1", + "html-webpack-plugin": "^5.3.2", + "http-server": "^0.12.0", + "jest": "^27.4.5", + "npm-run-all": "^4.1.5", + "path": "^0.12.7", + "postcss": "^8.4.5", + "rimraf": "^2.6.2", + "stylus": "^0.54.5", + "stylus-loader": "^3.0.2", + "terser": "^5.0.0", + "terser-webpack-plugin": "^5.3.6", + "ts-jest": "^27.1.2", + "ts-loader": "^9.2.6", + "typescript": "^4.1.2", + "vconsole": "^3.2.0", + "vite": "^4.0.0", + "vue-loader": "^15.9.3", + "vue-router": "^3.5.4", + "vue-style-loader": "^4.1.2", + "vue-template-compiler": "~2.6.10", + "webpack": "^5.69.1", + "webpack-bundle-analyzer": "^4.5.0", + "webpack-merge": "^5.8.0", + "lodash": "^4.1.1", + "esbuild": "^0.16.17" + } +} diff --git a/examples/mpx-transform-web/postcss.config.js b/examples/mpx-transform-web/postcss.config.js new file mode 100644 index 0000000000..e66012e084 --- /dev/null +++ b/examples/mpx-transform-web/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: [ + require('autoprefixer')({ remove: false }) + + ] +} diff --git a/examples/mpx-transform-web/src/app.json b/examples/mpx-transform-web/src/app.json new file mode 100644 index 0000000000..9c886b1f98 --- /dev/null +++ b/examples/mpx-transform-web/src/app.json @@ -0,0 +1,33 @@ +{ + "pages": [ + "./pages/wxs?async=app_pages2", + "./pages/index", + "./pages/index2?async=app_pages1" + ], + "packages": ["./packages/index"], + "subpackages": [ + { + "root": "packageA", + "pages": [ + "./pages/picker", + "./pages/swiper" + ], + "independent": true + } + ], + "tabBar": { + "custom": false, + "color": "red", + "selectedColor": "black", + "backgroundColor": "white", + "list": [{ + "pagePath": "pages/index", + "text": "page10", + "iconPath": "./images/icon1.png" + }, { + "pagePath": "pages/wxs", + "text": "page2", + "iconPath": "https://ut-static.udache.com/webx/mini-pics/pZ0qkcV-yweUQ644F5pVp.png" + }] + } +} diff --git a/examples/mpx-transform-web/src/app.mpx b/examples/mpx-transform-web/src/app.mpx new file mode 100644 index 0000000000..74a8d304a4 --- /dev/null +++ b/examples/mpx-transform-web/src/app.mpx @@ -0,0 +1,21 @@ + + + + + diff --git a/examples/mpx-transform-web/src/common/index.js b/examples/mpx-transform-web/src/common/index.js new file mode 100644 index 0000000000..26e3333d23 --- /dev/null +++ b/examples/mpx-transform-web/src/common/index.js @@ -0,0 +1,26 @@ +import mpx, { createPage } from '@mpxjs/core' +import swiperPage from '../packageA/pages/swiper.mpx?resolve' +createPage({ + data: { + a: 1, + index: 0, + array: ['美国', '中国', '巴西', '日本'] + }, + onLoad () { + console.log('load index') + }, + onShow () { + console.log('show index') + // console.log(111, swiperPage) + }, + methods: { + reload () { + window.location.reload() + }, + jumpPage () { + mpx.navigateTo({ + url: swiperPage + }) + } + } +}) diff --git a/examples/mpx-transform-web/src/common/js/index.js b/examples/mpx-transform-web/src/common/js/index.js new file mode 100644 index 0000000000..0b787b82dd --- /dev/null +++ b/examples/mpx-transform-web/src/common/js/index.js @@ -0,0 +1,28 @@ +import mpx, { createPage } from '@mpxjs/core' +import swiperPage from '../../packageA/pages/swiper.mpx?resolve' +// import img from '../../images/icon1.png' +createPage({ + data: { + // img, + a: 1, + index: 0, + array: ['美国', '中国', '巴西', '日本'] + }, + onLoad () { + console.log('load index') + }, + onShow () { + console.log('show index123') + console.log('test', swiperPage, mpx.i18n.t('message.title')) + }, + methods: { + reload () { + window.location.reload() + }, + jumpPage () { + mpx.navigateTo({ + // url: swiperPage + }) + } + } +}) diff --git a/examples/mpx-transform-web/src/common/js/list.ts b/examples/mpx-transform-web/src/common/js/list.ts new file mode 100644 index 0000000000..14f24edbe0 --- /dev/null +++ b/examples/mpx-transform-web/src/common/js/list.ts @@ -0,0 +1,11 @@ +import { createComponent } from '@mpxjs/core' + +createComponent({ + externalClasses: ['list-class'], + data: { + listData: ['手机', '电视', '电脑'] + }, + onShow () { + console.log('list') + } +}) diff --git a/examples/mpx-transform-web/src/common/js/list2.ts b/examples/mpx-transform-web/src/common/js/list2.ts new file mode 100644 index 0000000000..847b6ced66 --- /dev/null +++ b/examples/mpx-transform-web/src/common/js/list2.ts @@ -0,0 +1,11 @@ +import { createComponent } from '@mpxjs/core' + +createComponent({ + externalClasses: ['list-class'], + data: { + listData: ['手机', '电视'] + }, + onShow () { + console.log('list') + } +}) diff --git a/examples/mpx-transform-web/src/components/custom-tab-bar/index.mpx b/examples/mpx-transform-web/src/components/custom-tab-bar/index.mpx new file mode 100644 index 0000000000..99993ede3e --- /dev/null +++ b/examples/mpx-transform-web/src/components/custom-tab-bar/index.mpx @@ -0,0 +1,227 @@ + + + + + + diff --git a/examples/mpx-transform-web/src/components/hello.wxs b/examples/mpx-transform-web/src/components/hello.wxs new file mode 100644 index 0000000000..147c69d865 --- /dev/null +++ b/examples/mpx-transform-web/src/components/hello.wxs @@ -0,0 +1,10 @@ +var foo = "'hello world' from hello.wxs" +var bar = function (d) { + return d +} +console.log(123) +module.exports = { + FOO: foo, + bar: bar +} +module.exports.msg = 'some msg' diff --git a/examples/mpx-transform-web/src/components/hello2.wxs b/examples/mpx-transform-web/src/components/hello2.wxs new file mode 100644 index 0000000000..eee66da1a9 --- /dev/null +++ b/examples/mpx-transform-web/src/components/hello2.wxs @@ -0,0 +1,10 @@ +var foo = "'hello world' from hello.wxs2" +var bar = function (d) { + return d +} +console.log(123) +module.exports = { + FOO: foo, + bar: bar +} +module.exports.msg = 'some msg' diff --git a/examples/mpx-transform-web/src/components/list.mpx b/examples/mpx-transform-web/src/components/list.mpx new file mode 100644 index 0000000000..9aa0eabedd --- /dev/null +++ b/examples/mpx-transform-web/src/components/list.mpx @@ -0,0 +1,18 @@ + + + + + + + diff --git a/examples/mpx-transform-web/src/components/list.web.didi.mpx b/examples/mpx-transform-web/src/components/list.web.didi.mpx new file mode 100644 index 0000000000..9980a6779b --- /dev/null +++ b/examples/mpx-transform-web/src/components/list.web.didi.mpx @@ -0,0 +1,19 @@ + + + + + + + diff --git a/examples/mpx-transform-web/src/components/list1.mpx b/examples/mpx-transform-web/src/components/list1.mpx new file mode 100644 index 0000000000..3b770a918b --- /dev/null +++ b/examples/mpx-transform-web/src/components/list1.mpx @@ -0,0 +1,18 @@ + + + + + + + diff --git a/examples/mpx-transform-web/src/components/list2.mpx b/examples/mpx-transform-web/src/components/list2.mpx new file mode 100644 index 0000000000..29de6dc909 --- /dev/null +++ b/examples/mpx-transform-web/src/components/list2.mpx @@ -0,0 +1,19 @@ + + + + + + + diff --git a/examples/mpx-transform-web/src/custom-tab-bar/index.mpx b/examples/mpx-transform-web/src/custom-tab-bar/index.mpx new file mode 100644 index 0000000000..6e4e27cca1 --- /dev/null +++ b/examples/mpx-transform-web/src/custom-tab-bar/index.mpx @@ -0,0 +1,217 @@ + + + + + + diff --git a/examples/mpx-transform-web/src/i18n/cn.js b/examples/mpx-transform-web/src/i18n/cn.js new file mode 100644 index 0000000000..e8dddd3f34 --- /dev/null +++ b/examples/mpx-transform-web/src/i18n/cn.js @@ -0,0 +1,5 @@ +/* eslint-disable */ +const cn = { + 'page_title': '测试' +} +module.exports = cn diff --git a/examples/mpx-transform-web/src/i18n/en.js b/examples/mpx-transform-web/src/i18n/en.js new file mode 100644 index 0000000000..2291f2903d --- /dev/null +++ b/examples/mpx-transform-web/src/i18n/en.js @@ -0,0 +1,5 @@ +/* eslint-disable */ +const en = { + 'page_title': 'i18n test===' +} +module.exports = en diff --git a/examples/mpx-transform-web/src/i18n/index.js b/examples/mpx-transform-web/src/i18n/index.js new file mode 100644 index 0000000000..9e0a050347 --- /dev/null +++ b/examples/mpx-transform-web/src/i18n/index.js @@ -0,0 +1,17 @@ +// log MPX i18n 因为是在node环境下引入文件 所以需要使用commonjs规范 +// import cn from './cn' +// import en from './en' +const en = require('./en') +const cn = require('./cn') +/** + * 语言类型由端传来,key为lang,可能的值有:zh-CN, en-US, pt-BR, en-BR, zh-HK, zh-TW + */ + +// export default { +// 'zh-CN': cn, +// 'en-US': en +// } +module.exports = { + 'zh-CN': cn, + 'en-US': en +} diff --git a/examples/mpx-transform-web/src/images/icon1.png b/examples/mpx-transform-web/src/images/icon1.png new file mode 100644 index 0000000000..bb2201d300 Binary files /dev/null and b/examples/mpx-transform-web/src/images/icon1.png differ diff --git a/examples/mpx-transform-web/src/images/icon2.png b/examples/mpx-transform-web/src/images/icon2.png new file mode 100644 index 0000000000..93ddfb2149 Binary files /dev/null and b/examples/mpx-transform-web/src/images/icon2.png differ diff --git a/examples/mpx-transform-web/src/index.html b/examples/mpx-transform-web/src/index.html new file mode 100644 index 0000000000..8fee0722b2 --- /dev/null +++ b/examples/mpx-transform-web/src/index.html @@ -0,0 +1,18 @@ + + + + + + mpx-test + + + + + + + +
+ + + diff --git a/examples/mpx-transform-web/src/packageA/pages/picker.mpx b/examples/mpx-transform-web/src/packageA/pages/picker.mpx new file mode 100644 index 0000000000..87f4410361 --- /dev/null +++ b/examples/mpx-transform-web/src/packageA/pages/picker.mpx @@ -0,0 +1,99 @@ + + + + + + + diff --git a/examples/mpx-transform-web/src/packageA/pages/swiper.mpx b/examples/mpx-transform-web/src/packageA/pages/swiper.mpx new file mode 100644 index 0000000000..b6bb2f1d4d --- /dev/null +++ b/examples/mpx-transform-web/src/packageA/pages/swiper.mpx @@ -0,0 +1,108 @@ + + + + + + + diff --git a/examples/mpx-transform-web/src/packages/index.mpx b/examples/mpx-transform-web/src/packages/index.mpx new file mode 100644 index 0000000000..946640da06 --- /dev/null +++ b/examples/mpx-transform-web/src/packages/index.mpx @@ -0,0 +1,7 @@ + diff --git a/examples/mpx-transform-web/src/packages/pages/other.mpx b/examples/mpx-transform-web/src/packages/pages/other.mpx new file mode 100644 index 0000000000..0bcaab04cf --- /dev/null +++ b/examples/mpx-transform-web/src/packages/pages/other.mpx @@ -0,0 +1,3 @@ + diff --git a/examples/mpx-transform-web/src/pages/config/index.json b/examples/mpx-transform-web/src/pages/config/index.json new file mode 100644 index 0000000000..ba8ec758c7 --- /dev/null +++ b/examples/mpx-transform-web/src/pages/config/index.json @@ -0,0 +1,5 @@ +{ + "usingComponents": { + "list": "../../components/list" + } +} \ No newline at end of file diff --git a/examples/mpx-transform-web/src/pages/index.mpx b/examples/mpx-transform-web/src/pages/index.mpx new file mode 100644 index 0000000000..8e9cd00a14 --- /dev/null +++ b/examples/mpx-transform-web/src/pages/index.mpx @@ -0,0 +1,67 @@ + + + + + diff --git a/examples/mpx-transform-web/src/pages/index2.mpx b/examples/mpx-transform-web/src/pages/index2.mpx new file mode 100644 index 0000000000..706673b945 --- /dev/null +++ b/examples/mpx-transform-web/src/pages/index2.mpx @@ -0,0 +1,37 @@ + + + + + diff --git a/examples/mpx-transform-web/src/pages/wxs.mpx b/examples/mpx-transform-web/src/pages/wxs.mpx new file mode 100644 index 0000000000..748745ec07 --- /dev/null +++ b/examples/mpx-transform-web/src/pages/wxs.mpx @@ -0,0 +1,51 @@ + + + + + + + + + diff --git a/examples/mpx-transform-web/tsconfig.json b/examples/mpx-transform-web/tsconfig.json new file mode 100644 index 0000000000..2104f8944b --- /dev/null +++ b/examples/mpx-transform-web/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "esnext", + "noImplicitThis": true, + "noImplicitAny": true, + "strictNullChecks": true, + "esModuleInterop": true, + "moduleResolution": "node", + "lib": [ + "esnext", + "dom", + "dom.iterable" + ] + }, + "include": ["src", "vite.config.ts"], + "exclude": [ + "build", + "dist", + "config" + ] +} diff --git a/examples/mpx-transform-web/vite.config.ts b/examples/mpx-transform-web/vite.config.ts new file mode 100644 index 0000000000..dda23944fb --- /dev/null +++ b/examples/mpx-transform-web/vite.config.ts @@ -0,0 +1,77 @@ +import mpx from '@mpxjs/web-plugin/vite' +import path from 'path' +import { defineConfig, splitVendorChunkPlugin } from 'vite' +// import legacy from '@vitejs/plugin-legacy' + +export default defineConfig({ + optimizeDeps:{ + include: ['@mpxjs/api-proxy', '@mpxjs/core'] + }, + plugins: [ + mpx({ + env: 'didi', + mode: 'web', + externalClasses: ['list-class'], + // 定义一些全局环境变量,可在JS/模板/样式/JSON中使用 + defs: { + // eslint-disable-next-line camelcase + __application_name__: 'dd' + }, + + // 是否转换px到rpx + transRpxRules: [ + { + mode: 'only', + comment: 'use rpx', + include: path.join(__dirname, '..', 'src') + } + ], + i18n: { + locale: 'en-US', + // messages既可以通过对象字面量传入,也可以通过messagesPath指定一个js模块路径,在该模块中定义配置并导出,dateTimeFormats/dateTimeFormatsPath和numberFormats/numberFormatsPath同理 + // messagesPath: path.resolve('./src/i18n/index.js'), + messages: { + 'en-US': { + message: { + title: 'test', + hello: '{msg} world' + } + }, + 'zh-CN': { + message: { + title: '中文', + hello: '{msg} 世界' + } + } + } + } + }), + // test with split chunk + splitVendorChunkPlugin() + // test with legency + // legacy() + ], + resolve: { + alias: { + '@': path.resolve('.') + }, + extensions: ['.mpx', '.mjs', '.js', '.ts', '.jsx', '.tsx', '.json'] + }, + build: { + target: ['es2015'], + sourcemap: !(process.env.NODE_ENV === 'production'), + minify: false, + rollupOptions: { + output: { + manualChunks (id) { + if (id.includes('vueComponentNormalizer')) { + return 'vendor' + } + } + } + } + }, + css: { + devSourcemap: !(process.env.NODE_ENV === 'production') + } +}) diff --git a/examples/mpx-use-iconfont/build/package.json b/examples/mpx-use-iconfont/build/package.json index a84ff3ca30..6a50995ce9 100644 --- a/examples/mpx-use-iconfont/build/package.json +++ b/examples/mpx-use-iconfont/build/package.json @@ -1,5 +1,5 @@ { - "name": "build", + "name": "mpx-use-iconfont-build", "version": "1.0.0", "description": "避免webpack通过分析目录依赖错误地将项目package.json中声明的dependencies作为buildDependencies,请勿删除该文件!" } diff --git a/examples/mpx-use-iconfont/config/package.json b/examples/mpx-use-iconfont/config/package.json index a59b4221e8..a879e1e2f5 100644 --- a/examples/mpx-use-iconfont/config/package.json +++ b/examples/mpx-use-iconfont/config/package.json @@ -1,5 +1,5 @@ { - "name": "config", + "name": "mpx-use-iconfont-config", "version": "1.0.0", "description": "避免webpack通过分析目录依赖错误地将项目package.json中声明的dependencies作为buildDependencies,请勿删除该文件!" } diff --git a/examples/mpx-use-iconfont/package.json b/examples/mpx-use-iconfont/package.json index c26a7fce0f..9c91835048 100644 --- a/examples/mpx-use-iconfont/package.json +++ b/examples/mpx-use-iconfont/package.json @@ -1,5 +1,5 @@ { - "name": "mpx-template-demo-iconfont", + "name": "mpx-use-iconfont-demo", "version": "1.0.0", "description": "A mpx project", "main": "index.js", @@ -19,10 +19,10 @@ "author": "xuegan ", "license": "ISC", "dependencies": { - "@mpxjs/api-proxy": "^2.7.28", - "vue": "^2.6.10", + "@mpxjs/api-proxy": "workspace:*", + "vue": "~2.6.10", "vue-i18n": "^8.15.3", - "@mpxjs/core": "^2.7.37" + "@mpxjs/core": "workspace:*" }, "browserslist": [ "ios >= 9", @@ -30,13 +30,13 @@ ], "devDependencies": { "copy-webpack-plugin": "^9.0.1", - "@mpxjs/webpack-plugin": "^2.7.38", + "@mpxjs/webpack-plugin": "workspace:*", "http-server": "^0.12.0", "npm-run-all": "^4.1.5", "html-webpack-plugin": "^5.3.2", "vue-loader": "^15.9.3", "vue-router": "^3.1.3", - "vue-template-compiler": "^2.6.10", + "vue-template-compiler": "~2.6.10", "vue-style-loader": "^4.1.2", "jest": "^27.4.5", "@mpxjs/miniprogram-simulate": "^1.4.12", @@ -58,7 +58,7 @@ "@babel/core": "^7.10.4", "@babel/plugin-transform-runtime": "^7.10.4", "@babel/preset-env": "^7.10.4", - "@babel/runtime-corejs3": "^7.10.4", + "@babel/runtime-corejs3": "^7.20.13", "@babel/eslint-parser": "^7.16.0", "babel-loader": "^8.1.0", "chalk": "^2.3.2", diff --git a/examples/mpx-useuilib/babel.config.json b/examples/mpx-useuilib/babel.config.json index 6be2f424a9..3aad4a5b59 100644 --- a/examples/mpx-useuilib/babel.config.json +++ b/examples/mpx-useuilib/babel.config.json @@ -13,7 +13,7 @@ "@babel/transform-runtime", { "corejs": 3, - "version": "^7.12.5" + "version": "^7.20.13" } ], [ diff --git a/examples/mpx-useuilib/build/package.json b/examples/mpx-useuilib/build/package.json index a84ff3ca30..de0c91edb9 100644 --- a/examples/mpx-useuilib/build/package.json +++ b/examples/mpx-useuilib/build/package.json @@ -1,5 +1,5 @@ { - "name": "build", + "name": "mpx-useuilib-build", "version": "1.0.0", "description": "避免webpack通过分析目录依赖错误地将项目package.json中声明的dependencies作为buildDependencies,请勿删除该文件!" } diff --git a/examples/mpx-useuilib/config/package.json b/examples/mpx-useuilib/config/package.json index a59b4221e8..3753582642 100644 --- a/examples/mpx-useuilib/config/package.json +++ b/examples/mpx-useuilib/config/package.json @@ -1,5 +1,5 @@ { - "name": "config", + "name": "mpx-useuilib-config", "version": "1.0.0", "description": "避免webpack通过分析目录依赖错误地将项目package.json中声明的dependencies作为buildDependencies,请勿删除该文件!" } diff --git a/examples/mpx-useuilib/package.json b/examples/mpx-useuilib/package.json index 58e35a36cd..ee192ff4fa 100644 --- a/examples/mpx-useuilib/package.json +++ b/examples/mpx-useuilib/package.json @@ -1,5 +1,5 @@ { - "name": "mpx-subpackage-demo", + "name": "mpx-useuilib-demo", "version": "1.0.0", "description": "A mpx project", "main": "index.js", @@ -19,21 +19,21 @@ "author": "sky-admin <546485299@qq.com>", "license": "ISC", "dependencies": { - "@mpxjs/api-proxy": "^2.7.28", - "@mpxjs/core": "^2.7.33", + "@mpxjs/api-proxy": "workspace:*", + "@mpxjs/core": "workspace:*", "html-webpack-plugin": "^5.5.0", "iview-weapp": "^2.0.0", "vant": "^2.12.47", "vant-weapp": "^0.5.14", - "vue": "^2.6.10" + "vue": "~2.6.10" }, "devDependencies": { "@babel/core": "7.12.10", "@babel/eslint-parser": "7.16.0", "@babel/plugin-transform-runtime": "7.12.10", "@babel/preset-env": "7.12.11", - "@babel/runtime-corejs3": "7.12.5", - "@mpxjs/webpack-plugin": "^2.7.33", + "@babel/runtime-corejs3": "7.20.13", + "@mpxjs/webpack-plugin": "workspace:*", "@typescript-eslint/eslint-plugin": "^5.2.0", "@typescript-eslint/parser": "^5.2.0", "babel-jest": "^25.3.0", diff --git a/examples/mpx-webview/babel.config.json b/examples/mpx-webview/babel.config.json index 6be2f424a9..3aad4a5b59 100644 --- a/examples/mpx-webview/babel.config.json +++ b/examples/mpx-webview/babel.config.json @@ -13,7 +13,7 @@ "@babel/transform-runtime", { "corejs": 3, - "version": "^7.12.5" + "version": "^7.20.13" } ], [ diff --git a/examples/mpx-webview/build/package.json b/examples/mpx-webview/build/package.json index a84ff3ca30..c0c29e3924 100644 --- a/examples/mpx-webview/build/package.json +++ b/examples/mpx-webview/build/package.json @@ -1,5 +1,5 @@ { - "name": "build", + "name": "mpx-webview-build", "version": "1.0.0", "description": "避免webpack通过分析目录依赖错误地将项目package.json中声明的dependencies作为buildDependencies,请勿删除该文件!" } diff --git a/examples/mpx-webview/config/package.json b/examples/mpx-webview/config/package.json index a59b4221e8..0829abfee6 100644 --- a/examples/mpx-webview/config/package.json +++ b/examples/mpx-webview/config/package.json @@ -1,5 +1,5 @@ { - "name": "config", + "name": "mpx-webview-config", "version": "1.0.0", "description": "避免webpack通过分析目录依赖错误地将项目package.json中声明的dependencies作为buildDependencies,请勿删除该文件!" } diff --git a/examples/mpx-webview/package.json b/examples/mpx-webview/package.json index 04b31e8d73..8e33537cac 100644 --- a/examples/mpx-webview/package.json +++ b/examples/mpx-webview/package.json @@ -1,5 +1,5 @@ { - "name": "mpx-subpackage-demo", + "name": "mpx-webview-demo", "version": "1.0.0", "description": "A mpx project", "main": "index.js", @@ -18,16 +18,16 @@ "author": "sky-admin <546485299@qq.com>", "license": "ISC", "dependencies": { - "@mpxjs/api-proxy": "^2.7.28", - "@mpxjs/core": "^2.7.26" + "@mpxjs/api-proxy": "workspace:*", + "@mpxjs/core": "workspace:*" }, "devDependencies": { "@babel/core": "7.12.10", "@babel/eslint-parser": "7.16.0", "@babel/plugin-transform-runtime": "7.12.10", "@babel/preset-env": "7.12.11", - "@babel/runtime-corejs3": "7.12.5", - "@mpxjs/webpack-plugin": "^2.7.27", + "@babel/runtime-corejs3": "7.20.13", + "@mpxjs/webpack-plugin": "workspace:*", "@typescript-eslint/eslint-plugin": "^5.2.0", "@typescript-eslint/parser": "^5.2.0", "babel-jest": "^25.3.0", diff --git a/lerna.json b/lerna.json index 91ae09e47f..b1079583c2 100644 --- a/lerna.json +++ b/lerna.json @@ -2,5 +2,7 @@ "packages": [ "packages/*" ], + "useWorkspaces": "true", + "npmClient": "pnpm", "version": "2.8.24" } diff --git a/package.json b/package.json index 56a3d469fd..fe091bd265 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,7 @@ "fix": "eslint --fix --ext .js packages/", "test": "jest", "release": "npm run lint && npm run test && npx lerna version", - "docs:dev": "vuepress dev docs-vuepress", - "docs:build": "vuepress build docs-vuepress" + "dev": "pnpm -r --parallel --filter=./packages/* run dev" }, "devDependencies": { "@babel/core": "^7.8.7", @@ -17,20 +16,43 @@ "@docsearch/css": "^3.0.0", "@docsearch/js": "^3.0.0", "@testing-library/jest-dom": "^4.2.4", + "@types/async": "^2.4.2", + "@types/babel__code-frame": "^7.0.3", + "@types/babel__generator": "^7.6.4", + "@types/babel__traverse": "^7.18.3", + "@types/babel-traverse": "^6.25.4", + "@types/babel-types": "^7.0.4", + "@types/debug": "^4.1.7", + "@types/hash-sum": "^1.0.0", + "@types/loader-utils": "^2.0.3", + "@types/lodash": "^4.14.191", + "@types/lodash-es": "^4.17.6", + "@types/lru-cache": "^7.10.10", + "@types/node": "^16.11.6", + "@types/mime": "^3.0.1", + "@types/qs": "^6.9.7", "@types/jest": "^27.0.1", - "@vuepress/plugin-back-to-top": "^1.8.2", - "@vuepress/plugin-pwa": "^1.8.0", + "@typescript-eslint/eslint-plugin": "^5.17.0", + "@typescript-eslint/parser": "^5.17.0", + "babel-eslint": "^10.0.1", "eslint": "^7.32.0", + "eslint-config-babel": "^8.0.2", "eslint-config-standard": "^16.0.3", + "eslint-friendly-formatter": "^4.0.1", "eslint-plugin-html": "^6.2.0", "eslint-plugin-import": "^2.25.2", "eslint-plugin-jest": "^27.0.1", + "eslint-plugin-local-rules": "^0.1.0", "eslint-plugin-node": "^11.1.0", + "eslint-plugin-prettier": "^2.6.2", "eslint-plugin-promise": "^5.1.1", + "eslint-plugin-standard": "^4.0.0", "identity-obj-proxy": "^3.0.0", "jest": "^27.2.0", - "lerna": "^3.4.3", - "typescript": "^4.1.3", - "vuepress": "^1.9.7" + "lerna": "^6.5.0", + "rimraf": "^3.0.2", + "typescript": "^4.7.4", + "vue": "^2.7.10", + "webpack": "^5.48.0" } } diff --git a/packages/api-proxy/package.json b/packages/api-proxy/package.json index 10564c74b8..050d8f59b1 100644 --- a/packages/api-proxy/package.json +++ b/packages/api-proxy/package.json @@ -38,5 +38,8 @@ "homepage": "https://github.com/didi/mpx#readme", "dependencies": { "axios": "^0.21.1" + }, + "peerDependencies": { + "@mpxjs/core": "workspace:*" } } diff --git a/packages/api-proxy/src/common/js/utils.js b/packages/api-proxy/src/common/js/utils.js index 455646cf80..a0d7888b61 100644 --- a/packages/api-proxy/src/common/js/utils.js +++ b/packages/api-proxy/src/common/js/utils.js @@ -88,8 +88,7 @@ function error (msg) { console.error && console.error(`[@mpxjs/api-proxy error]:\n ${msg}`) } -function noop () { -} +function noop () {} function makeMap (arr) { return arr.reduce((obj, item) => { diff --git a/packages/compile-utils/README.md b/packages/compile-utils/README.md new file mode 100644 index 0000000000..4029717c9e --- /dev/null +++ b/packages/compile-utils/README.md @@ -0,0 +1,10 @@ +# `mpx-utils` + +> A toolkit for mpx framework + +## Usage + + +```js +import { xxx } from '@mpxjs/utils' +``` \ No newline at end of file diff --git a/packages/compile-utils/package.json b/packages/compile-utils/package.json new file mode 100644 index 0000000000..6bdc700b46 --- /dev/null +++ b/packages/compile-utils/package.json @@ -0,0 +1,32 @@ +{ + "name": "@mpxjs/compile-utils", + "version": "2.7.40", + "description": "mpx compile utils", + "main": "dist/index.js", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "keywords": [ + "mpx" + ], + "author": "lareinayanyu", + "license": "ISC", + "scripts": { + "dev": "tsc -w", + "build": "rimraf ./dist && tsc" + }, + "dependencies": { + "json5": "^2.1.3", + "loader-utils": "^2.0.2" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org" + }, + "repository": { + "type": "git", + "url": "git@github.com:didi/mpx.git" + }, + "homepage": "https://didi.github.io/mpx/", + "bugs": { + "url": "https://github.com/didi/mpx/issues" + } +} diff --git a/packages/compile-utils/src/add-infix.ts b/packages/compile-utils/src/add-infix.ts new file mode 100644 index 0000000000..19bc59c854 --- /dev/null +++ b/packages/compile-utils/src/add-infix.ts @@ -0,0 +1,15 @@ +import path from 'path' + +export function addInfix ( + resourcePath: string, + infix: string, + extname: string | any[] +) { + extname = extname || path.extname(resourcePath) + return ( + resourcePath.substring(0, resourcePath.length - extname.length) + + '.' + + infix + + extname + ) +} diff --git a/packages/compile-utils/src/add-query.ts b/packages/compile-utils/src/add-query.ts new file mode 100644 index 0000000000..700e008dc4 --- /dev/null +++ b/packages/compile-utils/src/add-query.ts @@ -0,0 +1,43 @@ +import { parseRequest } from './parse-request' +import { stringifyQuery } from './stringify-query' +import { type as t } from './type' +import { hasOwn } from './has-own' + +// 默认为非强行覆盖原query,如需强行覆盖传递force为true +export function addQuery ( + request: any, + data: any = {}, + force?: any, + removeKeys?: any[] +) { + const { + rawResourcePath: resourcePath, + loaderString, + queryObj: queryObjRaw + } = parseRequest(request) + const queryObj = Object.assign({}, queryObjRaw) + if (force) { + Object.assign(queryObj, data) + } else { + Object.keys(data).forEach((key) => { + if (!hasOwn(queryObj, key)) { + queryObj[key] = data[key] + } + }) + } + + if (removeKeys) { + if (t(removeKeys) === 'String') { + removeKeys = [removeKeys] + } + removeKeys.forEach(key => { + delete queryObj[key] + }) + } + + return ( + (loaderString ? `${loaderString}!` : '') + + resourcePath + + stringifyQuery(queryObj) + ) +} diff --git a/packages/compile-utils/src/ensure-array.ts b/packages/compile-utils/src/ensure-array.ts new file mode 100644 index 0000000000..82b482525d --- /dev/null +++ b/packages/compile-utils/src/ensure-array.ts @@ -0,0 +1,16 @@ +/** + * forked from https://github.com/rollup/plugins/blob/master/packages/pluginutils/src/utils/ensureArray.ts + * Helper since Typescript can't detect readonly arrays with Array.isArray + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function isArray(arg: unknown): arg is any[] | readonly any[] { + return Array.isArray(arg) +} + +export function ensureArray( + thing: readonly T[] | T | undefined | null +): readonly T[] { + if (isArray(thing)) return thing + if (thing == null) return [] + return [thing] +} diff --git a/packages/compile-utils/src/env.js b/packages/compile-utils/src/env.js new file mode 100644 index 0000000000..fe424ef88b --- /dev/null +++ b/packages/compile-utils/src/env.js @@ -0,0 +1,24 @@ +export function getEnvObj () { + switch (__mpx_mode__) { + case 'wx': + return wx + case 'ali': + return my + case 'swan': + return swan + case 'qq': + return qq + case 'tt': + return tt + case 'jd': + return jd + case 'qa': + return qa + case 'dd': + return dd + } +} + +export const isBrowser = typeof window !== 'undefined' + +export const isDev = process.env.NODE_ENV !== 'production' diff --git a/packages/compile-utils/src/env.ts b/packages/compile-utils/src/env.ts new file mode 100644 index 0000000000..5b1beac49b --- /dev/null +++ b/packages/compile-utils/src/env.ts @@ -0,0 +1,4 @@ +/** + * 判断当前环境是否是浏览器环境 + */ +export const inBrowser = typeof window !== 'undefined' diff --git a/packages/compile-utils/src/gen-code.ts b/packages/compile-utils/src/gen-code.ts new file mode 100644 index 0000000000..6c48b4b523 --- /dev/null +++ b/packages/compile-utils/src/gen-code.ts @@ -0,0 +1,13 @@ +import { stringify } from './stringify' + +export const genImport = (importer: string, name?: string): string => { + return `import ${name ? `${name} from` : ''} ${stringify(importer)}` +} + +export const genAsyncImport = ( + importer: string, + name?: string, + callback?: string +): string => { + return `import(${importer})${name ? `.then((${name}) => ${callback})` : ''}` +} diff --git a/packages/compile-utils/src/gen-component-tag.ts b/packages/compile-utils/src/gen-component-tag.ts new file mode 100644 index 0000000000..0f43d1ba66 --- /dev/null +++ b/packages/compile-utils/src/gen-component-tag.ts @@ -0,0 +1,64 @@ +import { type as t } from './type' + +function stringifyAttr (val: string) { + if (typeof val === 'string') { + const hasSingle = val.indexOf("'") > -1 + const hasDouble = val.indexOf('"') > -1 + // 移除属性中换行 + val = val.replace(/\n/g, '') + + if (hasSingle && hasDouble) { + val = val.replace(/'/g, '"') + } + if (hasDouble) { + return `'${val}'` + } else { + return `"${val}"` + } + } +} + +function stringifyAttrs (attrs: { [x: string]: any }) { + let result = '' + Object.keys(attrs).forEach(function (name) { + result += ' ' + name + const value = attrs[name] + if (value != null && value !== true) { + result += '=' + stringifyAttr(value) + } + }) + return result +} + +export function genComponentTag ( + part: { content: string; tag: string; attrs: any }, + processor: any = {} +) { + // normalize + if (t(processor) === 'Function') { + processor = { + content: processor + } + } + if (part.content) { + // unpad + // part.content = '\n' + part.content.replace(/^\n*/m, '') + } + + const tag = processor.tag ? processor.tag(part) : part.tag + const attrs = processor.attrs ? processor.attrs(part) : part.attrs + const content = processor.content ? processor.content(part) : part.content + let result = '' + if (tag) { + result += `<${tag}` + if (attrs) { + result += stringifyAttrs(attrs) + } + if (content) { + result += `>${content}` + } else { + result += '/>' + } + } + return result +} diff --git a/packages/compile-utils/src/get-entry-name.ts b/packages/compile-utils/src/get-entry-name.ts new file mode 100644 index 0000000000..c4efa304a0 --- /dev/null +++ b/packages/compile-utils/src/get-entry-name.ts @@ -0,0 +1,15 @@ +import { LoaderContext, NormalModule } from 'webpack' + +export function getEntryName (loaderContext: LoaderContext): string { + if (!loaderContext._compilation) return '' + const moduleGraph = loaderContext._compilation.moduleGraph + let entryName = '' + for (const [name, { dependencies }] of loaderContext._compilation.entries) { + const entryModule = moduleGraph.getModule(dependencies[0]) as NormalModule + if (entryModule && entryModule.resource === loaderContext.resource) { + entryName = name + break + } + } + return entryName +} diff --git a/packages/compile-utils/src/has-own.ts b/packages/compile-utils/src/has-own.ts new file mode 100644 index 0000000000..8c9c37f891 --- /dev/null +++ b/packages/compile-utils/src/has-own.ts @@ -0,0 +1,5 @@ +const hasOwnProperty = Object.prototype.hasOwnProperty + +export function hasOwn (obj: Record, key: string) { + return hasOwnProperty.call(obj, key) +} diff --git a/packages/compile-utils/src/helpers.ts b/packages/compile-utils/src/helpers.ts new file mode 100644 index 0000000000..c10259da94 --- /dev/null +++ b/packages/compile-utils/src/helpers.ts @@ -0,0 +1,130 @@ +import loaderUtils from 'loader-utils' +import { addQuery, parseRequest } from './index' + +const selectorPath = '@mpxjs/loaders/selector-loader' +const scriptSetupPath = '@mpxjs/loaders/script-setup-loader' + +interface defaultLangType { + template: 'wxml' + styles: 'wxss' + script: 'js' + json: 'json' + wxs: 'wxs' + [key: string]: string +} + +interface Options { + mpx: boolean + type: string + index: number + extract?: boolean + mode?: string + [key: string]: any +} + +const defaultLang: defaultLangType = { + template: 'wxml', + styles: 'wxss', + script: 'js', + json: 'json', + wxs: 'wxs' +} + +export function createHelpers (loaderContext: any) { + const rawRequest = loaderUtils.getRemainingRequest(loaderContext) + const { resourcePath, queryObj } = parseRequest(loaderContext.resource) + // @ts-ignore + const { mode, env } = loaderContext.getMpx() || {} + + function getRequire ( + type: string, + part: Record, + extraOptions: Record, + index?: number + ) { + return 'require(' + getRequestString(type, part, extraOptions, index) + ')' + } + + function getImport ( + type: string, + part: Record, + extraOptions: Record, + index: number + ) { + return ( + 'import __' + + type + + '__ from ' + + getRequestString(type, part, extraOptions, index) + ) + } + + function getNamedExports ( + type: string, + part: Record, + extraOptions: Record, + index: number + ) { + return 'export * from ' + getRequestString(type, part, extraOptions, index) + } + + function getFakeRequest (type: string, part: Record) { + const lang = part.lang || defaultLang[type] || type + const options = { ...queryObj } + if (lang === 'json') options.asScript = true + return addQuery(`${resourcePath}.${lang}`, options) + } + + function getRequestString ( + type: string, + part: Record, + extraOptions: Record, + index = 0 + ) { + const src = part.src + const options: Options = { + mpx: true, + type, + index, + ...extraOptions + } + + switch (type) { + case 'json': + options.asScript = true + if (part.useJSONJS) options.useJSONJS = true + // eslint-disable-next-line no-fallthrough + case 'styles': + case 'template': + options.extract = true + } + + if (part.mode) options.mode = part.mode + + if (src) { + return loaderUtils.stringifyRequest( + loaderContext, + addQuery(src, options, true) + ) + } else { + const fakeRequest = getFakeRequest(type, part) + let request = `${selectorPath}?mode=${mode}&env=${env}!${addQuery( + rawRequest, + options, + true + )}` + if (part.setup && type === 'script') request = scriptSetupPath + '!' + request + return loaderUtils.stringifyRequest( + loaderContext, + `${fakeRequest}!=!${request}` + ) + } + } + + return { + getRequire, + getImport, + getNamedExports, + getRequestString + } +} diff --git a/packages/compile-utils/src/index.ts b/packages/compile-utils/src/index.ts new file mode 100644 index 0000000000..887def2997 --- /dev/null +++ b/packages/compile-utils/src/index.ts @@ -0,0 +1,28 @@ +export * from './add-infix' +export * from './add-query' +export * from './ensure-array' +export * from './env' +export * from './gen-code' +export * from './gen-component-tag' +export * from './get-entry-name' +export * from './has-own' +export * from './helpers' +export * from './is-empty-object' +export * from './is-url-request' +export * from './is-valid-identifier-str' +export * from './match-condition' +export * from './mpx-json' +export * from './noop' +export * from './normalize' +export * from './omit' +export * from './parse-request' +export * from './pre-process-defs' +export * from './resolve' +export * from './resolve-module-context' +export * from './string' +export * from './stringify' +export * from './stringify-loaders-resource' +export * from './stringify-query' +export * from './to-posix' +export * from './ts-loader-watch-run-loader-filter' +export * from './type' diff --git a/packages/compile-utils/src/is-empty-object.ts b/packages/compile-utils/src/is-empty-object.ts new file mode 100644 index 0000000000..0f6f19b6dd --- /dev/null +++ b/packages/compile-utils/src/is-empty-object.ts @@ -0,0 +1,11 @@ +export function isEmptyObject (obj: any) { + if (!obj) { + return true + } + // @ts-ignore + // eslint-disable-next-line no-unreachable-loop + for (const key in obj) { + return false + } + return true +} diff --git a/packages/compile-utils/src/is-url-request.ts b/packages/compile-utils/src/is-url-request.ts new file mode 100644 index 0000000000..69a1f87bf0 --- /dev/null +++ b/packages/compile-utils/src/is-url-request.ts @@ -0,0 +1,22 @@ +import loaderUtils from 'loader-utils' +const tagRE = /\{\{((?:.|\n|\r)+?)\}\}(?!})/ + +export function isUrlRequest (url: string, root: any, externals: any[]) { + // 对于非字符串或空字符串url直接返回false + if (!url || typeof url !== 'string') return false + // 对于@开头且后续字符串为合法标识符的情况也返回false,识别为theme变量 + if (/^@[A-Za-z_$][A-Za-z0-9_$]*$/.test(url)) return false + if (/^.+:\/\//.test(url)) return false + // 对于url中存在Mustache插值的情况也返回false + if (tagRE.test(url)) return false + // url存在于externals中也返回false + if (externals && externals.some((external) => { + if (typeof external === 'string') { + return external === url + } else if (external instanceof RegExp) { + return external.test(url) + } + return false + })) return false + return loaderUtils.isUrlRequest(url, root) +} diff --git a/packages/compile-utils/src/is-valid-identifier-str.ts b/packages/compile-utils/src/is-valid-identifier-str.ts new file mode 100644 index 0000000000..d7f27a7869 --- /dev/null +++ b/packages/compile-utils/src/is-valid-identifier-str.ts @@ -0,0 +1,3 @@ +export function isValidIdentifierStr (str: string) { + return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(str) +} diff --git a/packages/compile-utils/src/match-condition.ts b/packages/compile-utils/src/match-condition.ts new file mode 100644 index 0000000000..3ca0fa8037 --- /dev/null +++ b/packages/compile-utils/src/match-condition.ts @@ -0,0 +1,50 @@ +const orMatcher = (items: string | any[]) => { + return (str: any) => { + for (let i = 0; i < items.length; i++) { + if (items[i](str)) return true + } + return false + } +} + +const normalizeCondition = (condition: any): any => { + if (!condition) throw new Error('Expected condition but got falsy value') + if (typeof condition === 'string') { + return (str: string | string[]) => str.indexOf(condition) !== -1 + } + if (typeof condition === 'function') { + return condition + } + if (condition instanceof RegExp) { + return condition.test.bind(condition) + } + if (Array.isArray(condition)) { + const items = condition.map(c => normalizeCondition(c)) + return orMatcher(items) + } + throw Error( + 'Unexcepted ' + + typeof condition + + ' when condition was expected (' + + condition + + ')' + ) +} + +// 匹配规则为include匹配到且未被exclude匹配到的资源为true,其余资源全部为false,如果需要实现不传include为全部匹配的话可以将include的默认值设置为()=>true进行传入 +const matchCondition = (resourcePath: any, condition: any = {}) => { + let matched = false + const includeMatcher = + condition.include && normalizeCondition(condition.include) + const excludeMatcher = + condition.exclude && normalizeCondition(condition.exclude) + if (includeMatcher && includeMatcher(resourcePath)) { + matched = true + } + if (excludeMatcher && excludeMatcher(resourcePath)) { + matched = false + } + return matched +} + +export { matchCondition, normalizeCondition } diff --git a/packages/compile-utils/src/mpx-json.ts b/packages/compile-utils/src/mpx-json.ts new file mode 100644 index 0000000000..f0b86a8f63 --- /dev/null +++ b/packages/compile-utils/src/mpx-json.ts @@ -0,0 +1,50 @@ +import path from 'path' + +// 将JS生成JSON +function compileMPXJSON ({ source, defs, filePath }: any) { + const defKeys = Object.keys(defs) + const defValues = defKeys.map(key => { + return defs[key] + }) + // eslint-disable-next-line no-new-func + const func = new Function( + 'exports', + 'require', + 'module', + '__filename', + '__dirname', + ...defKeys, + source + ) + // 模拟commonJS执行 + // support exports + const e = {} + const m = { + exports: e + } + const dirname = path.dirname(filePath) + func( + e, + function (modulePath: string) { + if (!path.isAbsolute(modulePath)) { + if (modulePath.indexOf('.') === 0) { + modulePath = path.resolve(dirname, modulePath) + } + } + return require(modulePath) + }, + m, + filePath, + dirname, + ...defValues + ) + return m.exports +} + +function compileMPXJSONText (opts: any) { + return JSON.stringify(compileMPXJSON(opts), null, 2) +} + +export { + compileMPXJSONText +} diff --git a/packages/compile-utils/src/noop.ts b/packages/compile-utils/src/noop.ts new file mode 100644 index 0000000000..e643899bad --- /dev/null +++ b/packages/compile-utils/src/noop.ts @@ -0,0 +1,2 @@ +// eslint-disable-next-line @typescript-eslint/no-empty-function +export const NOOP = () => {} diff --git a/packages/compile-utils/src/normalize.ts b/packages/compile-utils/src/normalize.ts new file mode 100644 index 0000000000..ab2191a9e9 --- /dev/null +++ b/packages/compile-utils/src/normalize.ts @@ -0,0 +1,5 @@ +export const normalize = { + lib: (file: string) => '@mpxjs/webpack-plugin/lib/' + file, + runtime: (file: string) => '@mpxjs/web-plugin/src/runtime/' + file, + utils: (file: string) => '@mpxjs/compile-utils/' + file +} diff --git a/packages/compile-utils/src/omit.ts b/packages/compile-utils/src/omit.ts new file mode 100644 index 0000000000..599743eec7 --- /dev/null +++ b/packages/compile-utils/src/omit.ts @@ -0,0 +1,8 @@ +export function omit( + obj: T, + omitKeys: K[] +): Omit { + const result = { ...obj } + omitKeys.forEach((key) => delete result[key]) + return result +} diff --git a/packages/compile-utils/src/parse-request.ts b/packages/compile-utils/src/parse-request.ts new file mode 100644 index 0000000000..662834cfaa --- /dev/null +++ b/packages/compile-utils/src/parse-request.ts @@ -0,0 +1,56 @@ +import path from 'path' +import { OptionObject, parseQuery } from 'loader-utils' +const seen = new Map() + +export interface Result { + resource: string + loaderString: string + resourcePath: string + resourceQuery: string + rawResourcePath: string + queryObj: OptionObject +} + +function genQueryObj (result: Result) { + // 避免外部修改queryObj影响缓存 + result.queryObj = parseQuery(result.resourceQuery || '?') + return result +} + +export function parseRequest (request: string) { + if (seen.has(request)) { + return genQueryObj(seen.get(request)) + } + const elements = request.split('!') + const resource = elements.pop() as string + const loaderString = elements.join('!') + let resourcePath = resource + let resourceQuery = '' + const queryIndex = resource.indexOf('?') + if (queryIndex >= 0) { + resourcePath = resource.slice(0, queryIndex) + resourceQuery = resource.slice(queryIndex) + } + const queryObj = parseQuery(resourceQuery || '?') + const rawResourcePath = resourcePath + if (queryObj.resourcePath) { + resourcePath = queryObj.resourcePath as string + } else if (queryObj.infix) { + const resourceDir = path.dirname(resourcePath) + const resourceBase = path.basename(resourcePath) + resourcePath = path.join( + resourceDir, + resourceBase.replace(queryObj.infix as string, '') + ) + } + const result = { + resource, + loaderString, + resourcePath, + resourceQuery, + rawResourcePath, + queryObj + } + seen.set(request, result) + return result +} diff --git a/packages/compile-utils/src/pre-process-defs.ts b/packages/compile-utils/src/pre-process-defs.ts new file mode 100644 index 0000000000..b797ba9b29 --- /dev/null +++ b/packages/compile-utils/src/pre-process-defs.ts @@ -0,0 +1,31 @@ +/** + * @file common util methods + */ + +/** + * 预处理一次常量对象,处理掉不符合标识符规则的变量,目前只处理'.',用于在JSON/style中使用的场景 + * @param {object} defs 待处理的常量 + * @returns {object} 处理完毕的常量对象 + */ +function preProcessDefs (defs: any) { + const newDefs: any = {} + Object.keys(defs).forEach(key => { + if (typeof key === 'string' && key.indexOf('.') !== -1) { + key.split('.').reduce((prev: any, curr, index, arr) => { + if (index === arr.length - 1) { + prev[curr] = defs[key] + } else { + prev[curr] = prev[curr] || {} + } + return prev[curr] + }, newDefs) + } else { + newDefs[key] = defs[key] + } + }) + return newDefs +} + +export { + preProcessDefs +} diff --git a/packages/compile-utils/src/query.d.ts b/packages/compile-utils/src/query.d.ts new file mode 100644 index 0000000000..bf34d7eda4 --- /dev/null +++ b/packages/compile-utils/src/query.d.ts @@ -0,0 +1,6 @@ +import 'loader-utils' +declare module 'loader-utils' { + export interface OptionObject { + context?: string + } +} diff --git a/packages/compile-utils/src/resolve-module-context.ts b/packages/compile-utils/src/resolve-module-context.ts new file mode 100644 index 0000000000..186b7e9de8 --- /dev/null +++ b/packages/compile-utils/src/resolve-module-context.ts @@ -0,0 +1,5 @@ +import path from 'path' + +export function resolveModuleContext (moduleId: string): string { + return path.dirname(moduleId) +} diff --git a/packages/compile-utils/src/resolve.ts b/packages/compile-utils/src/resolve.ts new file mode 100644 index 0000000000..a217eccd73 --- /dev/null +++ b/packages/compile-utils/src/resolve.ts @@ -0,0 +1,21 @@ +import { parseRequest } from './parse-request' +import { LoaderContext } from 'webpack' + +type LoaderContextResolveCallback = Parameters< + LoaderContext['resolve'] +>[2] +// todo 提供不记录dependency的resolve方法,非必要的情况下不记录dependency,提升缓存利用率 +export function resolve ( + context: string, + request: string, + loaderContext: LoaderContext, + callback: LoaderContextResolveCallback +) { + const { queryObj } = parseRequest(request) + context = queryObj.context || context + return loaderContext.resolve(context, request, (err, resource, info) => { + if (err) return callback(err) + if (resource === false) return callback(new Error('Resolve ignored!')) + callback(null, resource, info) + }) +} diff --git a/packages/compile-utils/src/set.ts b/packages/compile-utils/src/set.ts new file mode 100644 index 0000000000..7fa6bc297a --- /dev/null +++ b/packages/compile-utils/src/set.ts @@ -0,0 +1,51 @@ +export type ItemType = string | Record +export type SetType = Set +export type FnType = (item: ItemType) => unknown + +export default { + every (set: SetType, fn: FnType) { + for (const item of set) { + if (!fn(item)) return false + } + return true + }, + has (set: SetType, fn: FnType) { + for (const item of set) { + if (fn(item)) return true + } + return false + }, + map (set: SetType, fn: FnType) { + const result = new Set() + set.forEach((item: ItemType) => { + result.add(fn(item)) + }) + return result + }, + filter (set: SetType, fn: FnType) { + const result = new Set() + set.forEach((item: ItemType) => { + if (fn(item)) { + result.add(item) + } + }) + return result + }, + concat (setA: SetType, setB: SetType) { + const result = new Set() + setA.forEach((item: ItemType) => { + result.add(item) + }) + setB.forEach((item: ItemType) => { + result.add(item) + }) + return result + }, + mapToArr (set: SetType, fn: FnType) { + const result: Array = [] + set.forEach((item: ItemType) => { + result.push(fn(item)) + }) + return result + } +} diff --git a/packages/compile-utils/src/string.ts b/packages/compile-utils/src/string.ts new file mode 100644 index 0000000000..dfe51179e2 --- /dev/null +++ b/packages/compile-utils/src/string.ts @@ -0,0 +1,30 @@ +function isCapital (c: string) { + return /[A-Z]/.test(c) +} + +function isMustache (str: string) { + return /\{\{((?:.|\n|\r)+?)\}\}(?!})/.test(str) +} + +// WordExample/wordExample -> word-example +function capitalToHyphen (v: string) { + let ret = '' + for (let c, i = 0; i < v.length; i++) { + c = v[i] + if (isCapital(c)) { + if (i === 0) { + c = c.toLowerCase() + } else { + c = '-' + c.toLowerCase() + } + } + ret += c + } + return ret +} + +export { + isCapital, + isMustache, + capitalToHyphen +} diff --git a/packages/compile-utils/src/stringify-loaders-resource.ts b/packages/compile-utils/src/stringify-loaders-resource.ts new file mode 100644 index 0000000000..e4fc357e61 --- /dev/null +++ b/packages/compile-utils/src/stringify-loaders-resource.ts @@ -0,0 +1,23 @@ +const loaderToIdent = (data: { options: string; loader: string; ident: string }) => { + if (!data.options) { + return data.loader + } + if (typeof data.options === 'string') { + return data.loader + '?' + data.options + } + if (typeof data.options !== 'object') { + throw new Error('loader options must be string or object') + } + if (data.ident) { + return data.loader + '??' + data.ident + } + return data.loader + '?' + JSON.stringify(data.options) +} + +export const stringifyLoadersAndResource = (loaders: any, resource: string) => { + let str = '' + for (const loader of loaders) { + str += loaderToIdent(loader) + '!' + } + return str + resource +} diff --git a/packages/compile-utils/src/stringify-query.ts b/packages/compile-utils/src/stringify-query.ts new file mode 100644 index 0000000000..6ff24bcf01 --- /dev/null +++ b/packages/compile-utils/src/stringify-query.ts @@ -0,0 +1,44 @@ +/** + * stringify object to query string, started with '?' + * @param {Object} obj + * @param {boolean} useJSON + * @return {string} queryString + */ +import JSON5 from 'json5' + +export function stringifyQuery (obj: any, useJSON?: boolean) { + if (useJSON) return `?${JSON5.stringify(obj)}` + + const res = obj + ? Object.keys(obj) + .sort() + .map(key => { + const val = obj[key] + + if (val === undefined) { + return val + } + + if (val === true) { + return key + } + + if (Array.isArray(val)) { + + const key2 = `${key}[]` + const result: string[] = [] + val.slice().forEach(val2 => { + if (val2 === undefined) { + return + } + result.push(`${key2}=${encodeURIComponent(val2)}`) + }) + return result.join('&') + } + return `${key}=${encodeURIComponent(val)}` + }) + .filter(x => x) + .join('&') + : null + return res ? `?${res}` : '' +} diff --git a/packages/compile-utils/src/stringify.ts b/packages/compile-utils/src/stringify.ts new file mode 100644 index 0000000000..23cf0aa634 --- /dev/null +++ b/packages/compile-utils/src/stringify.ts @@ -0,0 +1,30 @@ +import { hasOwn } from './has-own' +const stringify = JSON.stringify.bind(JSON) + +export { stringify } + +export function stringifyObject ( + obj?: Record +): Record { + const result: Record = {} + if (obj) { + Object.keys(obj).forEach(key => { + result[key] = stringify(obj[key]) + }) + } + return result +} + +export function shallowStringify (obj: Record): string { + const arr = [] + for (const key in obj) { + if (hasOwn(obj, key)) { + let value = obj[key] + if (Array.isArray(value)) { + value = `[${value.join(',')}]` + } + arr.push(`'${key}':${value}`) + } + } + return `{${arr.join(',')}}` +} diff --git a/packages/compile-utils/src/to-posix.ts b/packages/compile-utils/src/to-posix.ts new file mode 100644 index 0000000000..1beb15ca67 --- /dev/null +++ b/packages/compile-utils/src/to-posix.ts @@ -0,0 +1,3 @@ +export function toPosix (path: string) { + return path.replace(/\\/g, '/') +} diff --git a/packages/compile-utils/src/ts-loader-watch-run-loader-filter.ts b/packages/compile-utils/src/ts-loader-watch-run-loader-filter.ts new file mode 100644 index 0000000000..380f1f9cfe --- /dev/null +++ b/packages/compile-utils/src/ts-loader-watch-run-loader-filter.ts @@ -0,0 +1,23 @@ +import set from './set' +const selectorPath = '@mpxjs/loaders/selector-loader.js' +const scriptSetupPath = '@mpxjs/loaders/script-setup-loader.js' +const webMpxLoaderPath = '@mpxjs/web-plugin/webpack/loader/web-loader.js' +const mpxLoaderPath = '@mpxjs/webpack-plugin/lib/loader.js' +const tsLoaderWatchRunFilterLoaders = new Set([ + selectorPath, + scriptSetupPath, + mpxLoaderPath, + webMpxLoaderPath, + 'node_modules/vue-loader/lib/index.js' +]) + +export function tsWatchRunLoaderFilter (loaders: Array>, loaderIndex: number) { + for (let len = loaders.length; len > 0; --len) { + const currentLoader = loaders[len - 1] + if (!set.has(tsLoaderWatchRunFilterLoaders, filterLoaderPath => currentLoader.path.endsWith(filterLoaderPath))) { + break + } + loaderIndex-- + } + return loaderIndex +} diff --git a/packages/compile-utils/src/type.ts b/packages/compile-utils/src/type.ts new file mode 100644 index 0000000000..d2dbea8a5c --- /dev/null +++ b/packages/compile-utils/src/type.ts @@ -0,0 +1,3 @@ +export function type (n: any) { + return Object.prototype.toString.call(n).slice(8, -1) +} diff --git a/packages/compile-utils/tsconfig.json b/packages/compile-utils/tsconfig.json new file mode 100644 index 0000000000..b091ad4c02 --- /dev/null +++ b/packages/compile-utils/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.root.json", + "compilerOptions": { + "baseUrl": ".", + "declaration": true, + "outDir": "dist", + "module": "commonjs" + }, + "include": ["src"], + "exclude": ["node_modules"] +} diff --git a/packages/compiler/README.md b/packages/compiler/README.md new file mode 100644 index 0000000000..29c2e4a0d7 --- /dev/null +++ b/packages/compiler/README.md @@ -0,0 +1 @@ +# `@mpxjs/compiler` diff --git a/packages/compiler/package.json b/packages/compiler/package.json new file mode 100644 index 0000000000..58815090b3 --- /dev/null +++ b/packages/compiler/package.json @@ -0,0 +1,46 @@ +{ + "name": "@mpxjs/compiler", + "version": "2.7.40", + "description": "mpx compile", + "keywords": [ + "mpx" + ], + "homepage": "https://didi.github.io/mpx/", + "bugs": { + "url": "https://github.com/didi/mpx/issues" + }, + "repository": { + "type": "git", + "url": "git@github.com:didi/mpx.git" + }, + "license": "ISC", + "author": "lareinayanyu", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "rimraf ./dist && tsc", + "dev": "tsc -w" + }, + "dependencies": { + "@babel/code-frame": "^7.16.0", + "@babel/generator": "^7.16.0", + "@babel/parser": "^7.16.2", + "@babel/traverse": "^7.16.0", + "@babel/types": "^7.16.0", + "@mpxjs/compile-utils": "workspace:*", + "@mpxjs/plugin-proxy": "workspace:*", + "hash-sum": "^1.0.2", + "he": "^1.1.1", + "json5": "^2.1.3", + "loader-utils": "^2.0.2", + "lru-cache": "^4.1.2", + "magic-string": "^0.26.2", + "postcss": "^8.4.5", + "postcss-load-config": "^3.1.1", + "postcss-selector-parser": "^6.0.8", + "source-map": "^0.6.1" + }, + "publishConfig": { + "registry": "https://registry.npmjs.org" + } +} diff --git a/packages/compiler/src/global.d.ts b/packages/compiler/src/global.d.ts new file mode 100644 index 0000000000..44d4b4a0b1 --- /dev/null +++ b/packages/compiler/src/global.d.ts @@ -0,0 +1,15 @@ + +declare module 'lru-cache' { + class LruCache { + constructor(cache: number); + + get(cacheKey: string): T; + + set(cacheKey: string, content: string): void; + } + export default LruCache +} + +declare module 'he' {} + +declare module 'consolidate' {} \ No newline at end of file diff --git a/packages/compiler/src/index.ts b/packages/compiler/src/index.ts new file mode 100644 index 0000000000..8a0183b984 --- /dev/null +++ b/packages/compiler/src/index.ts @@ -0,0 +1,11 @@ +import platform from './platform' +import templateCompiler from './template-compiler' +import scriptSetupCompiler from './script-setup-compiler/index' +import styleCompiler from './style-compiler' +import jsonCompiler from './json-compiler' + +export * from './template-compiler/index' +export * from './style-compiler/index' +export * from './json-compiler' + +export { templateCompiler, styleCompiler, jsonCompiler, platform, scriptSetupCompiler } diff --git a/packages/compiler/src/json-compiler/index.ts b/packages/compiler/src/json-compiler/index.ts new file mode 100644 index 0000000000..ebbbcfe195 --- /dev/null +++ b/packages/compiler/src/json-compiler/index.ts @@ -0,0 +1,155 @@ +import { parseRequest, stringify } from '@mpxjs/compile-utils' +import { ProxyPluginContext } from '@mpxjs/plugin-proxy' +import fs from 'fs' +import json5 from 'json5' +import path from 'path' +import { promisify } from 'util' +import { CompilerResult } from '../template-compiler' +import { JsonConfig } from './json-config' + +export * from './json-config' + +export const DEFAULT_TAB_BAR_CONFIG = { + borderStyle: 'black', + position: 'bottom', + custom: false, + isShow: true +} + +export const JSON_JS_EXT = '.json.js' + +function evalJSONJS( + source: string, + filename: string, + defs: Record, + fs: any, + callback: (filename: string) => void +): Record { + const defKeys = Object.keys(defs) + const defValues = defKeys.map(key => { + return defs[key] + }) + const dirname = path.dirname(filename) + // eslint-disable-next-line no-new-func + const func = new Function( + 'module', + 'exports', + 'require', + '__filename', + '__dirname', + ...defKeys, + source + ) + const module = { + exports: {} + } + // 此处采用readFileSync+evalJSONJS而不直接使用require获取依赖内容有两个原因: + // 1. 支持依赖中正常访问defs变量 + // 2. 避免对应的依赖文件被作为buildDependencies + func( + module, + module.exports, + function (request: string) { + if (request.startsWith('.')) { + request = path.join(dirname, request) + } + const filename = require.resolve(request) + callback(filename) + const source = fs.readFileSync(filename).toString('utf-8') + return evalJSONJS(source, filename, fs, defs || {}, callback) + }, + filename, + dirname, + ...defValues + ) + + return module.exports +} + +async function getJSONContent( + json: CompilerResult['json'], + filename: string, + pluginContext: ProxyPluginContext, + defs: Record | unknown, + fs: any +) { + let jsonPath = filename + if (json) { + let jsonContent = json.content + let useJSONJS = json.useJSONJS + let resourcePath = '' + if (json.src) { + const resolvedJsonPath = await pluginContext.resolve(json.src, filename) + if (resolvedJsonPath) { + const { rawResourcePath } = parseRequest(resolvedJsonPath.id) + jsonPath = resolvedJsonPath.id + useJSONJS = rawResourcePath.endsWith(JSON_JS_EXT) + const readFile = promisify(fs.readFile) + jsonContent = await readFile(rawResourcePath, 'utf-8') + json.content = jsonContent + resourcePath = rawResourcePath + pluginContext.addDependency(resolvedJsonPath.id) + } + } + if (useJSONJS) { + return { + content: stringify( + evalJSONJS(jsonContent, resourcePath, defs || {}, fs, filename => { + pluginContext.addDependency(filename) + }) + ), + path: jsonPath + } + } + return { + content: jsonContent, + path: jsonPath + } + } + return { + content: '{}', + path: jsonPath + } +} + +/** + * resolve json content + * @param descriptor - SFCDescriptor + * @param pluginContext - TransformPluginContext + * @param options - ResolvedOptions + * @returns json config + */ +async function parse( + compilerResult: CompilerResult, + context: string, + pluginContext: ProxyPluginContext, + defs: any, + fsInfo?: any +): Promise { + const { json } = compilerResult + const jsonContent = await getJSONContent( + json, + context, + pluginContext, + defs, + fsInfo || fs + ) + const jsonResult = json5.parse(jsonContent.content) + if (jsonResult.tabBar) { + jsonResult.tabBar = { + ...DEFAULT_TAB_BAR_CONFIG, + ...jsonResult.tabBar + } + } + return { + ...jsonResult, + path: jsonContent.path + } +} + +const jsonCompiler = { + parse +} + + +export default jsonCompiler \ No newline at end of file diff --git a/packages/compiler/src/json-compiler/json-config.ts b/packages/compiler/src/json-compiler/json-config.ts new file mode 100644 index 0000000000..9aecea69a1 --- /dev/null +++ b/packages/compiler/src/json-compiler/json-config.ts @@ -0,0 +1,46 @@ +/** + * wechat miniprogram app/page/component config type + */ +export type TabBarItem = { + pagePath: string + text: string + iconPath?: string + selectedIconPath?: string +} + +export interface JsonConfig { + path?: string, + component?: boolean + usingComponents?: Record + componentGenerics?: Record + packages?: string[] + pages?: ( + | string + | { + src: string + path: string + } + )[] + tabBar?: { + custom?: boolean + color?: string + selectedColor?: string + backgroundColor?: string + list?: TabBarItem[] + } + networkTimeout?: { + request: number + connectSocket: number + uploadFile: number + downloadFile: number + } + subpackages?: { + root?: 'string' + pages: JsonConfig['pages'] + }[] + window?: Record + style?: string + singlePage?: { + navigationBarFit: boolean + } +} \ No newline at end of file diff --git a/packages/compiler/src/platform/index.ts b/packages/compiler/src/platform/index.ts new file mode 100644 index 0000000000..1b7771ce9a --- /dev/null +++ b/packages/compiler/src/platform/index.ts @@ -0,0 +1,25 @@ +// @ts-nocheck +import runRules from './run-rules' +import wxTemplate from './template/wx' +import wxJson from './json/wx' + +export default function getRulesRunner ({ type, mode, srcMode, data, meta, testKey, mainKey, waterfall, warn, error }) { + const specMap = { + template: { + wx: wxTemplate({ warn, error }) + }, + json: { + wx: wxJson({ warn, error }) + } + } + const spec = specMap[type] && specMap[type][srcMode] + if (spec && spec.supportedModes.indexOf(mode) > -1) { + const normalizeTest = spec.normalizeTest + const mainRules = mainKey ? spec[mainKey] : spec + if (mainRules) { + return function (input) { + return runRules(mainRules, input, { mode, data, meta, testKey, waterfall, normalizeTest }) + } + } + } +} diff --git a/packages/compiler/src/platform/json/change-key.ts b/packages/compiler/src/platform/json/change-key.ts new file mode 100644 index 0000000000..a51f667965 --- /dev/null +++ b/packages/compiler/src/platform/json/change-key.ts @@ -0,0 +1,6 @@ +export default function changeKey (input: Record, srcKey: string, targetKey: string) { + const value = input[srcKey] + delete input[srcKey] + input[targetKey] = value + return input +} diff --git a/packages/compiler/src/platform/json/normalize-test.ts b/packages/compiler/src/platform/json/normalize-test.ts new file mode 100644 index 0000000000..1b2fa25bce --- /dev/null +++ b/packages/compiler/src/platform/json/normalize-test.ts @@ -0,0 +1,20 @@ +import { hasOwn } from '@mpxjs/compile-utils' + +export default function normalizeTest (test: string) { + if (test) { + return (input: Record, meta: Record) => { + const pathArr = test.split('|') + meta.paths = [] + let result = false + for (let i = 0; i < pathArr.length; i++) { + if (hasOwn(input, pathArr[i])) { + meta.paths.push(pathArr[i]) + result = true + } + } + return result + } + } else { + return () => true + } +} diff --git a/packages/compiler/src/platform/json/wx/index.ts b/packages/compiler/src/platform/json/wx/index.ts new file mode 100644 index 0000000000..578b9d499e --- /dev/null +++ b/packages/compiler/src/platform/json/wx/index.ts @@ -0,0 +1,403 @@ +// @ts-nocheck +import runRules from '../../run-rules' +import normalizeTest from '../normalize-test' +import changeKey from '../change-key' +import { normalize } from '@mpxjs/compile-utils' +const mpxViewPath = normalize.lib('runtime/components/ali/mpx-view.mpx') +const mpxTextPath = normalize.lib('runtime/components/ali/mpx-text.mpx') + +export default function getSpec ({ warn, error }) { + function print (mode, path, isError) { + const msg = `Json path <${path}> is not supported in ${mode} environment!` + isError ? error(msg) : warn(msg) + } + + function deletePath (opts) { + let isError = opts + let shouldLog = true + if (typeof opts === 'object') { + shouldLog = !opts.noLog + isError = opts.isError + } + + return function (input, { mode, pathArr = [] }, meta) { + const currPath = meta.paths.join('|') + if (shouldLog) { + print(mode, pathArr.concat(currPath).join('.'), isError) + } + meta.paths.forEach((path) => { + delete input[path] + }) + return input + } + } + + /** + * @desc 在app.mpx里配置usingComponents作为全局组件 + */ + function addGlobalComponents (input, { globalComponents }) { + if (globalComponents) { + input.usingComponents = Object.assign({}, globalComponents, input.usingComponents) + } + return input + } + + // 处理支付宝 componentPlaceholder 不支持 view、text 原生标签 + function aliComponentPlaceholderFallback (input) { + const componentPlaceholder = input.componentPlaceholder + const usingComponents = input.usingComponents || (input.usingComponents = {}) + for (const cph in componentPlaceholder) { + const cur = componentPlaceholder[cph] + const placeholderCompMatched = cur.match(/^(?:view|text)$/g) + if (!Array.isArray(placeholderCompMatched)) continue + let compName, compPath + switch (placeholderCompMatched[0]) { + case 'view': + compName = 'mpx-view' + compPath = mpxViewPath + break + case 'text': + compName = 'mpx-text' + compPath = mpxTextPath + } + usingComponents[compName] = compPath + componentPlaceholder[cph] = compName + } + return input + } + + const spec = { + supportedModes: ['ali', 'swan', 'qq', 'tt', 'jd', 'qa', 'dd'], + normalizeTest, + page: [ + { + test: 'navigationBarTitleText', + ali (input) { + return changeKey(input, this.test, 'defaultTitle') + } + }, + { + test: 'enablePullDownRefresh', + ali (input) { + input = changeKey(input, this.test, 'pullRefresh') + if (input.pullRefresh) { + input.allowsBounceVertical = 'YES' + } + return input + }, + jd: deletePath() + }, + { + test: 'navigationBarBackgroundColor', + ali (input) { + return changeKey(input, this.test, 'titleBarColor') + } + }, + { + test: 'disableSwipeBack', + ali: deletePath(), + qq: deletePath(), + jd: deletePath(), + swan: deletePath() + }, + { + test: 'onReachBottomDistance', + qq: deletePath(), + jd: deletePath() + }, + { + test: 'disableScroll', + ali: deletePath(), + qq: deletePath(), + jd: deletePath() + }, + { + test: 'backgroundColorTop|backgroundColorBottom', + ali: deletePath(), + swan: deletePath() + }, + { + test: 'navigationBarTextStyle|navigationStyle|backgroundTextStyle', + ali: deletePath() + }, + { + test: 'pageOrientation', + ali: deletePath(), + swan: deletePath(), + tt: deletePath(), + jd: deletePath() + }, + { + test: 'componentPlaceholder', + ali: aliComponentPlaceholderFallback + }, + { + ali: addGlobalComponents, + swan: addGlobalComponents, + qq: addGlobalComponents, + tt: addGlobalComponents, + jd: addGlobalComponents + } + ], + component: [ + { + test: 'componentGenerics', + ali: deletePath(true) + }, + { + test: 'componentPlaceholder', + ali: aliComponentPlaceholderFallback + }, + { + ali: addGlobalComponents, + swan: addGlobalComponents, + qq: addGlobalComponents, + tt: addGlobalComponents + } + ], + tabBar: { + list: [ + { + test: 'text', + ali (input) { + return changeKey(input, this.test, 'name') + } + }, + { + test: 'iconPath', + ali (input) { + return changeKey(input, this.test, 'icon') + } + }, + { + test: 'selectedIconPath', + ali (input) { + return changeKey(input, this.test, 'activeIcon') + } + } + ], + rules: [ + { + test: 'color', + ali (input) { + return changeKey(input, this.test, 'textColor') + } + }, + { + test: 'list', + ali (input) { + const value = input.list + delete input.list + input.items = value.map(item => { + return runRules(spec.tabBar.list, item, { + mode: 'ali', + normalizeTest, + waterfall: true, + data: { + pathArr: ['tabBar', 'list'] + } + }) + }) + return input + } + }, + { + test: 'position', + ali: deletePath(), + swan: deletePath() + }, + { + test: 'borderStyle', + ali: deletePath() + }, + { + test: 'custom', + ali: deletePath(), + swan: deletePath(), + tt: deletePath(), + jd: deletePath() + } + ] + }, + rules: [ + { + test: 'resizable', + ali: deletePath(), + qq: deletePath(), + swan: deletePath(), + tt: deletePath(), + jd: deletePath() + }, + { + test: 'preloadRule', + tt: deletePath(), + jd: deletePath() + }, + { + test: 'functionalPages', + ali: deletePath(true), + qq: deletePath(true), + swan: deletePath(true), + tt: deletePath(), + jd: deletePath(true) + }, + { + test: 'plugins', + qq: deletePath(true), + swan: deletePath(true), + tt: deletePath(), + jd: deletePath(true) + }, + { + test: 'usingComponents', + ali: deletePath({ noLog: true }), + qq: deletePath({ noLog: true }), + swan: deletePath({ noLog: true }), + tt: deletePath({ noLog: true }), + jd: deletePath({ noLog: true }) + }, + { + test: 'debug', + ali: deletePath(), + swan: deletePath() + }, + { + test: 'requiredBackgroundModes', + ali: deletePath(), + tt: deletePath() + }, + { + test: 'workers', + jd: deletePath(), + ali: deletePath(), + swan: deletePath(), + tt: deletePath() + }, + { + test: 'subpackages|subPackages', + jd: deletePath(true) + }, + { + test: 'packages', + jd: deletePath() + }, + { + test: 'navigateToMiniProgramAppIdList|networkTimeout', + ali: deletePath(), + jd: deletePath() + }, + { + test: 'tabBar', + ali (input) { + input.tabBar = runRules(spec.tabBar, input.tabBar, { + mode: 'ali', + normalizeTest, + waterfall: true, + data: { + pathArr: ['tabBar'] + } + }) + }, + qq (input) { + input.tabBar = runRules(spec.tabBar, input.tabBar, { + mode: 'qq', + normalizeTest, + waterfall: true, + data: { + pathArr: ['tabBar'] + } + }) + }, + swan (input) { + input.tabBar = runRules(spec.tabBar, input.tabBar, { + mode: 'swan', + normalizeTest, + waterfall: true, + data: { + pathArr: ['tabBar'] + } + }) + }, + tt (input) { + input.tabBar = runRules(spec.tabBar, input.tabBar, { + mode: 'tt', + normalizeTest, + waterfall: true, + data: { + pathArr: ['tabBar'] + } + }) + }, + jd (input) { + input.tabBar = runRules(spec.tabBar, input.tabBar, { + mode: 'jd', + normalizeTest, + waterfall: true, + data: { + pathArr: ['tabBar'] + } + }) + } + }, + { + test: 'window', + ali (input) { + input.window = runRules(spec.page, input.window, { + mode: 'ali', + normalizeTest, + waterfall: true, + data: { + pathArr: ['window'] + } + }) + return input + }, + qq (input) { + input.window = runRules(spec.page, input.window, { + mode: 'qq', + normalizeTest, + waterfall: true, + data: { + pathArr: ['window'] + } + }) + return input + }, + swan (input) { + input.window = runRules(spec.page, input.window, { + mode: 'swan', + normalizeTest, + waterfall: true, + data: { + pathArr: ['window'] + } + }) + return input + }, + tt (input) { + input.window = runRules(spec.page, input.window, { + mode: 'tt', + normalizeTest, + waterfall: true, + data: { + pathArr: ['window'] + } + }) + return input + }, + jd (input) { + input.window = runRules(spec.page, input.window, { + mode: 'jd', + normalizeTest, + waterfall: true, + data: { + pathArr: ['window'] + } + }) + return input + } + } + ] + } + return spec +} diff --git a/packages/compiler/src/platform/run-rules.ts b/packages/compiler/src/platform/run-rules.ts new file mode 100644 index 0000000000..4f6fa4d64e --- /dev/null +++ b/packages/compiler/src/platform/run-rules.ts @@ -0,0 +1,40 @@ +// @ts-nocheck +import { type } from '@mpxjs/compile-utils' + +function defaultNormalizeTest (rawTest, context) { + const testType = type(rawTest) + switch (testType) { + case 'Function': + return rawTest.bind(context) + case 'RegExp': + return input => rawTest.test(input) + case 'String': + return input => rawTest === input + default: + return () => true + } +} + +export default function runRules (rules = [], input, options = {}) { + const { mode, testKey, normalizeTest, data = {}, meta = {}, waterfall } = options + rules = rules.rules || rules + for (let i = 0; i < rules.length; i++) { + const rule = rules[i] + const tester = (normalizeTest || defaultNormalizeTest)(rule.test, rule) + const testInput = testKey ? input[testKey] : input + const processor = rule[mode] + // mode传入data中供processor使用 + Object.assign(data, { + mode + }) + if (tester(testInput, meta) && processor) { + const result = processor.call(rule, input, data, meta) + meta.processed = true + if (result !== undefined) { + input = result + } + if (!waterfall) break + } + } + return input +} diff --git a/packages/compiler/src/platform/template/normalize-component-rules.ts b/packages/compiler/src/platform/template/normalize-component-rules.ts new file mode 100644 index 0000000000..a7b74aeb7b --- /dev/null +++ b/packages/compiler/src/platform/template/normalize-component-rules.ts @@ -0,0 +1,68 @@ +// @ts-nocheck + +import runRules from '../run-rules' +import templateCompiler from '../../template-compiler' +import { Config } from './wx/component-config' + +/** + * @desc 针对每一个组件(属性,event,指令等)执行规则判断 + * @params cfgs [{test: 'camera', props:[], event: []}] 组件配置列表 + * @params spec ../index.js中公共的spec + */ +export default function normalizeComponentRules (cfgs: Config[], spec) { + return cfgs.map((cfg) => { + const result = {} + if (cfg.test) { + result.test = cfg.test + } + const supportedModes = cfg.supportedModes || spec.supportedModes + // 合并component-config中组件的event 与index中公共的event规则 + const eventRules = (cfg.event || []).concat(spec.event.rules) + supportedModes.forEach((mode) => { + result[mode] = function (el, data) { + data = Object.assign({}, data, { el, eventRules }) + const testKey = 'name' + let rAttrsList = [] + const options = { + mode, + testKey, + data + } + el.attrsList.forEach((attr) => { + const meta = {} + let rAttr = runRules(spec.directive, attr, { + ...options, + meta + }) + // 指令未匹配到时说明为props,因为目前所有的指令都需要转换 + if (!meta.processed) { + rAttr = runRules(spec.preProps, rAttr, options) + rAttr = runRules(cfg.props, rAttr, options) + if (Array.isArray(rAttr)) { + rAttr = rAttr.map((attr) => { + return runRules(spec.postProps, attr, options) + }) + } else if (rAttr !== false) { + rAttr = runRules(spec.postProps, rAttr, options) + } + } + // 生成目标attrsList + if (Array.isArray(rAttr)) { + rAttrsList = rAttrsList.concat(rAttr) + } else if (rAttr !== false) { + rAttrsList.push(rAttr) + } + }) + el.attrsList = rAttrsList + el.attrsMap = templateCompiler.compiler.makeAttrsMap(rAttrsList) + // 前置处理attrs,便于携带信息用于tag的处理 + const rTag = cfg[mode] && cfg[mode].call(this, el.tag, data) + if (rTag) { + el.tag = rTag + } + return el + } + }) + return result + }) +} diff --git a/packages/compiler/src/platform/template/wx/component-config/README.md b/packages/compiler/src/platform/template/wx/component-config/README.md new file mode 100644 index 0000000000..f47e1244a3 --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/README.md @@ -0,0 +1,39 @@ +# 基于微信的模板转换规则 + +> 每个文件是一个微信组件的规则。需要为mpx跨的所有平台编写对应的转换规则。 + +## web + +web的额外逻辑,因为小程序组件和web存在差异,比如事件相关的东西,因此可能无法直接单纯地转换,因此会做额外判断决定是否注入一个内建组件来polyfill,这些内建组件在 `/lib/runtime/components/web` 。 + +## 规则结构 + +每个文件对应一个组件,里面要覆盖到Mpx支持的所有平台。 + +每个平台不支持的组件写在 unsupported.js 中。 + +Mpx的转换一个重要原则是转不了的东西通过控制台打印提示用户,要求用户提供一份符合对应平台的代码通过条件编译支持。因此错误输出格式保持一致是有必要的。 + +在 index.js 中,会汇总每个组件的转换规则函数,为了使错误信息标准化,一致化,错误打印函数的生成函数实现在index.js里。 + +每个组件文件是一个方法,接受错误打印生成方法,根据组件名生成对应的错误打印方法。 + +在转换规则对应的平台处理方法里决定是否要进行错误打印。 + +## 转换规则编写指南 + +每一条规则其实是一个对象。主要的属性有: + +test: 用于匹配标签名,可以是字符串或正则 + +\[平台(如wx/qq/swan/ali/tt/web)]: 一个方法,用于处理这个平台下的标签转换,如view在web下需要判断是否有事件决定转成普通的div还是内建组件mpx-view,参数待完善 + +props和event: 都是数组,每一项都如大规则一样包含test用于命中要转换的项,然后是平台方法,用于处理转换。 + +## 单元测试 + +鉴于转换规则可能在不同时间被多人多次编辑,且规则繁杂,为了避免框架底层改动/他人规则改动导致之前编写的规则失败,建议为转换编写对应的单元测试case。 + +同时,单元测试也可以帮助你更好更快编写转换规则,而无需找个真实的项目尝试。 + +转换规则的单元测试放置在 webpack-plugin/test/platform/wx 下,util中提供了一个转换方法,可参考之前的示例进行。 diff --git a/packages/compiler/src/platform/template/wx/component-config/ad.ts b/packages/compiler/src/platform/template/wx/component-config/ad.ts new file mode 100644 index 0000000000..75638cb536 --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/ad.ts @@ -0,0 +1,57 @@ +import { DefineConfig } from "." + +const TAG_NAME = 'ad' + +export default function ({ print }) { + const ttValueWarningLog = print({ platform: 'bytedance', type: 'value', tag: TAG_NAME, isError: false }) + const ttPropLog = print({ platform: 'bytedance', tag: TAG_NAME, isError: false }) + const baiduValueWarningLog = print({ platform: 'baidu', type: 'value', tag: TAG_NAME, isError: false }) + const baiduPropLog = print({ platform: 'baidu', tag: TAG_NAME, isError: false }) + const qqValueWarningLog = print({ platform: 'qq', type: 'value', tag: TAG_NAME, isError: false }) + const qqPropLog = print({ platform: 'qq', tag: TAG_NAME, isError: false }) + const qqEventLog = print({ platform: 'qq', tag: TAG_NAME, isError: false, type: 'event' }) + return { + test: TAG_NAME, + props: [ + { + test: /^ad-type$/, + tt (obj) { + obj.name = 'type' + if (obj.value === 'grid') { + ttValueWarningLog({ name: 'type', value: obj.value }) + } + return obj + }, + qq (obj) { + obj.name = 'type' + if (obj.value === 'grid' || obj.value === 'video') { + qqValueWarningLog({ name: 'type', value: obj.value }) + } + return obj + }, + swan (obj) { + obj.name = 'type' + if (obj.value === 'grid' || obj.value === 'video') { + baiduValueWarningLog({ name: 'type', value: obj.value }) + } + return obj + } + }, + { + test: /^ad-theme$/, + tt: ttPropLog + }, + { + test: /^(ad-intervals|ad-theme)$/, + qq: qqPropLog, + swan: baiduPropLog + } + ], + event: [ + { + test: /^(close)$/, + qq: qqEventLog + } + ] + } +} diff --git a/packages/compiler/src/platform/template/wx/component-config/block.ts b/packages/compiler/src/platform/template/wx/component-config/block.ts new file mode 100644 index 0000000000..84d67e790b --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/block.ts @@ -0,0 +1,12 @@ +import { DefineConfig } from "." + +const TAG_NAME = 'block' + +export default function () { + return { + test: TAG_NAME, + web () { + return 'template' + } + } +} diff --git a/packages/compiler/src/platform/template/wx/component-config/button.ts b/packages/compiler/src/platform/template/wx/component-config/button.ts new file mode 100644 index 0000000000..1d8c9824c7 --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/button.ts @@ -0,0 +1,182 @@ +import { isMustache } from '@mpxjs/compile-utils' +import { DefineConfig } from '.' + +const TAG_NAME = 'button' + +// 微信支持的属性及其值 +const wxSupportPropsValue = { + 'open-type': ['contact', 'share', 'getPhoneNumber', 'getUserInfo', 'launchApp', 'openSetting', 'feedback'] +} + +export default function ({ print }) { + const aliValueLogError = print({ platform: 'ali', tag: TAG_NAME, isError: true, type: 'value' }) + const aliValueLog = print({ platform: 'ali', tag: TAG_NAME, isError: false, type: 'value' }) + const aliPropLog = print({ platform: 'ali', tag: TAG_NAME, isError: false }) + const aliEventLog = print({ platform: 'ali', tag: TAG_NAME, isError: false, type: 'event' }) + const baiduValueLogError = print({ platform: 'baidu', tag: TAG_NAME, isError: true, type: 'value' }) + const baiduValueLog = print({ platform: 'baidu', tag: TAG_NAME, isError: false, type: 'value' }) + const baiduPropLog = print({ platform: 'baidu', tag: TAG_NAME, isError: false }) + const baiduEventLog = print({ platform: 'baidu', tag: TAG_NAME, isError: false }) + const qqValueLogError = print({ platform: 'qq', tag: TAG_NAME, isError: true, type: 'value' }) + const qqValueLog = print({ platform: 'qq', tag: TAG_NAME, isError: false, type: 'value' }) + const qqPropLog = print({ platform: 'qq', tag: TAG_NAME, isError: false }) + const qqEventLog = print({ platform: 'qq', tag: TAG_NAME, isError: false, type: 'event' }) + const jdPropLog = print({ platform: 'jd', tag: TAG_NAME, isError: false }) + const jdEventLog = print({ platform: 'jd', tag: TAG_NAME, isError: false, type: 'event' }) + const ttPropLog = print({ platform: 'bytedance', tag: TAG_NAME, isError: false }) + const ttValueLogError = print({ platform: 'bytedance', tag: TAG_NAME, isError: true, type: 'value' }) + const ttValueLog = print({ platform: 'bytedance', tag: TAG_NAME, isError: false, type: 'value' }) + const ttEventLog = print({ platform: 'bytedance', tag: TAG_NAME, isError: false, type: 'event' }) + const webPropLog = print({ platform: 'web', tag: TAG_NAME, isError: false }) + const webEventLog = print({ platform: 'web', tag: TAG_NAME, isError: false, type: 'event' }) + const qaPropLog = print({ platform: 'qa', tag: TAG_NAME, isError: false }) + const wxPropValueLog = print({ platform: 'wx', tag: TAG_NAME, isError: false, type: 'value' }) + + return { + test: TAG_NAME, + web (_tag, { el }) { + el.isBuiltIn = true + return 'mpx-button' + }, + props: [ + { + test: 'open-type', + ali ({ name, value }) { + const notSupported = ['contact', 'launchApp', 'openSetting', 'feedback'] + if (notSupported.indexOf(value) > -1) { + aliValueLogError({ name, value }) + } else if (value === 'getPhoneNumber') { + return [ + { + name: 'open-type', + value: 'getAuthorize' + }, + { + name: 'scope', + value: 'phoneNumber' + } + ] + } else if (value === 'getUserInfo') { + return [ + { + name: 'open-type', + value: 'getAuthorize' + }, + { + name: 'scope', + value: 'userInfo' + } + ] + } else if (isMustache(value)) { + // 如果是个变量,报warning + aliValueLog({ name, value }) + } + }, + swan ({ name, value }) { + const supportList = ['contact', 'share', 'getUserInfo', 'getPhoneNumber', 'openSetting', 'chooseAddress', 'chooseInvoiceTitle', 'login'] + if (name === 'open-type' && wxSupportPropsValue[name] && wxSupportPropsValue[name].indexOf(value) === -1) { + wxPropValueLog({ name, value }) + } + if (isMustache(value)) { + // 如果是个变量,报warning + baiduValueLog({ name, value }) + } else if (value && supportList.indexOf(value) === -1) { + baiduValueLogError({ name, value }) + } + }, + qq ({ name, value }) { + const supportList = ['share', 'getUserInfo', 'getPhoneNumber', 'launchApp', 'openSetting', 'contact', 'feedback', 'openGroupProfile', 'addFriend', 'addColorSign', 'openPublicProfile', 'addGroupApp', 'shareMessageToFriend', 'addToFavorites'] + if (name === 'open-type' && wxSupportPropsValue[name] && wxSupportPropsValue[name].indexOf(value) === -1) { + wxPropValueLog({ name, value }) + } + if (isMustache(value)) { + // 如果是个变量,报warning + qqValueLog({ name, value }) + } else if (value && supportList.indexOf(value) === -1) { + qqValueLogError({ name, value }) + } + }, + tt ({ name, value }) { + if (name === 'open-type' && wxSupportPropsValue[name] && wxSupportPropsValue[name].indexOf(value) === -1) { + wxPropValueLog({ name, value }) + } + if (isMustache(value)) { + ttValueLog({ name, value }) + } else { + const supportList = ['share', 'getPhoneNumber', 'contact'] + if (value && supportList.indexOf(value) === -1) { + ttValueLogError({ name, value }) + } + } + } + }, + { + test: /^(lang|session-from|send-message-title|send-message-path|send-message-img|show-message-card)$/, + ali: aliPropLog + }, + { + test: /^(lang|session-from|send-message-title|send-message-path|send-message-img|show-message-card|app-parameter)$/, + swan: baiduPropLog + }, + { + test: /^(session-from|send-message-title|send-message-path|send-message-img|show-message-card)$/, + qq: qqPropLog + }, + { + test: /^(session-from|send-message-title|send-message-path|send-message-img|show-message-card|bindcontact|bindlaunchapp)$/, + jd: jdPropLog + }, + { + test: /^(plain|lang|session-from|send-message-title|send-message-path|send-message-img|app-parameter|show-message-card)$/, + tt: ttPropLog + }, + { + test: /^(open-type|lang|session-from|send-message-title|send-message-path|send-message-img|show-message-card|app-parameter)$/, + web: webPropLog + }, + { + test: /^(size|type|plain|loading|form-type|hover-class|hover-stop-propagation|hover-start-time|hover-stay-time|use-built-in)$/, + web (_prop, { el }) { + // todo 这部分能力基于内部封装实现 + el.isBuiltIn = true + } + }, + { + test: /^(open-type|lang|session-from|send-message-title|send-message-path|send-message-img|app-parameter|show-message-card|bindgetuserinfo|bindcontact|bindgetphonenumber|binderror|bindopensetting|bindlaunchapp)$/, + qa: qaPropLog + } + ], + event: [ + { + test: /^(getphonenumber|getuserinfo)$/, + ali () { + return 'getAuthorize' + } + }, + { + test: /^(contact|launchapp|opensetting)$/, + ali: aliEventLog + }, + { + test: /^(error|launchapp)$/, + swan: baiduEventLog + }, + { + test: /^(getphonenumber)$/, + qq: qqEventLog + }, + { + test: /^(contact|feedback)$/, + jd: jdEventLog + }, + { + test: /^(getuserinfo|contact|error|launchapp|opensetting)$/, + tt: ttEventLog + }, + { + test: /^(getuserinfo|contact|error|launchapp|opensetting|getphonenumber)$/, + web: webEventLog + } + ] + } +} diff --git a/packages/compiler/src/platform/template/wx/component-config/camera.ts b/packages/compiler/src/platform/template/wx/component-config/camera.ts new file mode 100644 index 0000000000..348a103ed2 --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/camera.ts @@ -0,0 +1,122 @@ +import { isMustache } from '@mpxjs/compile-utils' +import { DefineConfig } from '.' + +const TAG_NAME = 'camera' + +export default function ({ print }) { + const ttValueLogError = print({ + platform: 'bytedance', + tag: TAG_NAME, + isError: true, + type: 'value' + }) + const ttEventLog = print({ + platform: 'bytedance', + tag: TAG_NAME, + isError: false + }) + const ttPropLog = print({ + platform: 'bytedance', + tag: TAG_NAME, + isError: false + }) + const baiduValueLogError = print({ + platform: 'baidu', + tag: TAG_NAME, + isError: true, + type: 'value' + }) + const baiduEventLog = print({ + platform: 'baidu', + tag: TAG_NAME, + isError: false + }) + const qqValueLog = print({ + platform: 'qq', + tag: TAG_NAME, + isError: false, + type: 'value' + }) + const qqPropLog = print({ platform: 'qq', tag: TAG_NAME, isError: false }) + const qqEventLog = print({ + platform: 'qq', + tag: TAG_NAME, + isError: false, + type: 'event' + }) + const qaPropLog = print({ platform: 'qa', tag: TAG_NAME, isError: false }) + const qaEventLog = print({ + platform: 'qa', + tag: TAG_NAME, + isError: false, + type: 'event' + }) + return { + test: TAG_NAME, + props: [ + { + test: 'mode', + swan({ name, value }) { + // 百度只有相机模式,也就是微信的mode=normal + if (value !== 'normal') { + baiduValueLogError({ name, value }) + } + return false + }, + tt({ name, value }) { + if (value !== 'normal') { + ttValueLogError({ name, value }) + } + return false + }, + qa: qaPropLog + }, + { + test: 'flash', + qq({ name, value }) { + const supportList = ['auto', 'on', 'off'] + if (isMustache(value) || supportList.indexOf(value) === -1) { + // 如果是个变量,或者是不支持的属性值,报warning + qqValueLog({ name, value }) + } + }, + tt: ttPropLog + }, + { + test: /^(resolution|frame-size)$/, + qq: qqPropLog + }, + { + test: /^(frame-size|device-position)$/, + qa(prop) { + const propsMap = { + 'device-position': 'deviceposition', + 'frame-size': 'framesize' + } + prop.name = propsMap[prop.name as keyof typeof propsMap] + if (prop.name === 'framesize') { + const valueMap = { + small: 'low', + medium: 'medium', + large: 'high' + } + prop.value = valueMap[prop.value as keyof typeof valueMap] + } + return prop + } + } + ], + event: [ + { + test: /^(scancode)$/, + swan: baiduEventLog, + tt: ttEventLog, + qa: qaEventLog + }, + { + test: /^(initdone)$/, + qq: qqEventLog + } + ] + } +} diff --git a/packages/compiler/src/platform/template/wx/component-config/canvas.ts b/packages/compiler/src/platform/template/wx/component-config/canvas.ts new file mode 100644 index 0000000000..4cb009af2f --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/canvas.ts @@ -0,0 +1,60 @@ +import { DefineConfig } from "." + +const TAG_NAME = 'canvas' + +export default function ({ print }) { + const aliEventLog = print({ platform: 'ali', tag: TAG_NAME, isError: false, type: 'event' }) + const ttPropLog = print({ platform: 'bytedance', tag: TAG_NAME, isError: false }) + const ttEventLog = print({ platform: 'bytedance', tag: TAG_NAME, isError: false, type: 'event' }) + const jdPropLog = print({ platform: 'jd', tag: TAG_NAME, isError: false }) + const qaEventLog = print({ platform: 'qa', tag: TAG_NAME, isError: false, type: 'event' }) + return { + test: TAG_NAME, + props: [ + { + test: /^canvas-id$/, + ali ({ value }) { + return { + name: 'id', + value + } + } + }, + { + test: 'disable-scroll', + tt: ttPropLog + }, + { + test: 'type', + jd: jdPropLog + } + ], + // 组件事件中的差异部分 + // 微信中基础事件有touchstart|touchmove|touchcancel|touchend|tap|longpress|longtap|transitionend|animationstart|animationiteration|animationend|touchforcechange + // 支付宝中的基础事件有touchStart|touchMove|touchEnd|touchCancel|tap|longTap + event: [ + { + test: /^(touchstart|touchmove|touchend|touchcancel|longtap)$/, + ali (eventName) { + const eventMap = { + touchstart: 'touchStart', + touchmove: 'touchMove', + touchend: 'touchEnd', + touchcancel: 'touchCancel', + longtap: 'longTap' + } + return eventMap[eventName as keyof typeof eventMap] + } + }, + { + test: /^(error)$/, + ali: aliEventLog + }, + { + test: /^(longtap|error)$/, + tt: ttEventLog, + qa: qaEventLog + } + ] + } +} diff --git a/packages/compiler/src/platform/template/wx/component-config/checkbox-group.ts b/packages/compiler/src/platform/template/wx/component-config/checkbox-group.ts new file mode 100644 index 0000000000..8747cbc247 --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/checkbox-group.ts @@ -0,0 +1,13 @@ +import { DefineConfig } from '.' + +const TAG_NAME = 'checkbox-group' + +export default function () { + return { + test: TAG_NAME, + web (_tag, { el }) { + el.isBuiltIn = true + return 'mpx-checkbox-group' + } + } +} diff --git a/packages/compiler/src/platform/template/wx/component-config/checkbox.ts b/packages/compiler/src/platform/template/wx/component-config/checkbox.ts new file mode 100644 index 0000000000..a9b7d5be66 --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/checkbox.ts @@ -0,0 +1,22 @@ +import { DefineConfig } from '.' + +const TAG_NAME = 'checkbox' + +export default function () { + return { + test: TAG_NAME, + web (_tag, { el }) { + el.isBuiltIn = true + return 'mpx-checkbox' + }, + event: [ + { + test: 'tap', + ali () { + // 支付宝checkbox上不支持tap事件,change事件的表现和tap类似所以替换 + return 'change' + } + } + ] + } +} diff --git a/packages/compiler/src/platform/template/wx/component-config/component.ts b/packages/compiler/src/platform/template/wx/component-config/component.ts new file mode 100644 index 0000000000..2d40e84510 --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/component.ts @@ -0,0 +1,45 @@ +import { parseMustache } from '../../../../template-compiler/compiler' +import { normalize } from '@mpxjs/compile-utils' +import { DefineConfig, PropsTransformer } from '.' + +const TAG_NAME = 'component' + +/** is 属性格式化为中划线(-)连接 */ +const formatPropIs: PropsTransformer = (obj, data) => { + const parsed = parseMustache(obj.value) + let value = parsed.result + if (parsed.hasBinding) value = value.slice(1, -1) + const el = data.el + if (el) { + const injectWxsProp = { + injectWxsPath: '~' + normalize.lib('runtime/utils.wxs'), + injectWxsModuleName: '__wxsUtils__' + } + if (el.injectWxsProps && Array.isArray(el.injectWxsProps)) { + el.injectWxsProps.push(injectWxsProp) + } else { + el.injectWxsProps = [injectWxsProp] + } + } + return { + name: 'is', + value: `{{__wxsUtils__.humpToLine(${value})}}` + } +} + +export default function () { + return { + test: TAG_NAME, + props: [ + { + test: 'is', + ali(obj, data) { + return formatPropIs(obj, data) + }, + swan(obj, data) { + return formatPropIs(obj, data) + } + } + ] + } +} diff --git a/packages/compiler/src/platform/template/wx/component-config/cover-image.ts b/packages/compiler/src/platform/template/wx/component-config/cover-image.ts new file mode 100644 index 0000000000..9a2dcf5b3d --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/cover-image.ts @@ -0,0 +1,31 @@ +import { DefineConfig } from '.' + +const TAG_NAME = 'cover-image' + +export default function ({ print }) { + const aliEventLog = print({ platform: 'ali', tag: TAG_NAME, isError: false, type: 'event' }) + return { + test: TAG_NAME, + web (_tag, { el }) { + el.isBuiltIn = true + return 'mpx-image' + }, + tt () { + return 'image' + }, + props: [ + { + test: 'use-built-in', + web (_prop, { el }) { + el.isBuiltIn = true + } + } + ], + event: [ + { + test: /^(load|error)$/, + ali: aliEventLog + } + ] + } +} diff --git a/packages/compiler/src/platform/template/wx/component-config/cover-view.ts b/packages/compiler/src/platform/template/wx/component-config/cover-view.ts new file mode 100644 index 0000000000..8024d8cb72 --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/cover-view.ts @@ -0,0 +1,43 @@ +import { DefineConfig } from '.' + +const TAG_NAME = 'cover-view' + +export default function ({ print }) { + const aliPropLog = print({ platform: 'ali', tag: TAG_NAME, isError: false }) + const baiduValueLogError = print({ platform: 'baidu', tag: TAG_NAME, isError: true, type: 'value' }) + const webPropLog = print({ platform: 'web', tag: TAG_NAME, isError: false }) + return { + test: TAG_NAME, + web (_tag, { el }) { + if (el.hasEvent) { + el.isBuiltIn = true + } + if (el.isBuiltIn) { + return 'mpx-view' + } else { + return 'div' + } + }, + tt () { + return 'view' + }, + props: [ + { + test: 'scroll-top', + ali: aliPropLog, + swan ({ name, value }) { + if (typeof value === 'string') { + baiduValueLogError({ name, value }) + } + }, + web: webPropLog + }, + { + test: 'use-built-in', + web (_prop, { el }) { + el.isBuiltIn = true + } + } + ] + } +} diff --git a/packages/compiler/src/platform/template/wx/component-config/form.ts b/packages/compiler/src/platform/template/wx/component-config/form.ts new file mode 100644 index 0000000000..4de5899bd1 --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/form.ts @@ -0,0 +1,35 @@ +import { DefineConfig } from '.' + +const TAG_NAME = 'form' + +export default function ({ print }) { + const aliPropLog = print({ platform: 'ali', tag: TAG_NAME, isError: false }) + const baiduPropLog = print({ platform: 'baidu', tag: TAG_NAME, isError: false }) + const qqPropLog = print({ platform: 'qq', tag: TAG_NAME, isError: false }) + const jdPropLog = print({ platform: 'jd', tag: TAG_NAME, isError: false }) + const webPropLog = print({ platform: 'web', tag: TAG_NAME, isError: false }) + const qaPropLog = print({ platform: 'qa', tag: TAG_NAME, isError: false }) + + return { + test: TAG_NAME, + web (_tag, { el }) { + // form全量使用内建组件 + el.isBuiltIn = true + return 'mpx-form' + }, + props: [ + { + test: /^(report-submit-timeout)$/, + ali: aliPropLog, + swan: baiduPropLog, + jd: jdPropLog, + qq: qqPropLog + }, + { + test: /^(report-submit|report-submit-timeout)$/, + web: webPropLog, + qa: qaPropLog + } + ] + } +} diff --git a/packages/compiler/src/platform/template/wx/component-config/hypen-tag-name.ts b/packages/compiler/src/platform/template/wx/component-config/hypen-tag-name.ts new file mode 100644 index 0000000000..effaaa94d8 --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/hypen-tag-name.ts @@ -0,0 +1,15 @@ +import { capitalToHyphen } from '@mpxjs/compile-utils' +import { DefineConfig } from '.' + +export default function () { + function convertTagName (name: string) { + return capitalToHyphen(name) + } + + return { + // tag name contains capital letters + test: /[A-Z]/, + ali: convertTagName, + swan: convertTagName + } +} diff --git a/packages/compiler/src/platform/template/wx/component-config/icon.ts b/packages/compiler/src/platform/template/wx/component-config/icon.ts new file mode 100644 index 0000000000..4e9805f5fa --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/icon.ts @@ -0,0 +1,13 @@ +import { DefineConfig } from '.' + +const TAG_NAME = 'icon' + +export default function () { + return { + test: TAG_NAME, + web (_tag, { el }) { + el.isBuiltIn = true + return 'mpx-icon' + } + } +} diff --git a/packages/compiler/src/platform/template/wx/component-config/image.ts b/packages/compiler/src/platform/template/wx/component-config/image.ts new file mode 100644 index 0000000000..a7b9328e95 --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/image.ts @@ -0,0 +1,42 @@ +import { DefineConfig } from '.' + +const TAG_NAME = 'image' +export default function ({ print }) { + const aliPropLog = print({ platform: 'ali', tag: TAG_NAME, isError: false }) + const baiduPropLog = print({ platform: 'baidu', tag: TAG_NAME, isError: false }) + const qqPropLog = print({ platform: 'qq', tag: TAG_NAME, isError: false }) + const jdPropLog = print({ platform: 'jd', tag: TAG_NAME, isError: false }) + const ttPropLog = print({ platform: 'bytedance', tag: TAG_NAME, isError: false }) + const qaPropLog = print({ platform: 'qa', tag: TAG_NAME, isError: false }) + + return { + test: TAG_NAME, + web (_tag, { el }) { + el.isBuiltIn = true + return 'mpx-image' + }, + props: [ + { + test: /^show-menu-by-longpress$/, + ali: aliPropLog, + swan: baiduPropLog, + qq: qqPropLog, + tt: ttPropLog + }, + { + test: /^webp|show-menu-by-longpress$/, + jd: jdPropLog + }, + { + test: /^(mode|lazy-load|show-menu-by-longpress|webp|use-built-in)$/, + web (_prop, { el }) { + el.isBuiltIn = true + } + }, + { + test: /^(show-menu-by-longpress|webp)$/, + qa: qaPropLog + } + ] + } +} diff --git a/packages/compiler/src/platform/template/wx/component-config/index.ts b/packages/compiler/src/platform/template/wx/component-config/index.ts new file mode 100644 index 0000000000..8c9c6cf89d --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/index.ts @@ -0,0 +1,172 @@ +import ad from './ad' +import block from './block' +import button from './button' +import camera from './camera' +import canvas from './canvas' +import checkboxGroup from './checkbox-group' +import checkbox from './checkbox' +import coverImage from './cover-image' +import coverView from './cover-view' +import form from './form' +import HyphenTagName from './hypen-tag-name' +import icon from './icon' +import image from './image' +import input from './input' +import livePlayer from './live-player' +import livePusher from './live-pusher' +import map from './map' +import movableArea from './movable-area' +import movableView from './movable-view' +import navigator from './navigator' +import pickerViewColumn from './picker-view-column' +import pickerView from './picker-view' +import picker from './picker' +import progress from './progress' +import radioGroup from './radio-group' +import radio from './radio' +import richText from './rich-text' +import scrollView from './scroll-view' +import slider from './slider' +import swiperItem from './swiper-item' +import swiper from './swiper' +import switchComponent from './switch' +import template from './template' +import text from './text' +import textarea from './textarea' +import Nonsupport from './unsupported' +import video from './video' +import view from './view' +import webView from './web-view' +import wxs from './wxs' +import component from './component' + +interface Attr { + name: string + value: any +} + +type Test = string | RegExp + +export type PropsTransformer = (attr: Attr, params: { el: any }) => any +export type EventTransformer = (event: string, params: { el: any }) => any +export type TagTransformer = (tag: string, params: { el: any }) => any +export type PrintLog = (tag?: Attr | string) => void +export interface Config { + test?: Test + supportedModes?: string[] + [key: string]: + | Config['props'] + | Config['event'] + | Config['test'] + | TagTransformer + | string[] + props?: { + [key: string]: PropsTransformer | string | undefined | RegExp + test?: Test + }[] + event?: { + [key: string]: EventTransformer | string | undefined | RegExp + test?: Test + }[] +} + +export type Print = (params: { + platform: any + tag?: any + type?: string | undefined + isError?: boolean | undefined +}) => PrintLog + +export type DefineConfig = (params: { print: Print }) => Config +export type DefineConfigs = (params: { print: Print }) => Config[] + +export default function getComponentConfigs({ + warn, + error +}: { + warn: any + error: any +}) { + const print: Print = + ({ platform, tag, type = 'property', isError = false }) => + arg => { + if (type === 'tag') { + error(`<${arg}> is not supported in ${platform} environment!`) + return + } + let msg + switch (type) { + case 'event': + msg = `<${tag}> does not support [bind${arg}] event in ${platform} environment!` + break + case 'property': + msg = `<${tag}> does not support [${ + arg && (arg as Attr).name + }] property in ${platform} environment!` + break + case 'value': + msg = `<${tag}>'s property '${ + arg && (arg as Attr).name + }' does not support '[${ + arg && (arg as Attr).value + }]' value in ${platform} environment!` + break + case 'tagRequiredProps': + msg = `<${tag}> should have '${arg}' attr in ali environment!` + break + case 'value-attr-uniform': + msg = `The internal attribute name of the <${tag}>'s attribute '${ + arg && (arg as Attr).value + }' is not supported in the ali environment, Please check!` + break + default: + msg = `<${tag}>'s transform has some error happened!` + } + isError ? error(msg) : warn(msg) + } + + // 转换规则只需以微信为基准配置微信和支付宝的差异部分,比如微信和支付宝都支持但是写法不一致,或者微信支持而支付宝不支持的部分(抛出错误或警告) + return [ + ...Nonsupport({ print }), + ad({ print }), + view({ print }), + scrollView({ print }), + swiper({ print }), + swiperItem({ print }), + movableView({ print }), + movableArea({ print }), + coverView({ print }), + coverImage({ print }), + text({ print }), + richText({ print }), + progress({ print }), + button({ print }), + checkboxGroup({ print }), + checkbox({ print }), + radioGroup({ print }), + radio({ print }), + form({ print }), + input({ print }), + picker({ print }), + pickerView({ print }), + pickerViewColumn({ print }), + slider({ print }), + switchComponent({ print }), + textarea({ print }), + navigator({ print }), + image({ print }), + map({ print }), + canvas({ print }), + wxs({ print }), + template({ print }), + block({ print }), + icon({ print }), + webView({ print }), + video({ print }), + camera({ print }), + livePlayer({ print }), + livePusher({ print }), + HyphenTagName({ print }), + component({ print }) + ] +} diff --git a/packages/compiler/src/platform/template/wx/component-config/input.ts b/packages/compiler/src/platform/template/wx/component-config/input.ts new file mode 100644 index 0000000000..f8dbc079e0 --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/input.ts @@ -0,0 +1,86 @@ +import { DefineConfig } from '.' + +const TAG_NAME = 'input' + +export default function ({ print }) { + const aliPropLog = print({ platform: 'ali', tag: TAG_NAME, isError: false }) + const aliEventLog = print({ platform: 'ali', tag: TAG_NAME, isError: false, type: 'event' }) + const baiduPropLog = print({ platform: 'baidu', tag: TAG_NAME, isError: false }) + const baiduEventLog = print({ platform: 'baidu', tag: TAG_NAME, isError: false, type: 'event' }) + const ttPropLog = print({ platform: 'bytedance', tag: TAG_NAME, isError: false }) + const ttEventLog = print({ platform: 'bytedance', tag: TAG_NAME, isError: false, type: 'event' }) + const jdPropLog = print({ platform: 'jd', tag: TAG_NAME, isError: false }) + const jdEventLog = print({ platform: 'jd', tag: TAG_NAME, isError: false, type: 'event' }) + const webPropLog = print({ platform: 'web', tag: TAG_NAME, isError: false }) + const webEventLog = print({ platform: 'web', tag: TAG_NAME, isError: false, type: 'event' }) + const webValueLog = print({ platform: 'web', tag: TAG_NAME, isError: false, type: 'value' }) + const qaPropLog = print({ platform: 'qa', tag: TAG_NAME, isError: false }) + + return { + test: TAG_NAME, + web (_tag, { el }) { + el.isBuiltIn = true + return 'mpx-input' + }, + props: [ + { + test: /^(cursor-spacing|auto-focus|adjust-position|hold-keyboard)$/, + ali: aliPropLog + }, + { + test: /^(auto-focus|hold-keyboard)$/, + swan: baiduPropLog + }, + { + test: /^(placeholder-class|auto-focus|confirm-type|confirm-hold|adjust-position|hold-keyboard)$/, + tt: ttPropLog + }, + { + test: /^(hold-keyboard)$/, + jd: jdPropLog + }, + { + test: 'type', + web (prop) { + let { value } = prop + if (value === 'idcard' || value === 'digit') { + webValueLog(prop) + value = 'text' + } + return { + name: prop.name, + value + } + } + }, + { + test: /^(password|auto-focus|focus|cursor|selection-start|selection-end|use-built-in)$/, + web (_prop, { el }) { + el.isBuiltIn = true + } + }, + { + test: /^(placeholder-style|placeholder-class|cursor-spacing|confirm-type|confirm-hold|adjust-position|hold-keyboard)$/, + web: webPropLog + }, + { + test: /^(always-embed|bindkeyboardheightchange)$/, + qa: qaPropLog + } + ], + event: [ + { + test: 'keyboardheightchange', + ali: aliEventLog, + swan: baiduEventLog, + tt: ttEventLog, + web: webEventLog, + jd: jdEventLog + }, + { + test: 'confirm', + web: webEventLog + } + ] + } +} diff --git a/packages/compiler/src/platform/template/wx/component-config/live-player.ts b/packages/compiler/src/platform/template/wx/component-config/live-player.ts new file mode 100644 index 0000000000..b98f45f749 --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/live-player.ts @@ -0,0 +1,51 @@ +import { DefineConfig } from "." + +const TAG_NAME = 'live-player' + +export default function ({ print }) { + const baiduValueLogError = print({ platform: 'baidu', tag: TAG_NAME, isError: true, type: 'value' }) + const baiduPropLog = print({ platform: 'baidu', tag: TAG_NAME, isError: false }) + const baiduEventLog = print({ platform: 'baidu', tag: TAG_NAME, isError: false, type: 'event' }) + const qqPropLog = print({ platform: 'qq', tag: TAG_NAME, isError: false }) + const qqEventLog = print({ platform: 'qq', tag: TAG_NAME, isError: false, type: 'event' }) + const ttEventLog = print({ platform: 'bytedance', tag: TAG_NAME, isError: false, type: 'event' }) + const ttPropLog = print({ platform: 'bytedance', tag: TAG_NAME, isError: false }) + return { + test: TAG_NAME, + props: [ + { + test: 'mode', + swan ({ name, value }) { + // 百度只有直播模式,也就是微信的mode=live + if (value !== 'live') { + baiduValueLogError({ name, value }) + } + return false + } + }, + { + test: /^(sound-mode|auto-pause-if-navigate|auto-pause-if-open-native)$/, + swan: baiduPropLog + }, + { + test: /^(background-mute|picture-in-picture-mode)$/, + qq: qqPropLog + }, + { + test: /^(mode|background-mute|min-cache|max-cache|sound-mode|auto-pause-if-navigate|auto-pause-if-open-native)$/, + tt: ttPropLog + } + ], + event: [ + { + test: /^(audiovolumenotify|enterpictureinpicture|leavepictureinpicture)$/, + qq: qqEventLog, + swan: baiduEventLog + }, + { + test: /^(netstatus|audiovolumenotify|enterpictureinpicture|leavepictureinpicture)$/, + tt: ttEventLog + } + ] + } +} diff --git a/packages/compiler/src/platform/template/wx/component-config/live-pusher.ts b/packages/compiler/src/platform/template/wx/component-config/live-pusher.ts new file mode 100644 index 0000000000..8f37d98289 --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/live-pusher.ts @@ -0,0 +1,23 @@ +import { DefineConfig } from "." + +const TAG_NAME = 'live-pusher' + +export default function ({ print }) { + const qqPropLog = print({ platform: 'qq', tag: TAG_NAME, isError: false }) + const qqEventLog = print({ platform: 'qq', tag: TAG_NAME, isError: false, type: 'event' }) + return { + test: TAG_NAME, + props: [ + { + test: /^(remote-mirror|local-mirror|audio-reverb-type|enable-mic|enable-agc|enable-ans|audio-volume-type|video-width|video-height|beauty-style|filter)$/, + qq: qqPropLog + } + ], + event: [ + { + test: /^(audiovolumenotify)$/, + qq: qqEventLog + } + ] + } +} diff --git a/packages/compiler/src/platform/template/wx/component-config/map.ts b/packages/compiler/src/platform/template/wx/component-config/map.ts new file mode 100644 index 0000000000..1a81076398 --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/map.ts @@ -0,0 +1,101 @@ +import { DefineConfig } from '.' + +const TAG_NAME = 'map' + +export default function ({ print }) { + const aliPropLog = print({ platform: 'ali', tag: TAG_NAME, isError: false }) + const aliEventLogError = print({ platform: 'ali', tag: TAG_NAME, isError: true, type: 'event' }) + const aliPropValueWarningLog = print({ platform: 'ali', tag: TAG_NAME, isError: false, type: 'value-attr-uniform' }) + const baiduPropLog = print({ platform: 'baidu', tag: TAG_NAME, isError: false }) + const baiduEventLogError = print({ platform: 'baidu', tag: TAG_NAME, isError: true, type: 'event' }) + const jdPropLog = print({ platform: 'jd', tag: TAG_NAME, isError: false }) + const jdEventLogError = print({ platform: 'jd', tag: TAG_NAME, isError: true, type: 'event' }) + const ttEventLog = print({ platform: 'bytedance', tag: TAG_NAME, isError: false, type: 'event' }) + const ttPropLog = print({ platform: 'bytedance', tag: TAG_NAME, isError: false }) + const qaPropLog = print({ platform: 'qa', tag: TAG_NAME, isError: false }) + const qaEventLogError = print({ platform: 'qa', tag: TAG_NAME, isError: true, type: 'event' }) + return { + // 匹配标签名,可传递正则 + test: TAG_NAME, + // 组件属性中的差异部分 + props: [ + { + test: /^(min-scale|max-scale|covers|subkey|layer-style|rotate|skew|enable-3D|show-compass|show-scale|enable-overlooking|enable-zoom|enable-scroll|enable-rotate|enable-satellite|enable-traffic|enable-poi|enable-building)$/, + ali: aliPropLog + }, + { + test: /^polygons$/, + ali ({ name, value }) { + name = 'polygon' + // TODO 标签的属性名不一致,后续考虑通过wxs注入的方式实现转换 + aliPropValueWarningLog() + return { name, value } + } + }, + { + test: 'subkey', + swan: baiduPropLog + }, + { + test: /^(covers|polygons|subkey|layer-style|rotate|skew|enable-3D|show-compass|show-scale|enable-overlooking|enable-zoom|enable-scroll|enable-rotate|enable-satellite|enable-traffic|setting)$/, + jd: jdPropLog + }, + { + test: /^(include-points|show-location)$/, + jd ({ name }) { + const propsMap = { + 'include-points': 'includePoints', + 'show-location': 'showLocation' + } + return propsMap[name as keyof typeof propsMap] + } + }, + { + test: /^(min-scale|max-scale|polyline|controls|polygons|subkey|layer-style|rotate|skew|enable-3D|show-compass|show-scale|enable-overlooking|enable-zoom|enable-scroll|enable-rotate|enable-satellite|enable-traffic|enable-poi|enable-building|setting)$/, + tt: ttPropLog + }, + { + test: /^(min-scale|max-scale|covers|polyline|include-points|show-location|subkey|layer-style|skew|enable-3D|show-compass|show-scale|enable-overlooking|enable-zoom|enable-scroll|enable-rotate|enable-satellite|enable-traffic|enable-poi|enable-building|setting)$/, + qa: qaPropLog + } + ], + // 组件事件中的差异部分 + // 微信中基础事件有touchstart|touchmove|touchcancel|touchend|tap|longpress|longtap|transitionend|animationstart|animationiteration|animationend|touchforcechange + // 支付宝中的基础事件有touchStart|touchMove|touchEnd|touchCancel|tap|longTap + event: [ + { + test: /^(tap|markertap|callouttap|controltap|regionchange|)$/, + ali (eventName) { + const eventMap = { + tap: 'tap', + markertap: 'markerTap', + callouttap: 'calloutTap', + controltap: 'controlTap', + regionchange: 'regionChange' + } + return eventMap[eventName as keyof typeof eventMap] + } + }, + { + test: /^(updated|poitap|anchorpointtap)$/, + ali: aliEventLogError + }, + { + test: 'poitap', + swan: baiduEventLogError + }, + { + test: /^(labeltap|updated|poitap)$/, + jd: jdEventLogError + }, + { + test: /^(labeltap|controltap|updated|regionchange|poitap|anchorpointtap)$/, + tt: ttEventLog + }, + { + test: /^(labeltap|anchorpointtap)$/, + qa: qaEventLogError + } + ] + } +} diff --git a/packages/compiler/src/platform/template/wx/component-config/movable-area.ts b/packages/compiler/src/platform/template/wx/component-config/movable-area.ts new file mode 100644 index 0000000000..6c77eab7cf --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/movable-area.ts @@ -0,0 +1,13 @@ +import { DefineConfig } from '.' + +const TAG_NAME = 'movable-area' + +export default function () { + return { + test: TAG_NAME, + web (_tag, { el }) { + el.isBuiltIn = true + return 'mpx-movable-area' + } + } +} diff --git a/packages/compiler/src/platform/template/wx/component-config/movable-view.ts b/packages/compiler/src/platform/template/wx/component-config/movable-view.ts new file mode 100644 index 0000000000..a467bca16e --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/movable-view.ts @@ -0,0 +1,27 @@ +import { DefineConfig } from '.' + +const TAG_NAME = 'movable-view' + +export default function ({ print }) { + const aliEventLog = print({ platform: 'ali', tag: TAG_NAME, isError: false, type: 'event' }) + const qaPropLog = print({ platform: 'qa', tag: TAG_NAME, isError: false }) + return { + test: TAG_NAME, + web (_tag, { el }) { + el.isBuiltIn = true + return 'mpx-movable-view' + }, + props: [ + { + test: /^(out-of-bounds)$/, + ali: qaPropLog + } + ], + event: [ + { + test: /^(htouchmove|vtouchmove)$/, + ali: aliEventLog + } + ] + } +} diff --git a/packages/compiler/src/platform/template/wx/component-config/navigator.ts b/packages/compiler/src/platform/template/wx/component-config/navigator.ts new file mode 100644 index 0000000000..a71807b6bc --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/navigator.ts @@ -0,0 +1,204 @@ +import { isMustache } from '@mpxjs/compile-utils' +import { DefineConfig } from '.' + +const TAG_NAME = 'navigator' + +// 微信支持的属性及其值 +const wxSupportPropsValue: { + [key: string]: any +} = { + 'open-type': [ + 'navigate', + 'redirect', + 'switchTab', + 'reLaunch', + 'navigateBack', + 'exit' + ] +} +export default function ({ print }) { + const aliValueLogError = print({ + platform: 'ali', + tag: TAG_NAME, + isError: true, + type: 'value' + }) + const aliPropLog = print({ platform: 'ali', tag: TAG_NAME, isError: false }) + const aliPropLogError = print({ + platform: 'ali', + tag: TAG_NAME, + isError: true + }) + const aliEventLog = print({ + platform: 'ali', + tag: TAG_NAME, + isError: false, + type: 'event' + }) + const ttValueLogError = print({ + platform: 'bytedance', + tag: TAG_NAME, + isError: true, + type: 'value' + }) + const ttPropLog = print({ + platform: 'bytedance', + tag: TAG_NAME, + isError: false + }) + const ttEventLog = print({ + platform: 'bytedance', + tag: TAG_NAME, + isError: false, + type: 'event' + }) + const webPropLog = print({ platform: 'web', tag: TAG_NAME, isError: false }) + const webEventLog = print({ platform: 'web', tag: TAG_NAME, isError: false }) + const webValueLogError = print({ + platform: 'web', + tag: TAG_NAME, + isError: true, + type: 'value' + }) + const wxPropValueLog = print({ + platform: 'wx', + tag: TAG_NAME, + isError: false, + type: 'value' + }) + const qaEventLog = print({ + platform: 'qa', + tag: TAG_NAME, + isError: false, + type: 'event' + }) + const qaPropLog = print({ platform: 'qa', tag: TAG_NAME, isError: false }) + const qaValueLogError = print({ + platform: 'qa', + tag: TAG_NAME, + isError: true, + type: 'value' + }) + return { + test: TAG_NAME, + web (_tag, { el }) { + el.isBuiltIn = true + return 'mpx-navigator' + }, + props: [ + { + test: /^(target|delta|app-id|path|extra-data|version|hover-stop-propagation)$/, + ali: aliPropLogError, + qa: qaPropLog + }, + { + test: 'open-type', + ali(attr) { + if ( + wxSupportPropsValue[attr.name] && + wxSupportPropsValue[attr.name].indexOf(attr.value) === -1 + ) { + wxPropValueLog({ name: attr.name, value: attr.value }) + } + if (isMustache(attr.value)) { + // 如果是个变量,报warning~ + aliPropLog(attr) + } else { + const supportedList = [ + 'navigate', + 'redirect', + 'switchTab', + 'navigateBack', + 'reLaunch', + 'exit' + ] + if (supportedList.indexOf(attr.value) === -1) { + aliValueLogError(attr) + } + } + }, + tt(attr) { + if ( + wxSupportPropsValue[attr.name] && + wxSupportPropsValue[attr.name].indexOf(attr.value) === -1 + ) { + wxPropValueLog({ name: attr.name, value: attr.value }) + } + if (isMustache(attr.value)) { + // 如果是个变量,报warning~ + ttPropLog(attr) + } else { + const supportedList = [ + 'navigate', + 'redirect', + 'switchTab', + 'navigateBack', + 'reLaunch' + ] + if (supportedList.indexOf(attr.value) === -1) { + ttValueLogError(attr) + } + } + }, + web(attr) { + const supportedList = [ + 'navigate', + 'redirect', + 'navigateBack', + 'reLaunch' + ] + if (supportedList.indexOf(attr.value) === -1) { + webValueLogError(attr) + } + }, + qa(attr) { + if ( + wxSupportPropsValue[attr.name] && + wxSupportPropsValue[attr.name].indexOf(attr.value) === -1 + ) { + wxPropValueLog({ name: attr.name, value: attr.value }) + } + if (isMustache(attr.value)) { + qaPropLog(attr) + } else { + const supportedList = [ + 'navigate', + 'redirect', + 'switchTab', + 'navigateBack', + 'reLaunch' + ] + if (supportedList.indexOf(attr.value) === -1) { + qaValueLogError(attr) + } + } + } + }, + { + test: /^(target|app-id|path|extra-data|version)$/, + tt: ttPropLog + }, + { + test: /^(target|app-id|path|extra-data|version)$/, + web: webPropLog + } + ], + event: [ + { + test: /^(success|fail|complete)$/, + ali: aliEventLog, + tt: ttEventLog, + web: webEventLog, + qa: qaEventLog, + jd(eventName) { + const eventMap = { + success: 'success', + fail: 'error', + complete: 'complete' + } + return eventMap[eventName as keyof typeof eventMap] + } + } + ] + } +} diff --git a/packages/compiler/src/platform/template/wx/component-config/picker-view-column.ts b/packages/compiler/src/platform/template/wx/component-config/picker-view-column.ts new file mode 100644 index 0000000000..8db2f2b7bf --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/picker-view-column.ts @@ -0,0 +1,13 @@ +import { DefineConfig } from '.' + +const TAG_NAME = 'picker-view-column' + +export default function () { + return { + test: TAG_NAME, + web (_tag, { el }) { + el.isBuiltIn = true + return 'mpx-picker-view-column' + } + } +} diff --git a/packages/compiler/src/platform/template/wx/component-config/picker-view.ts b/packages/compiler/src/platform/template/wx/component-config/picker-view.ts new file mode 100644 index 0000000000..265761e3aa --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/picker-view.ts @@ -0,0 +1,34 @@ +import { DefineConfig } from "." + +const TAG_NAME = 'picker-view' + +export default function ({ print }) { + const aliEventLog = print({ platform: 'ali', tag: TAG_NAME, isError: false, type: 'event' }) + const baiduEventLog = print({ platform: 'baidu', tag: TAG_NAME, isError: false }) + const ttPropLog = print({ platform: 'bytedance', tag: TAG_NAME, isError: false }) + const ttEventLog = print({ platform: 'bytedance', tag: TAG_NAME, isError: false, type: 'event' }) + const jdEventLog = print({ platform: 'jd', tag: TAG_NAME, isError: false, type: 'event' }) + + return { + test: TAG_NAME, + web (_tag, { el }) { + el.isBuiltIn = true + return 'mpx-picker-view' + }, + props: [ + { + test: /^(indicator-class|mask-class)$/, + tt: ttPropLog + } + ], + event: [ + { + test: /^(pickstart|pickend)$/, + ali: aliEventLog, + swan: baiduEventLog, + tt: ttEventLog, + jd: jdEventLog + } + ] + } +} diff --git a/packages/compiler/src/platform/template/wx/component-config/picker.ts b/packages/compiler/src/platform/template/wx/component-config/picker.ts new file mode 100644 index 0000000000..655bb2fc5a --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/picker.ts @@ -0,0 +1,61 @@ +import { DefineConfig } from '.' +const TAG_NAME = 'picker' + +export default function ({ print }) { + const aliPropLogError = print({ + platform: 'ali', + tag: TAG_NAME, + isError: true + }) + const aliPropLog = print({ platform: 'ali', tag: TAG_NAME, isError: false }) + const aliEventLog = print({ + platform: 'ali', + tag: TAG_NAME, + isError: false, + type: 'event' + }) + const jdPropLog = print({ platform: 'jd', tag: TAG_NAME, isError: true }) + const ttPropLog = print({ + platform: 'bytedance', + tag: TAG_NAME, + isError: false + }) + const baiduPropLog = print({ + platform: 'baidu', + tag: TAG_NAME, + isError: false + }) + const qaPropLog = print({ platform: 'qa', tag: TAG_NAME, isError: false }) + return { + test: TAG_NAME, + web (_tag, { el }) { + el.isBuiltIn = true + return 'mpx-picker' + }, + props: [ + { + test: 'mode', + ali (attr) { + if (attr.value !== 'selector') { + aliPropLogError(attr) + } + return false + } + }, + { + test: /^(header-text)$/, + tt: ttPropLog, + swan: baiduPropLog, + ali: aliPropLog, + jd: jdPropLog, + qa: qaPropLog + } + ], + event: [ + { + test: /^(cancel)$/, + ali: aliEventLog + } + ] + } +} diff --git a/packages/compiler/src/platform/template/wx/component-config/progress.ts b/packages/compiler/src/platform/template/wx/component-config/progress.ts new file mode 100644 index 0000000000..800eefa073 --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/progress.ts @@ -0,0 +1,73 @@ +import { DefineConfig } from '.' + +const TAG_NAME = 'progress' + +export default function ({ print }) { + const aliPropLog = print({ platform: 'ali', tag: TAG_NAME, isError: false }) + const aliEventLog = print({ platform: 'ali', tag: TAG_NAME, isError: false, type: 'event' }) + const baiduPropLog = print({ platform: 'baidu', tag: TAG_NAME, isError: false }) + const baiduEventLog = print({ platform: 'baidu', tag: TAG_NAME, isError: false, type: 'event' }) + const ttPropLog = print({ platform: 'bytedance', tag: TAG_NAME, isError: false }) + const ttEventLog = print({ platform: 'bytedance', tag: TAG_NAME, isError: false, type: 'event' }) + const jdPropLog = print({ platform: 'jd', tag: TAG_NAME, isError: false }) + const jdEventLog = print({ platform: 'jd', tag: TAG_NAME, isError: false, type: 'event' }) + const qqPropLog = print({ platform: 'qq', tag: TAG_NAME, isError: false }) + + return { + test: TAG_NAME, + web (_tag, { el }) { + el.isBuiltIn = true + return 'mpx-progress' + }, + props: [ + { + test: /^(border-radius|font-size|color|active-mode|duration)$/, + ali: aliPropLog + }, + { + test: /^(border-radius|font-size)$/, + swan: baiduPropLog + }, + { + test: /^(activeColor|backgroundColor)$/, + ali (obj) { + const propsMap = { + activeColor: 'active-color', + backgroundColor: 'background-color' + } + obj.name = propsMap[obj.name as keyof typeof propsMap] + return obj + }, + tt (obj) { + const propsMap = { + activeColor: 'active-color', + backgroundColor: 'background-color' + } + obj.name = propsMap[obj.name as keyof typeof propsMap] + return obj + } + }, + { + test: /^(show-info|border-radius|font-size|duration)$/, + tt: ttPropLog + }, + { + test: /^(border-radius|font-size|duration|bindactiveend)$/, + jd: jdPropLog + }, + { + test: /^(duration)$/, + qq: qqPropLog + } + ], + event: [ + { + test: /^(activeend)$/, + ali: aliEventLog, + swan: baiduEventLog, + tt: ttEventLog, + jd: jdEventLog + } + ] + } +} diff --git a/packages/compiler/src/platform/template/wx/component-config/radio-group.ts b/packages/compiler/src/platform/template/wx/component-config/radio-group.ts new file mode 100644 index 0000000000..e66371b39f --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/radio-group.ts @@ -0,0 +1,13 @@ +import { DefineConfig } from '.' + +const TAG_NAME = 'radio-group' + +export default function () { + return { + test: TAG_NAME, + web (_tag, { el }) { + el.isBuiltIn = true + return 'mpx-radio-group' + } + } +} diff --git a/packages/compiler/src/platform/template/wx/component-config/radio.ts b/packages/compiler/src/platform/template/wx/component-config/radio.ts new file mode 100644 index 0000000000..e53bb79dc3 --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/radio.ts @@ -0,0 +1,22 @@ +import { DefineConfig } from '.' + +const TAG_NAME = 'radio' + +export default function () { + return { + test: TAG_NAME, + web (_tag, { el }) { + el.isBuiltIn = true + return 'mpx-radio' + }, + event: [ + { + test: 'tap', + ali () { + // 支付宝radio上不支持tap事件,change事件的表现和tap类似所以替换 + return 'change' + } + } + ] + } +} diff --git a/packages/compiler/src/platform/template/wx/component-config/rich-text.ts b/packages/compiler/src/platform/template/wx/component-config/rich-text.ts new file mode 100644 index 0000000000..f3cf352ce4 --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/rich-text.ts @@ -0,0 +1,31 @@ +import { DefineConfig } from '.' + +const TAG_NAME = 'rich-text' + +export default function ({ print }) { + const aliPropLog = print({ platform: 'ali', tag: TAG_NAME, isError: false }) + const baiduPropLog = print({ platform: 'baidu', tag: TAG_NAME, isError: false }) + const ttPropLog = print({ platform: 'bytedance', tag: TAG_NAME, isError: false }) + const jdPropLog = print({ platform: 'jd', tag: TAG_NAME, isError: false }) + + return { + test: TAG_NAME, + web (_tag, { el }) { + el.isBuiltIn = true + return 'mpx-rich-text' + }, + props: [ + { + test: /^(space)$/, + ali: aliPropLog, + swan: baiduPropLog, + tt: ttPropLog, + jd: jdPropLog + }, + { + test: /^(nodes)$/, + jd: jdPropLog + } + ] + } +} diff --git a/packages/compiler/src/platform/template/wx/component-config/scroll-view.ts b/packages/compiler/src/platform/template/wx/component-config/scroll-view.ts new file mode 100644 index 0000000000..eabf725722 --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/scroll-view.ts @@ -0,0 +1,71 @@ +import { DefineConfig } from '.' + +const TAG_NAME = 'scroll-view' + +export default function ({ print }) { + const baiduPropLog = print({ platform: 'baidu', tag: TAG_NAME, isError: false }) + const baiduEventLog = print({ platform: 'baidu', tag: TAG_NAME, isError: false, type: 'event' }) + const ttPropLog = print({ platform: 'bytedance', tag: TAG_NAME, isError: false }) + const jdPropLog = print({ platform: 'jd', tag: TAG_NAME, isError: false }) + const jdEventLog = print({ platform: 'jd', tag: TAG_NAME, isError: false, type: 'event' }) + const qaPropLog = print({ platform: 'qa', tag: TAG_NAME, isError: false }) + const ttEventLog = print({ platform: 'bytedance', tag: TAG_NAME, isError: false, type: 'event' }) + const aliPropLog = print({ platform: 'ali', tag: TAG_NAME, isError: false }) + const aliEventLog = print({ platform: 'ali', tag: TAG_NAME, isError: false, type: 'event' }) + const qqEventLog = print({ platform: 'qq', tag: TAG_NAME, isError: false, type: 'event' }) + const qqPropLog = print({ platform: 'qq', tag: TAG_NAME, isError: false }) + + return { + test: TAG_NAME, + web (_tag, { el }) { + el.isBuiltIn = true + return 'mpx-scroll-view' + }, + props: [ + { + test: /^(enable-flex|scroll-anchorin|refresher-enabled|refresher-threshold|refresher-default-style|refresher-background|refresher-triggered|enhanced|bounces|show-scrollbar|paging-enabled|fast-deceleratio)$/, + ali: aliPropLog, + tt: ttPropLog, + qq: qqPropLog, + swan: baiduPropLog + }, + { + test: /^(enable-back-to-top)$/, + swan: baiduPropLog, + tt: ttPropLog + }, + { + test: /^(enable-flex|scroll-anchoring|refresher-enabled|refresher-threshold|refresher-default-style|refresher-background|refresher-triggered)$/, + jd: jdPropLog + }, + { + test: /^(enable-back-to-top|enable-flex|scroll-anchoring|enhanced|bounces|show-scrollbar|paging-enabled|fast-deceleration|binddragstart|binddragging|binddragend)$/, + qa: qaPropLog + } + ], + event: [ + { + test: /^(scrolltoupper|scrolltolower|scroll)$/, + ali (eventName) { + const eventMap = { + scrolltoupper: 'scrollToUpper', + scrolltolower: 'scrollToLower', + scroll: 'scroll' + } + return eventMap[eventName as keyof typeof eventMap] + } + }, + { + test: /^(refresherpulling|refresherrefresh|refresherrestore|refresherabort)$/, + jd: jdEventLog + }, + { + test: /^(dragstart|dragging|dragend|refresherpulling|refresherrefresh|refresherrestore|refresherabort)$/, + ali: aliEventLog, + tt: ttEventLog, + qq: qqEventLog, + swan: baiduEventLog + } + ] + } +} diff --git a/packages/compiler/src/platform/template/wx/component-config/slider.ts b/packages/compiler/src/platform/template/wx/component-config/slider.ts new file mode 100644 index 0000000000..d060444251 --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/slider.ts @@ -0,0 +1,57 @@ +import { DefineConfig } from '.' + +const TAG_NAME = 'slider' + +export default function ({ print }) { + const aliPropLog = print({ platform: 'ali', tag: TAG_NAME, isError: false }) + return { + test: TAG_NAME, + web (_tag, { el }) { + el.isBuiltIn = true + return 'mpx-slider' + }, + props: [ + { + test: /^color$/, + ali: aliPropLog + }, + { + test: /^(color|selected-color|activeColor|backgroundColor|block-size|block-color)$/, + ali (obj) { + const propsMap = { + color: 'background-color', + 'selected-color': 'active-color', + activeColor: 'active-color', + backgroundColor: 'background-color', + 'block-size': 'handle-size', + 'block-color': 'handle-color' + } + obj.name = propsMap[obj.name as keyof typeof propsMap] + return obj + } + }, + { + test: /^(color|selected-color)$/, + swan (obj) { + const propsMap = { + color: 'backgroundColor', + 'selected-color': 'activeColor' + } + obj.name = propsMap[obj.name as keyof typeof propsMap] + return obj + } + }, + { + test: /^(activeColor|backgroundColor)$/, + tt (obj) { + const propsMap = { + activeColor: 'active-color', + backgroundColor: 'background-color' + } + obj.name = propsMap[obj.name as keyof typeof propsMap] + return obj + } + } + ] + } +} diff --git a/packages/compiler/src/platform/template/wx/component-config/swiper-item.ts b/packages/compiler/src/platform/template/wx/component-config/swiper-item.ts new file mode 100644 index 0000000000..d15049a2e0 --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/swiper-item.ts @@ -0,0 +1,33 @@ +import { DefineConfig } from '.' + +const TAG_NAME = 'swiper-item' + +export default function ({ print }) { + const aliPropLog = print({ platform: 'ali', tag: TAG_NAME, isError: false }) + const qaPropLog = print({ platform: 'qa', tag: TAG_NAME, isError: false }) + const ttPropLog = print({ platform: 'bytedance', tag: TAG_NAME, isError: false }) + const baiduPropLog = print({ platform: 'baidu', tag: TAG_NAME, isError: false }) + const qqPropLog = print({ platform: 'qq', tag: TAG_NAME, isError: false }) + + return { + test: TAG_NAME, + web (_tag, { el }) { + el.isBuiltIn = true + return 'mpx-swiper-item' + }, + props: [ + { + test: /^(item-id)$/, + ali: aliPropLog + }, + { + test: /^(skip-hidden-item-layout)$/, + qa: qaPropLog, + ali: aliPropLog, + tt: ttPropLog, + swan: baiduPropLog, + qq: qqPropLog + } + ] + } +} diff --git a/packages/compiler/src/platform/template/wx/component-config/swiper.ts b/packages/compiler/src/platform/template/wx/component-config/swiper.ts new file mode 100644 index 0000000000..fc44e5401f --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/swiper.ts @@ -0,0 +1,70 @@ +import { DefineConfig } from '.' + +const TAG_NAME = 'swiper' + +export default function ({ print }) { + const aliPropLog = print({ platform: 'ali', tag: TAG_NAME, isError: false }) + const baiduPropLog = print({ platform: 'baidu', tag: TAG_NAME, isError: false }) + const baiduEventLog = print({ platform: 'baidu', tag: TAG_NAME, isError: false, type: 'event' }) + const qqPropLog = print({ platform: 'qq', tag: TAG_NAME, isError: false }) + const ttPropLog = print({ platform: 'bytedance', tag: TAG_NAME, isError: false }) + const webPropLog = print({ platform: 'web', tag: TAG_NAME, isError: false }) + const jdEventLog = print({ platform: 'jd', tag: TAG_NAME, isError: false, type: 'event' }) + const jdPropLog = print({ platform: 'jd', tag: TAG_NAME, isError: false }) + const qaPropLog = print({ platform: 'qa', tag: TAG_NAME, isError: false }) + + return { + test: TAG_NAME, + web (_tag, { el }) { + el.isBuiltIn = true + return 'mpx-swiper' + }, + props: [ + { + test: /^(display-multiple-items|skip-hidden-item-layout|easing-function)$/, + ali: aliPropLog + }, + { + test: /^(skip-hidden-item-layout|easing-function|snap-to-edge)$/, + swan: baiduPropLog + }, + { + test: /^(easing-function)$/, + jd: jdPropLog + }, + { + test: /^(easing-function|snap-to-edge)$/, + qq: qqPropLog + }, + { + test: /^(skip-hidden-item-layout|easing-function)$/, + tt: ttPropLog + }, + { + test: /^(previous-margin|next-margin|display-multiple-items|skip-hidden-item-layout)$/, + web: webPropLog + }, + { + test: /^(snap-to-edge|easing-function)$/, + qa: qaPropLog + } + ], + event: [ + { + test: /^(change|animationfinish)$/, + ali (eventName) { + const eventMap = { + change: 'change', + animationfinish: 'animationEnd' + } + return eventMap[eventName as keyof typeof eventMap] + } + }, + { + test: /^(transition)$/, + swan: baiduEventLog, + jd: jdEventLog + } + ] + } +} diff --git a/packages/compiler/src/platform/template/wx/component-config/switch.ts b/packages/compiler/src/platform/template/wx/component-config/switch.ts new file mode 100644 index 0000000000..55cd3a2ada --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/switch.ts @@ -0,0 +1,26 @@ +import { DefineConfig } from '.' + +const TAG_NAME = 'switch' + +export default function ({ print }) { + const aliPropLog = print({ platform: 'ali', tag: TAG_NAME, isError: false }) + const jdPropLog = print({ platform: 'jd', tag: TAG_NAME, isError: false }) + + return { + test: TAG_NAME, + web (_tag, { el }) { + el.isBuiltIn = true + return 'mpx-switch' + }, + props: [ + { + test: /^type$/, + ali: aliPropLog + }, + { + test: /^disabled$/, + jd: jdPropLog + } + ] + } +} diff --git a/packages/compiler/src/platform/template/wx/component-config/template.ts b/packages/compiler/src/platform/template/wx/component-config/template.ts new file mode 100644 index 0000000000..07f1252a7f --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/template.ts @@ -0,0 +1,20 @@ +import { DefineConfig } from '.' + +const TAG_NAME = 'template' + +export default function () { + return { + test: TAG_NAME, + props: [ + { + test: 'data', + swan ({ name, value }) { + return { + name, + value: `{${value}}` + } + } + } + ] + } +} diff --git a/packages/compiler/src/platform/template/wx/component-config/text.ts b/packages/compiler/src/platform/template/wx/component-config/text.ts new file mode 100644 index 0000000000..d00ab8cf2e --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/text.ts @@ -0,0 +1,45 @@ +import { DefineConfig } from '.' + +const TAG_NAME = 'text' + +export default function ({ print }) { + const baiduPropLog = print({ platform: 'baidu', tag: TAG_NAME, isError: false }) + const qaPropLog = print({ platform: 'qa', tag: TAG_NAME, isError: false }) + const aliPropLog = print({ platform: 'ali', tag: TAG_NAME, isError: false }) + const ttPropLog = print({ platform: 'bytedance', tag: TAG_NAME, isError: false }) + const qqPropLog = print({ platform: 'qq', tag: TAG_NAME, isError: false }) + + return { + test: TAG_NAME, + web (_tag, { el }) { + if (el.hasEvent) { + el.isBuiltIn = true + } + if (el.isBuiltIn) { + return 'mpx-text' + } else { + return 'span' + } + }, + props: [ + { + test: /^(decode|user-select)$/, + swan: baiduPropLog + }, + { + test: /^(user-select)$/, + ali: aliPropLog, + tt: ttPropLog, + qq: qqPropLog, + qa: qaPropLog + }, + { + test: /^(selectable|space|decode|use-built-in)$/, + web (_prop, { el }) { + el.isBuiltIn = true + }, + qa: qaPropLog + } + ] + } +} diff --git a/packages/compiler/src/platform/template/wx/component-config/textarea.ts b/packages/compiler/src/platform/template/wx/component-config/textarea.ts new file mode 100644 index 0000000000..25f0dad390 --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/textarea.ts @@ -0,0 +1,79 @@ +import { DefineConfig } from '.' + +const TAG_NAME = 'textarea' + +export default function ({ print }) { + const aliPropLog = print({ platform: 'ali', tag: TAG_NAME, isError: false }) + const aliEventLog = print({ platform: 'ali', tag: TAG_NAME, isError: false, type: 'event' }) + const ttPropLog = print({ platform: 'bytedance', tag: TAG_NAME, isError: false }) + const ttEventLog = print({ platform: 'bytedance', tag: TAG_NAME, isError: false, type: 'event' }) + const webPropLog = print({ platform: 'web', tag: TAG_NAME, isError: false }) + const webEventLog = print({ platform: 'web', tag: TAG_NAME, isError: false, type: 'event' }) + const jdPropLog = print({ platform: 'jd', tag: TAG_NAME, isError: false }) + const jdEventLog = print({ platform: 'jd', tag: TAG_NAME, isError: false, type: 'event' }) + const qaPropLog = print({ platform: 'qa', tag: TAG_NAME, isError: false }) + const qaEventLog = print({ platform: 'qa', tag: TAG_NAME, isError: false, type: 'event' }) + const qqEventLog = print({ platform: 'qq', tag: TAG_NAME, isError: false, type: 'event' }) + const qqPropLog = print({ platform: 'qq', tag: TAG_NAME, isError: false }) + const baiduPropLog = print({ platform: 'baidu', tag: TAG_NAME, isError: false }) + + return { + test: TAG_NAME, + web (_tag, { el }) { + // form全量使用内建组件 + el.isBuiltIn = true + return 'mpx-textarea' + }, + props: [ + { + test: /^(auto-focus|fixed|cursor-spacing|cursor|show-confirm-bar|selection-start|selection-end|adjust-position|hold-keyboard|disable-default-padding|confirm-type)$/, + ali: aliPropLog + }, + { + test: /^(hold-keyboard|disable-default-padding|confirm-type)$/, + qq: qqPropLog, + swan: baiduPropLog + }, + { + test: /^(placeholder-class|auto-focus|show-confirm-bar|adjust-position|hold-keyboard|disable-default-padding|confirm-type)$/, + tt: ttPropLog + }, + { + test: /^(placeholder-style|placeholder-class|fixed|cursor-spacing|show-confirm-bar|adjust-position|hold-keyboard|auto-height)$/, + web: webPropLog + }, + { + test: /^(hold-keyboard|disable-default-padding)$/, + jd: jdPropLog + }, + { + test: /^(fixed|cursor-spacing|show-confirm-bar|adjust-position|hold-keyboard|auto-height)$/, + qa: qaPropLog + } + ], + event: [ + { + test: /^(confirm|linechange)$/, + web: webEventLog + }, + { + test: /^keyboardheightchange$/, + ali: aliEventLog, + jd: jdEventLog, + qq: qqEventLog + }, + { + test: 'confirm', + web: webEventLog + }, + { + test: 'blur|input|confirm', + qa: qaEventLog + }, + { + test: /^(linechange|keyboardheightchange)$/, + tt: ttEventLog + } + ] + } +} diff --git a/packages/compiler/src/platform/template/wx/component-config/unsupported.ts b/packages/compiler/src/platform/template/wx/component-config/unsupported.ts new file mode 100644 index 0000000000..f7b1d2bd80 --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/unsupported.ts @@ -0,0 +1,63 @@ +import { DefineConfigs } from '.' + +// 支付宝小程序不支持的标签集合 +const ALI_UNSUPPORTED_TAG_NAME_ARR = ['live-pusher', 'live-player', 'audio', 'functional-page-navigator', 'editor'] +// 百度小程序不支持的标签集合 +const BAIDU_UNSUPPORTED_TAG_NAME_ARR = ['functional-page-navigator', 'live-pusher', 'editor'] +// QQ小程序不支持的标签集合 +const QQ_UNSUPPORTED_TAG_NAME_ARR = ['functional-page-navigator', 'official-account', 'editor'] +// 头条小程序不支持的标签集合 +const TT_UNSUPPORTED_TAG_NAME_ARR = ['movable-view', 'cover-image', 'cover-view', 'movable-area', 'open-data', 'official-account', 'editor', 'functional-page-navigator', 'audio', 'live-pusher'] +// 京东小程序不支持的标签集合 +const JD_UNSUPPORTED_TAG_NAME_ARR = ['functional-page-navigator', 'live-pusher', 'live-player', 'rich-text', 'audio', 'video', 'camera'] +// 快应用不支持的标签集合 +const QA_UNSUPPORTED_TAG_NAME_ARR = ['movable-view', 'movable-area', 'open-data', 'official-account', 'editor', 'functional-page-navigator', 'live-player', 'live-pusher', 'ad', 'cover-image'] + +export default function ({ print }) { + const aliUnsupportedTagError = print({ platform: 'ali', isError: true, type: 'tag' }) + const baiduUnsupportedTagError = print({ platform: 'baidu', isError: true, type: 'tag' }) + const qqUnsupportedTagError = print({ platform: 'qq', isError: true, type: 'tag' }) + const ttUnsupportedTagError = print({ platform: 'bytedance', isError: true, type: 'tag' }) + const jdUnsupportedTagError = print({ platform: 'jd', isError: true, type: 'tag' }) + const qaUnsupportedTagError = print({ platform: 'qa', isError: true, type: 'tag' }) + + const aliUnsupportedExp = new RegExp('^(' + ALI_UNSUPPORTED_TAG_NAME_ARR.join('|') + ')$') + const baiduUnsupportedExp = new RegExp('^(' + BAIDU_UNSUPPORTED_TAG_NAME_ARR.join('|') + ')$') + const qqUnsupportedExp = new RegExp('^(' + QQ_UNSUPPORTED_TAG_NAME_ARR.join('|') + ')$') + const ttUnsupportedExp = new RegExp('^(' + TT_UNSUPPORTED_TAG_NAME_ARR.join('|') + ')$') + const jdUnsupportedExp = new RegExp('^(' + JD_UNSUPPORTED_TAG_NAME_ARR.join('|') + ')$') + const qaUnsupportedExp = new RegExp('^(' + QA_UNSUPPORTED_TAG_NAME_ARR.join('|') + ')$') + + return [ + { + supportedModes: ['swan'], + test: baiduUnsupportedExp, + swan: baiduUnsupportedTagError + }, + { + supportedModes: ['ali'], + test: aliUnsupportedExp, + ali: aliUnsupportedTagError + }, + { + supportedModes: ['qq'], + test: qqUnsupportedExp, + qq: qqUnsupportedTagError + }, + { + supportedModes: ['tt'], + test: ttUnsupportedExp, + tt: ttUnsupportedTagError + }, + { + supportedModes: ['jd'], + test: jdUnsupportedExp, + jd: jdUnsupportedTagError + }, + { + supportedModes: ['qa'], + test: qaUnsupportedExp, + qa: qaUnsupportedTagError + } + ] +} diff --git a/packages/compiler/src/platform/template/wx/component-config/video.ts b/packages/compiler/src/platform/template/wx/component-config/video.ts new file mode 100644 index 0000000000..d14e441394 --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/video.ts @@ -0,0 +1,121 @@ +import { DefineConfig } from '.' +const TAG_NAME = 'video' + +export default function ({ print }) { + const baiduPropLog = print({ + platform: 'baidu', + tag: TAG_NAME, + isError: false + }) + const baiduEventLogError = print({ + platform: 'baidu', + tag: TAG_NAME, + isError: false, + type: 'event' + }) + const qqPropLog = print({ platform: 'qq', tag: TAG_NAME, isError: false }) + const qqEventLogError = print({ + platform: 'qq', + tag: TAG_NAME, + isError: false, + type: 'event' + }) + const ttPropLog = print({ + platform: 'bytedance', + tag: TAG_NAME, + isError: false + }) + const ttEventLogError = print({ + platform: 'bytedance', + tag: TAG_NAME, + isError: false, + type: 'event' + }) + const aliPropLog = print({ platform: 'ali', tag: TAG_NAME, isError: false }) + const aliEventLogError = print({ + platform: 'ali', + tag: TAG_NAME, + isError: false, + type: 'event' + }) + const qaPropLog = print({ platform: 'qa', tag: TAG_NAME, isError: false }) + const qaEventLogError = print({ + platform: 'qa', + tag: TAG_NAME, + isError: false, + type: 'event' + }) + return { + test: TAG_NAME, + web (_tag, { el }) { + el.isBuiltIn = true + return 'mpx-video' + }, + props: [ + { + test: /^(enable-danmu|danmu-btn|show-progress|play-btn-position|enable-play-gesture|auto-pause-if-navigate|auto-pause-if-open-native|vslide-gesture|vslide-gesture-in-fullscreen|ad-unit-id|poster-for-crawler|show-casting-button|picture-in-picture-mode|picture-in-picture-show-progress|enable-auto-rotation|show-snapshot-button|show-screen-lock-button)$/, + ali: aliPropLog + }, + { + test: /^(duration|play-btn-position|auto-pause-if-navigate|auto-pause-if-open-native|ad-unit-id|poster-for-crawler|show-casting-button|picture-in-picture-mode|picture-in-picture-show-progress|enable-auto-rotation|show-snapshot-button|show-screen-lock-button)$/, + swan: baiduPropLog + }, + { + test: /^(vslide-gesture)$/, + qq(obj) { + const propsMap = { + 'vslide-gesture': 'page-gesture' + } + obj.name = propsMap[obj.name as keyof typeof propsMap] + return obj + } + }, + { + test: /^(vslide-gesture-in-fullscreen|ad-unit-id|show-screen-lock-button|enable-auto-rotation|show-snapshot-button|picture-in-picture-show-progress|picture-in-picture-mode|poster-for-crawler|show-casting-button|enable-play-gesture)$/, + qq: qqPropLog + }, + { + test: /^(duration|danmu-list|danmu-btn|enable-danmu|muted|initial-time|page-gesture|direction|show-progress|show-center-play-btn|enable-progress-gesture|show-mute-btn|title|enable-play-gesture|auto-pause-if-navigate|auto-pause-if-open-native|vslide-gesture|vslide-gesture-in-fullscreen|ad-unit-id|poster-for-crawler|show-casting-button|picture-in-picture-mode|picture-in-picture-show-progress|enable-auto-rotation|show-screen-lock-button|show-snapshot-button)$/, + tt: ttPropLog + }, + { + test: /^(duration|danmu-list|danmu-btn|enable-danmu|muted|initial-time|page-gesture|direction|show-progress|show-center-play-btn|enable-progress-gesture|show-mute-btn|enable-play-gesture|auto-pause-if-navigate|auto-pause-if-open-native|vslide-gesture|vslide-gesture-in-fullscreen|ad-unit-id|poster-for-crawler|show-casting-button|picture-in-picture-mode|picture-in-picture-show-progress|enable-auto-rotation|show-screen-lock-button|show-snapshot-button)$/, + qa: qaPropLog + } + ], + event: [ + { + test: /^(timeupdate|fullscreenchange|waiting|loadedmetadata)$/, + ali(evtName) { + const eventMap = { + timeupdate: 'timeUpdate', + fullscreenchange: 'fullScreenChange', + waiting: 'loading', + loadedmetadata: 'renderStart' + } + return eventMap[evtName as keyof typeof eventMap] + } + }, + { + test: /^(enterpictureinpicture|leavepictureinpicture|controlstoggle|seekcomplete)$/, + ali: aliEventLogError + }, + { + test: /^(enterpictureinpicture|leavepictureinpicture|controlstoggle|loadedmetadata|seekcomplete)$/, + qq: qqEventLogError + }, + { + test: /^(progress|enterpictureinpicture|leavepictureinpicture|controlstoggle|loadedmetadata|seekcomplete)$/, + swan: baiduEventLogError + }, + { + test: /^(progress|enterpictureinpicture|leavepictureinpicture|controlstoggle|loadedmetadata|seekcomplete)$/, + tt: ttEventLogError + }, + { + test: /^(progress|enterpictureinpicture|leavepictureinpicture|controlstoggle|loadedmetadata|seekcomplete)$/, + qa: qaEventLogError + } + ] + } +} diff --git a/packages/compiler/src/platform/template/wx/component-config/view.ts b/packages/compiler/src/platform/template/wx/component-config/view.ts new file mode 100644 index 0000000000..6a62557fbb --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/view.ts @@ -0,0 +1,62 @@ +import { DefineConfig } from '.' + +const TAG_NAME = 'view' + +export default function ({ print }) { + const qaPropLog = print({ platform: 'qa', tag: TAG_NAME, isError: false }) + const qaEventLogError = print({ platform: 'qa', tag: TAG_NAME, isError: false, type: 'event' }) + + return { + // 匹配标签名,可传递正则 + test: TAG_NAME, + // 支付宝标签名转换函数,如无差异可忽略 + // ali () { + // return 'a:view' + // }, + web (_tag, { el }) { + if (el.hasEvent) { + el.isBuiltIn = true + } + if (el.isBuiltIn) { + return 'mpx-view' + } else { + return 'div' + } + }, + qa () { + return 'div' + }, + // 组件属性中的差异部分 + props: [ + { + test: /^(hover-(class|stop-propagation|start-time|stay-time)|use-built-in)$/, + // 当遇到微信支持而支付宝不支持的特性时,转换函数可以只抛出错误或警告而不返回值 + web (_prop, { el }) { + el.isBuiltIn = true + }, + qa: qaPropLog + } + ], + // 组件事件中的差异部分 + // 微信中基础事件有touchstart|touchmove|touchcancel|touchend|tap|longpress|longtap|transitionend|animationstart|animationiteration|animationend|touchforcechange + // 支付宝中的基础事件有touchStart|touchMove|touchEnd|touchCancel|tap|longTap + // 快应用通用事件有touchstart|touchmove|touchend|touchcancel|longpress|click|focus|blur + event: [ + { + // 支付宝中的view组件额外支持了transitionEnd|animationStart|animationIteration|animationEnd,故在此声明了组件事件转换逻辑 + test: /^(transitionend|animationstart|animationiteration|animationend)$/, + // + ali (eventName) { + const eventMap = { + transitionend: 'transitionEnd', + animationstart: 'animationStart', + animationiteration: 'animationIteration', + animationend: 'animationEnd' + } + return eventMap[eventName as keyof typeof eventMap] + }, + qa: qaEventLogError + } + ] + } +} diff --git a/packages/compiler/src/platform/template/wx/component-config/web-view.ts b/packages/compiler/src/platform/template/wx/component-config/web-view.ts new file mode 100644 index 0000000000..a3009bf1fc --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/web-view.ts @@ -0,0 +1,13 @@ +import { DefineConfig } from '.' + +const TAG_NAME = 'web-view' + +export default function () { + return { + test: TAG_NAME, + web (_tag, { el }) { + el.isBuiltIn = true + return 'mpx-web-view' + } + } +} diff --git a/packages/compiler/src/platform/template/wx/component-config/wxs.ts b/packages/compiler/src/platform/template/wx/component-config/wxs.ts new file mode 100644 index 0000000000..cf6f9c48bd --- /dev/null +++ b/packages/compiler/src/platform/template/wx/component-config/wxs.ts @@ -0,0 +1,56 @@ +import { DefineConfig } from '.' + +const TAG_NAME = 'wxs' + +export default function () { + return { + // 匹配标签名,可传递正则 + test: TAG_NAME, + ali () { + return 'import-sjs' + }, + swan () { + return 'import-sjs' + }, + qq () { + return 'qs' + }, + jd () { + return 'jds' + }, + tt () { + return 'sjs' + }, + qa () { + return 'qjs' + }, + dd () { + return 'dds' + }, + // 组件属性中的差异部分 + props: [ + { + test: 'src', + ali (obj) { + obj.name = 'from' + return obj + }, + qa (obj) { + obj.name = 'from' + return obj + } + }, + { + test: 'module', + ali (obj) { + obj.name = 'name' + return obj + }, + qa (obj) { + obj.name = 'name' + return obj + } + } + ] + } +} diff --git a/packages/compiler/src/platform/template/wx/index.ts b/packages/compiler/src/platform/template/wx/index.ts new file mode 100644 index 0000000000..ddc20c2f27 --- /dev/null +++ b/packages/compiler/src/platform/template/wx/index.ts @@ -0,0 +1,433 @@ +// @ts-nocheck +import { parse } from 'json5' +import { normalize, isValidIdentifierStr } from '@mpxjs/compile-utils' +import runRules from '../../run-rules' +import getComponentConfigs from './component-config' +import normalizeComponentRules from '../normalize-component-rules' +import { parseMustache as _parseMustache, stringifyWithResolveComputed as _stringifyWithResolveComputed } from '../../../template-compiler/compiler' +const parseMustache = _parseMustache +const stringifyWithResolveComputed = _stringifyWithResolveComputed + +export default function getSpec ({ warn, error }) { + const spec = { + supportedModes: ['ali', 'swan', 'qq', 'tt', 'web', 'qa', 'jd', 'dd'], + // props预处理 + preProps: [], + // props后处理 + postProps: [ + { + web ({ name, value }) { + const parsed = parseMustache(value) + if (parsed.hasBinding) { + return { + name: name === 'animation' ? 'v-' + name : ':' + name, + value: parsed.result + } + } + } + } + ], + // 指令处理 + directive: [ + // 特殊指令 + { + test: 'wx:for', + swan (obj, data) { + const attrsMap = data.el.attrsMap + const parsed = parseMustache(obj.value) + let listName = parsed.result + const el = data.el + + const itemName = attrsMap['wx:for-item'] || 'item' + const indexName = attrsMap['wx:for-index'] || 'index' + const keyName = attrsMap['wx:key'] || null + let keyStr = '' + + if (parsed.hasBinding) { + listName = listName.slice(1, -1) + } + + if (keyName) { + const parsed = parseMustache(keyName) + if (parsed.hasBinding) { + // keyStr = ` trackBy ${parsed.result.slice(1, -1)}` + } else if (keyName === '*this') { + keyStr = ` trackBy ${itemName}` + } else { + if (!isValidIdentifierStr(keyName)) { + keyStr = ` trackBy ${itemName}['${keyName}']` + } else { + keyStr = ` trackBy ${itemName}.${keyName}` + } + } + } + if (el) { + const injectWxsProp = { + injectWxsPath: '~' + normalize.lib('runtime/swanHelper.wxs'), + injectWxsModuleName: '__swanHelper__' + } + if (el.injectWxsProps && Array.isArray(el.injectWxsProps)) { + el.injectWxsProps.push(injectWxsProp) + } else { + el.injectWxsProps = [injectWxsProp] + } + } + return { + name: 's-for', + value: `${itemName}, ${indexName} in __swanHelper__.processFor(${listName})${keyStr}` + } + }, + web ({ value }, { el }) { + const parsed = parseMustache(value) + const attrsMap = el.attrsMap + const itemName = attrsMap['wx:for-item'] || 'item' + const indexName = attrsMap['wx:for-index'] || 'index' + return { + name: 'v-for', + value: `(${itemName}, ${indexName}) in ${parsed.result}` + } + } + }, + { + test: 'wx:key', + swan () { + return false + }, + web ({ value }, { el }) { + // vue的template中不能包含key,对应于小程序中的block + if (el.tag === 'block') return false + const itemName = el.attrsMap['wx:for-item'] || 'item' + const keyName = value + if (value === '*this') { + value = itemName + } else { + if (isValidIdentifierStr(keyName)) { + value = `${itemName}.${keyName}` + } else { + value = `${itemName}['${keyName}']` + } + } + return { + name: ':key', + value + } + } + }, + { + // 在swan/web模式下删除for-index/for-item,转换为v/s-for表达式 + test: /^wx:(for-item|for-index)$/, + swan () { + return false + }, + web () { + return false + } + }, + { + test: 'wx:model', + web ({ value }, { el }) { + el.hasEvent = true + const attrsMap = el.attrsMap + const tagRE = /\{\{((?:.|\n|\r)+?)\}\}(?!})/ + const stringify = JSON.stringify + const match = tagRE.exec(value) + if (match) { + const modelProp = attrsMap['wx:model-prop'] || 'value' + const modelEvent = attrsMap['wx:model-event'] || 'input' + const modelValuePathRaw = attrsMap['wx:model-value-path'] + const modelValuePath = modelValuePathRaw === undefined ? 'value' : modelValuePathRaw + const modelFilter = attrsMap['wx:model-filter'] + let modelValuePathArr + try { + modelValuePathArr = parse(modelValuePath) + } catch (e) { + if (modelValuePath === '') { + modelValuePathArr = [] + } else { + modelValuePathArr = modelValuePath.split('.') + } + } + const modelValue = match[1].trim() + return [ + { + name: ':' + modelProp, + value: modelValue + }, + { + name: 'mpxModelEvent', + value: modelEvent + }, + { + name: '@mpxModel', + value: `__model(${stringifyWithResolveComputed(modelValue)}, $event, ${stringify(modelValuePathArr)}, ${stringify(modelFilter)})` + } + ] + } + } + }, + { + test: /^wx:(model-prop|model-event|model-value-path|model-filter)$/, + web () { + return false + } + }, + { + // ref只支持字符串字面量 + test: 'wx:ref', + web ({ value }) { + return { + name: 'ref', + value: `__mpx_ref_${value}__` + } + } + }, + { + // style样式绑定 + test: /^(style|wx:style)$/, + web ({ value }, { el }) { + if (el.isStyleParsed) { + return false + } + const styleBinding = [] + el.isStyleParsed = true + el.attrsList.forEach((item) => { + const parsed = parseMustache(item.value) + if (item.name === 'style') { + if (parsed.hasBinding || parsed.result.indexOf('rpx') > -1) { + styleBinding.push(parseMustache(item.value).result) + } else { + styleBinding.push(JSON.stringify(item.value)) + } + } else if (item.name === 'wx:style') { + styleBinding.push(parseMustache(item.value).result) + } + }) + return { + name: ':style', + value: `[${styleBinding}] | transRpxStyle` + } + } + }, + { + // 样式类名绑定 + test: /^wx:(class)$/, + web ({ value }) { + const parsed = parseMustache(value) + return { + name: ':class', + value: parsed.result + } + } + }, + // 通用指令 + { + test: /^wx:(.*)$/, + ali ({ name, value }) { + const dir = this.test.exec(name)[1] + return { + name: 'a:' + dir, + value + } + }, + swan ({ name, value }) { + const dir = this.test.exec(name)[1] + return { + name: 's-' + dir, + value + } + }, + qq ({ name, value }) { + const dir = this.test.exec(name)[1] + return { + name: 'qq:' + dir, + value + } + }, + jd ({ name, value }) { + const dir = this.test.exec(name)[1] + return { + name: 'jd:' + dir, + value + } + }, + tt ({ name, value }) { + const dir = this.test.exec(name)[1] + return { + name: 'tt:' + dir, + value + } + }, + dd ({ name, value }) { + const dir = this.test.exec(name)[1] + return { + name: 'dd:' + dir, + value + } + }, + web ({ name, value }) { + let dir = this.test.exec(name)[1] + const parsed = parseMustache(value) + if (dir === 'elif') { + dir = 'else-if' + } + return { + name: 'v-' + dir, + value: parsed.result + } + } + }, + // 事件 + { + test: /^(bind|catch|capture-bind|capture-catch):?(.*?)(\..*)?$/, + ali ({ name, value }, { eventRules }) { + const match = this.test.exec(name) + const prefix = match[1] + const eventName = match[2] + const modifierStr = match[3] || '' + const rPrefix = runRules(spec.event.prefix, prefix, { mode: 'ali' }) + const rEventName = runRules(eventRules, eventName, { mode: 'ali' }) + return { + name: rPrefix + rEventName.replace(/^./, (matched) => { + return matched.toUpperCase() + }) + modifierStr, + value + } + }, + swan ({ name, value }, { eventRules }) { + const match = this.test.exec(name) + const eventName = match[2] + runRules(eventRules, eventName, { mode: 'swan' }) + }, + qq ({ name, value }, { eventRules }) { + const match = this.test.exec(name) + const eventName = match[2] + runRules(eventRules, eventName, { mode: 'qq' }) + }, + jd ({ name, value }, { eventRules }) { + const match = this.test.exec(name) + const eventName = match[2] + runRules(eventRules, eventName, { mode: 'jd' }) + }, + // tt ({ name, value }, { eventRules }) { + // const match = this.test.exec(name) + // const prefix = match[1] + // const eventName = match[2] + // const modifierStr = match[3] || '' + // const rEventName = runRules(eventRules, eventName, { mode: 'tt' }) + // return { + // // 字节将所有事件转为小写 + // name: prefix + rEventName.toLowerCase() + modifierStr, + // value + // } + // }, + tt ({ name, value }, { eventRules }) { + const match = this.test.exec(name) + const eventName = match[2] + runRules(eventRules, eventName, { mode: 'tt' }) + }, + dd ({ name, value }, { eventRules }) { + const match = this.test.exec(name) + const eventName = match[2] + runRules(eventRules, eventName, { mode: 'dd' }) + }, + web ({ name, value }, { eventRules, el }) { + const match = this.test.exec(name) + const prefix = match[1] + const eventName = match[2] + const modifierStr = match[3] || '' + const meta = { + modifierStr + } + // 记录event监听信息用于后续判断是否需要使用内置基础组件 + el.hasEvent = true + const rPrefix = runRules(spec.event.prefix, prefix, { mode: 'web', meta }) + const rEventName = runRules(eventRules, eventName, { mode: 'web' }) + return { + name: rPrefix + rEventName + meta.modifierStr, + value + } + } + }, + // 无障碍 + { + test: /^aria-(role|label)$/, + ali () { + warn('Ali environment does not support aria-role|label props!') + } + } + ], + event: { + prefix: [ + { + ali (prefix) { + const prefixMap = { + bind: 'on', + catch: 'catch' + } + if (!prefixMap[prefix]) { + error(`Ali environment does not support [${prefix}] event handling!`) + return + } + return prefixMap[prefix] + }, + // 通过meta将prefix转化为modifier + web (prefix, data, meta) { + const modifierStr = meta.modifierStr + const modifierMap = modifierStr.split('.').reduce((map, key) => { + if (key) { + map[key] = true + } + return map + }, {}) + switch (prefix) { + case 'catch': + modifierMap.stop = true + break + case 'capture-bind': + modifierMap.capture = true + break + case 'capture-catch': + modifierMap.stop = true + modifierMap.capture = true + break + } + // web中不支持proxy modifier + delete modifierMap.proxy + const tempModifierStr = Object.keys(modifierMap).join('.') + meta.modifierStr = tempModifierStr ? '.' + tempModifierStr : '' + return '@' + } + } + ], + rules: [ + // 通用冒泡事件 + { + test: /^(touchstart|touchmove|touchcancel|touchend|tap|longpress|longtap|transitionend|animationstart|animationiteration|animationend|touchforcechange)$/, + ali (eventName) { + const eventMap = { + touchstart: 'touchStart', + touchmove: 'touchMove', + touchend: 'touchEnd', + touchcancel: 'touchCancel', + tap: 'tap', + longtap: 'longTap', + longpress: 'longTap' + } + if (eventMap[eventName]) { + return eventMap[eventName] + } else { + error(`Ali environment does not support [${eventName}] event!`) + } + }, + web (eventName) { + if (eventName === 'touchforcechange') { + error(`Web environment does not support [${eventName}] event!`) + } + } + } + ] + } + } + spec.rules = normalizeComponentRules(getComponentConfigs({ warn, error }).concat({}), spec) + return spec +} diff --git a/packages/compiler/src/script-setup-compiler/index.ts b/packages/compiler/src/script-setup-compiler/index.ts new file mode 100644 index 0000000000..0b251dc0c1 --- /dev/null +++ b/packages/compiler/src/script-setup-compiler/index.ts @@ -0,0 +1,1168 @@ +// @ts-nocheck +import MagicString from 'magic-string' +import * as babylon from '@babel/parser' +import traverse from '@babel/traverse' +import * as t from '@babel/types' +import formatCodeFrame from '@babel/code-frame' + +// Special compiler macros +const DEFINE_PROPS = 'defineProps' +const DEFINE_OPTIONS = 'defineOptions' +const DEFINE_EXPOSE = 'defineExpose' +const WITH_DEFAULTS = 'withDefaults' + +const MPX_CORE = '@mpxjs/core' + +const BindingTypes = { + /** + * declared as a prop + */ + PROPS: 'props', + /** + * a local alias of a ` + + diff --git a/packages/web-plugin/src/runtime/components/web/mpx-checkbox-group.vue b/packages/web-plugin/src/runtime/components/web/mpx-checkbox-group.vue new file mode 100644 index 0000000000..eb0b9fa007 --- /dev/null +++ b/packages/web-plugin/src/runtime/components/web/mpx-checkbox-group.vue @@ -0,0 +1,86 @@ + diff --git a/packages/web-plugin/src/runtime/components/web/mpx-checkbox.vue b/packages/web-plugin/src/runtime/components/web/mpx-checkbox.vue new file mode 100644 index 0000000000..d2731f1002 --- /dev/null +++ b/packages/web-plugin/src/runtime/components/web/mpx-checkbox.vue @@ -0,0 +1,115 @@ + + + diff --git a/packages/web-plugin/src/runtime/components/web/mpx-form.vue b/packages/web-plugin/src/runtime/components/web/mpx-form.vue new file mode 100644 index 0000000000..788b15322f --- /dev/null +++ b/packages/web-plugin/src/runtime/components/web/mpx-form.vue @@ -0,0 +1,87 @@ + diff --git a/packages/web-plugin/src/runtime/components/web/mpx-icon.vue b/packages/web-plugin/src/runtime/components/web/mpx-icon.vue new file mode 100644 index 0000000000..5dbfe5ba34 --- /dev/null +++ b/packages/web-plugin/src/runtime/components/web/mpx-icon.vue @@ -0,0 +1,158 @@ + + + diff --git a/packages/web-plugin/src/runtime/components/web/mpx-image.vue b/packages/web-plugin/src/runtime/components/web/mpx-image.vue new file mode 100644 index 0000000000..f976057a04 --- /dev/null +++ b/packages/web-plugin/src/runtime/components/web/mpx-image.vue @@ -0,0 +1,113 @@ + + + + diff --git a/packages/web-plugin/src/runtime/components/web/mpx-input.vue b/packages/web-plugin/src/runtime/components/web/mpx-input.vue new file mode 100644 index 0000000000..9a0c143950 --- /dev/null +++ b/packages/web-plugin/src/runtime/components/web/mpx-input.vue @@ -0,0 +1,159 @@ + + + diff --git a/packages/web-plugin/src/runtime/components/web/mpx-keep-alive.vue b/packages/web-plugin/src/runtime/components/web/mpx-keep-alive.vue new file mode 100644 index 0000000000..5b99ea8b29 --- /dev/null +++ b/packages/web-plugin/src/runtime/components/web/mpx-keep-alive.vue @@ -0,0 +1,100 @@ + + + diff --git a/packages/web-plugin/src/runtime/components/web/mpx-movable-area.vue b/packages/web-plugin/src/runtime/components/web/mpx-movable-area.vue new file mode 100644 index 0000000000..c772364631 --- /dev/null +++ b/packages/web-plugin/src/runtime/components/web/mpx-movable-area.vue @@ -0,0 +1,42 @@ + + + + diff --git a/packages/web-plugin/src/runtime/components/web/mpx-movable-view.vue b/packages/web-plugin/src/runtime/components/web/mpx-movable-view.vue new file mode 100644 index 0000000000..004c2c58f2 --- /dev/null +++ b/packages/web-plugin/src/runtime/components/web/mpx-movable-view.vue @@ -0,0 +1,308 @@ + + + + diff --git a/packages/web-plugin/src/runtime/components/web/mpx-navigator.vue b/packages/web-plugin/src/runtime/components/web/mpx-navigator.vue new file mode 100644 index 0000000000..b9e4e5e7e2 --- /dev/null +++ b/packages/web-plugin/src/runtime/components/web/mpx-navigator.vue @@ -0,0 +1,119 @@ + diff --git a/packages/web-plugin/src/runtime/components/web/mpx-picker-view-column.vue b/packages/web-plugin/src/runtime/components/web/mpx-picker-view-column.vue new file mode 100644 index 0000000000..5218cc34f8 --- /dev/null +++ b/packages/web-plugin/src/runtime/components/web/mpx-picker-view-column.vue @@ -0,0 +1,91 @@ + + + + + diff --git a/packages/web-plugin/src/runtime/components/web/mpx-picker-view.vue b/packages/web-plugin/src/runtime/components/web/mpx-picker-view.vue new file mode 100644 index 0000000000..0a4e81f247 --- /dev/null +++ b/packages/web-plugin/src/runtime/components/web/mpx-picker-view.vue @@ -0,0 +1,213 @@ + + + + + diff --git a/packages/web-plugin/src/runtime/components/web/mpx-picker.vue b/packages/web-plugin/src/runtime/components/web/mpx-picker.vue new file mode 100644 index 0000000000..5a22211928 --- /dev/null +++ b/packages/web-plugin/src/runtime/components/web/mpx-picker.vue @@ -0,0 +1,648 @@ + + + + + + diff --git a/packages/web-plugin/src/runtime/components/web/mpx-progress.vue b/packages/web-plugin/src/runtime/components/web/mpx-progress.vue new file mode 100644 index 0000000000..98156d820a --- /dev/null +++ b/packages/web-plugin/src/runtime/components/web/mpx-progress.vue @@ -0,0 +1,173 @@ + + + diff --git a/packages/web-plugin/src/runtime/components/web/mpx-radio-group.vue b/packages/web-plugin/src/runtime/components/web/mpx-radio-group.vue new file mode 100644 index 0000000000..669a822984 --- /dev/null +++ b/packages/web-plugin/src/runtime/components/web/mpx-radio-group.vue @@ -0,0 +1,91 @@ + diff --git a/packages/web-plugin/src/runtime/components/web/mpx-radio.vue b/packages/web-plugin/src/runtime/components/web/mpx-radio.vue new file mode 100644 index 0000000000..76e9530e08 --- /dev/null +++ b/packages/web-plugin/src/runtime/components/web/mpx-radio.vue @@ -0,0 +1,113 @@ + + + diff --git a/packages/web-plugin/src/runtime/components/web/mpx-rich-text.vue b/packages/web-plugin/src/runtime/components/web/mpx-rich-text.vue new file mode 100644 index 0000000000..09f4c92208 --- /dev/null +++ b/packages/web-plugin/src/runtime/components/web/mpx-rich-text.vue @@ -0,0 +1,31 @@ + diff --git a/packages/web-plugin/src/runtime/components/web/mpx-scroll-view.vue b/packages/web-plugin/src/runtime/components/web/mpx-scroll-view.vue new file mode 100644 index 0000000000..d339788545 --- /dev/null +++ b/packages/web-plugin/src/runtime/components/web/mpx-scroll-view.vue @@ -0,0 +1,429 @@ + + + diff --git a/packages/web-plugin/src/runtime/components/web/mpx-slider.vue b/packages/web-plugin/src/runtime/components/web/mpx-slider.vue new file mode 100644 index 0000000000..2e49fd9f7d --- /dev/null +++ b/packages/web-plugin/src/runtime/components/web/mpx-slider.vue @@ -0,0 +1,236 @@ + + + diff --git a/packages/web-plugin/src/runtime/components/web/mpx-swiper-item.vue b/packages/web-plugin/src/runtime/components/web/mpx-swiper-item.vue new file mode 100644 index 0000000000..64a939add7 --- /dev/null +++ b/packages/web-plugin/src/runtime/components/web/mpx-swiper-item.vue @@ -0,0 +1,20 @@ + + + diff --git a/packages/web-plugin/src/runtime/components/web/mpx-swiper.vue b/packages/web-plugin/src/runtime/components/web/mpx-swiper.vue new file mode 100644 index 0000000000..bf93eb441e --- /dev/null +++ b/packages/web-plugin/src/runtime/components/web/mpx-swiper.vue @@ -0,0 +1,274 @@ + + + diff --git a/packages/web-plugin/src/runtime/components/web/mpx-switch.vue b/packages/web-plugin/src/runtime/components/web/mpx-switch.vue new file mode 100644 index 0000000000..a2f452065f --- /dev/null +++ b/packages/web-plugin/src/runtime/components/web/mpx-switch.vue @@ -0,0 +1,173 @@ + + + diff --git a/packages/web-plugin/src/runtime/components/web/mpx-tab-bar-container.vue b/packages/web-plugin/src/runtime/components/web/mpx-tab-bar-container.vue new file mode 100644 index 0000000000..09d5ab7363 --- /dev/null +++ b/packages/web-plugin/src/runtime/components/web/mpx-tab-bar-container.vue @@ -0,0 +1,88 @@ + + + + diff --git a/packages/web-plugin/src/runtime/components/web/mpx-tab-bar.vue b/packages/web-plugin/src/runtime/components/web/mpx-tab-bar.vue new file mode 100644 index 0000000000..74cd1a8274 --- /dev/null +++ b/packages/web-plugin/src/runtime/components/web/mpx-tab-bar.vue @@ -0,0 +1,87 @@ + + + diff --git a/packages/web-plugin/src/runtime/components/web/mpx-text.vue b/packages/web-plugin/src/runtime/components/web/mpx-text.vue new file mode 100644 index 0000000000..549052dbe4 --- /dev/null +++ b/packages/web-plugin/src/runtime/components/web/mpx-text.vue @@ -0,0 +1,81 @@ + + + diff --git a/packages/web-plugin/src/runtime/components/web/mpx-textarea.vue b/packages/web-plugin/src/runtime/components/web/mpx-textarea.vue new file mode 100644 index 0000000000..ae9f6cae4d --- /dev/null +++ b/packages/web-plugin/src/runtime/components/web/mpx-textarea.vue @@ -0,0 +1,145 @@ + + + diff --git a/packages/web-plugin/src/runtime/components/web/mpx-video.vue b/packages/web-plugin/src/runtime/components/web/mpx-video.vue new file mode 100755 index 0000000000..cc0c23058d --- /dev/null +++ b/packages/web-plugin/src/runtime/components/web/mpx-video.vue @@ -0,0 +1,254 @@ + + + + + diff --git a/packages/web-plugin/src/runtime/components/web/mpx-view.vue b/packages/web-plugin/src/runtime/components/web/mpx-view.vue new file mode 100644 index 0000000000..dffd361504 --- /dev/null +++ b/packages/web-plugin/src/runtime/components/web/mpx-view.vue @@ -0,0 +1,73 @@ + diff --git a/packages/web-plugin/src/runtime/components/web/mpx-web-view.vue b/packages/web-plugin/src/runtime/components/web/mpx-web-view.vue new file mode 100644 index 0000000000..d1ace84d4a --- /dev/null +++ b/packages/web-plugin/src/runtime/components/web/mpx-web-view.vue @@ -0,0 +1,138 @@ + + + + + diff --git a/packages/web-plugin/src/runtime/components/web/util.js b/packages/web-plugin/src/runtime/components/web/util.js new file mode 100644 index 0000000000..a6655aa06b --- /dev/null +++ b/packages/web-plugin/src/runtime/components/web/util.js @@ -0,0 +1,47 @@ +/** + @file web运行时组件抹平中需要用到的一些工具方法 + */ + +/** + * 处理字符串类型的宽高数值,兼容rpx + * @param {object | number} size 宽高 + * @param {object} option 配置项,目前仅支持配置默认值 + * @param {number} option.default 默认值,当传入的size有问题时返回 + * @returns {number} 处理后的数字宽高,单位px + */ +export function processSize (size, option = {}) { + const defaultValue = option.default || 0 + if (typeof size === 'number') { + return size + } else if (typeof size === 'string') { + const rs = parseInt(size, 10) + if (size.indexOf('rpx') !== -1) { + // 计算rpx折算回px + const width = window.screen.width + const finalRs = Math.floor(rs / 750 * width) + return finalRs + } else { + return isNaN(rs) ? defaultValue : rs + } + } else { + return defaultValue + } +} + +// 判断对象类型 +export function type (n) { + return Object.prototype.toString.call(n).slice(8, -1) +} + +export function isEmptyObject (obj) { + if (!obj) { + return true + } + // eslint-disable-next-line no-unreachable-loop + for (const key in obj) { + return false + } + return true +} + +export const isBrowser = typeof window !== 'undefined' diff --git a/packages/web-plugin/src/runtime/option-processor.d.ts b/packages/web-plugin/src/runtime/option-processor.d.ts new file mode 100644 index 0000000000..fd17fd663c --- /dev/null +++ b/packages/web-plugin/src/runtime/option-processor.d.ts @@ -0,0 +1,13 @@ +declare global { + namespace NodeJS { + interface Global { + [key: string]: any + } + } +} + +export default function processOption (...args: any): object + +export function getComponent (...args: any): object + +export function getWxsMixin (...args: any): object diff --git a/packages/web-plugin/src/runtime/option-processor.js b/packages/web-plugin/src/runtime/option-processor.js new file mode 100644 index 0000000000..2ec6f83e77 --- /dev/null +++ b/packages/web-plugin/src/runtime/option-processor.js @@ -0,0 +1,319 @@ +/* eslint-disable */ +import { isBrowser, hasOwn } from './utils' +import transRpxStyle from './trans-rpx-style' +import animation from './animation' + +export default function processOption ({ + option, + ctorType, + firstPage, + outputPath, + pageConfig, + pagesMap, + componentsMap, + tabBarMap, + componentGenerics, + genericsInfo, + mixin, + hasApp, + Vue, + VueRouter + } +) { + if (ctorType === 'app') { + // 对于app中的组件需要全局注册 + for (const componentName in componentsMap) { + if (hasOwn(componentsMap, componentName)) { + const component = componentsMap[componentName] + Vue.component(componentName, component) + } + } + + Vue.directive('animation', animation) + + Vue.filter('transRpxStyle', transRpxStyle) + + const routes = [] + + for (const pagePath in pagesMap) { + if (hasOwn(pagesMap, pagePath)) { + const page = pagesMap[pagePath] + routes.push({ + path: '/' + pagePath, + component: page + }) + } + } + + if (routes.length) { + if (firstPage) { + routes.push({ + path: '/', + redirect: '/' + firstPage + }) + } + const webRouteConfig = global.__mpx.config.webRouteConfig + global.__mpxRouter = option.router = new VueRouter({ + ...webRouteConfig, + routes: routes + }) + global.__mpxRouter.stack = [] + global.__mpxRouter.needCache = null + global.__mpxRouter.needRemove = [] + // 处理reLaunch中传递的url并非首页时的replace逻辑 + global.__mpxRouter.beforeEach(function (to, from, next) { + let action = global.__mpxRouter.__mpxAction + const stack = global.__mpxRouter.stack + + // 处理人为操作 + if (!action) { + if (stack.length > 1 && stack[stack.length - 2].path === to.path) { + action = { + type: 'back', + delta: 1 + } + } else { + action = { + type: 'to' + } + } + } + + const pageInRoutes = routes.some(item => item.path === to.path) + if (!pageInRoutes) { + if (stack.length < 1) { + if (global.__mpxRouter.app.$options.onPageNotFound) { + // onPageNotFound,仅首次进入时生效 + global.__mpxRouter.app.$options.onPageNotFound({ + path: to.path, + query: to.query, + isEntryPage: true + }) + return + } else { + console.warn(`[Mpx runtime warn]: the ${to.path} path does not exist in the application,will redirect to the home page path ${firstPage}`) + return next({ + path: firstPage, + replace: true + }) + } + } else { + let methods = '' + switch (action.type) { + case 'to': + methods = 'navigateTo' + break + case 'redirect': + methods = 'redirectTo' + break + case 'back': + methods = 'navigateBack' + break + case 'reLaunch': + methods = 'reLaunch' + break + default: + methods = 'navigateTo' + } + throw new Error(`${methods}:fail page "${to.path}" is not found`) + } + } + + const insertItem = { + path: to.path + } + // 构建历史栈 + switch (action.type) { + case 'to': + stack.push(insertItem) + global.__mpxRouter.needCache = insertItem + break + case 'back': + global.__mpxRouter.needRemove = stack.splice(stack.length - action.delta, action.delta) + break + case 'redirect': + global.__mpxRouter.needRemove = stack.splice(stack.length - 1, 1, insertItem) + global.__mpxRouter.needCache = insertItem + break + case 'switch': + if (!action.replaced) { + action.replaced = true + return next({ + path: action.path, + replace: true + }) + } else { + // 将非tabBar页面remove + let tabItem = null + global.__mpxRouter.needRemove = stack.filter((item) => { + if (tabBarMap[item.path.slice(1)]) { + tabItem = item + return false + } + return true + }) + if (tabItem) { + global.__mpxRouter.stack = [tabItem] + } else { + global.__mpxRouter.stack = [insertItem] + global.__mpxRouter.needCache = insertItem + } + } + break + case 'reLaunch': + if (!action.replaced) { + action.replaced = true + return next({ + path: action.path, + query: { + reLaunchCount: action.reLaunchCount + }, + replace: true + }) + } else { + global.__mpxRouter.needRemove = stack + global.__mpxRouter.stack = [insertItem] + global.__mpxRouter.needCache = insertItem + } + } + next() + }) + // 处理visibilitychange时触发当前活跃页面组件的onshow/onhide + if (isBrowser) { + const errorHandler = function (args, fromVue) { + if (global.__mpxAppCbs && global.__mpxAppCbs.error && global.__mpxAppCbs.error.length) { + global.__mpxAppCbs.error.forEach((cb) => { + cb.apply(null, args) + }) + } else if (fromVue) { + throw args[0] + } + } + Vue.config.errorHandler = (...args) => { + return errorHandler(args, true) + } + window.addEventListener('error', (event) => { + return errorHandler([event.error, event]) + }) + window.addEventListener('unhandledrejection', (event) => { + return errorHandler([event.reason, event]) + }) + document.addEventListener('visibilitychange', function () { + const vnode = global.__mpxRouter && global.__mpxRouter.__mpxActiveVnode + if (vnode && vnode.componentInstance) { + const currentPage = vnode.tag.endsWith('mpx-tab-bar-container') ? vnode.componentInstance.$refs.tabBarPage : vnode.componentInstance + if (document.hidden) { + if (global.__mpxAppCbs && global.__mpxAppCbs.hide) { + global.__mpxAppCbs.hide.forEach((cb) => { + cb() + }) + } + if (currentPage) { + currentPage.mpxPageStatus = 'hide' + } + } else { + if (global.__mpxAppCbs && global.__mpxAppCbs.show) { + global.__mpxAppCbs.show.forEach((cb) => { + // todo 实现app.onShow参数 + /* eslint-disable node/no-callback-literal */ + cb({}) + }) + } + if (currentPage) { + currentPage.mpxPageStatus = 'show' + } + } + } + }) + // 初始化length + global.__mpxRouter.__mpxHistoryLength = global.history.length + } + } + + // 注入pinia + if (global.__mpxPinia) { + option.pinia = global.__mpxPinia + } + } else { + // 局部注册页面和组件中依赖的组件 + for (const componentName in componentsMap) { + if (hasOwn(componentsMap, componentName)) { + const component = componentsMap[componentName] + if (!option.components) { + option.components = {} + } + option.components[componentName] = component + } + } + + if (genericsInfo) { + const genericHash = genericsInfo.hash + global.__mpxGenericsMap[genericHash] = {} + Object.keys(genericsInfo.map).forEach((genericValue) => { + if (componentsMap[genericValue]) { + global.__mpxGenericsMap[genericHash][genericValue] = componentsMap[genericValue] + } else { + console.log(option) + console.warn(`[Mpx runtime warn]: generic value "${genericValue}" must be +registered in parent context!`) + } + }) + } + + if (componentGenerics) { + option.props = option.props || {} + option.props.generichash = String + Object.keys(componentGenerics).forEach((genericName) => { + if (componentGenerics[genericName].default) { + option.props[`generic${genericName}`] = { + type: String, + default: `${genericName}default` + } + } else { + option.props[`generic${genericName}`] = String + } + }) + } + + if (ctorType === 'page') { + option.__mpxPageConfig = Object.assign({}, global.__mpxPageConfig, pageConfig) + } + if (!hasApp) { + option.directives = { animation } + option.filters = { transRpxStyle } + } + } + + if (option.mixins) { + option.mixins.push(mixin) + } else { + option.mixins = [mixin] + } + + if (outputPath) { + option.componentPath = '/' + outputPath + } + return option +} + +export function getComponent (component, extendOptions) { + component = component.__esModule ? component.default : component + // eslint-disable-next-line + if (extendOptions) Object.assign(component, extendOptions) + return component +} + +export function getWxsMixin (wxsModules) { + if (!wxsModules || !Object.keys(wxsModules).length) return {} + return { + created () { + Object.keys(wxsModules).forEach((key) => { + if (key in this) { + console.error(`[Mpx runtime error]: The wxs module key [${key}] exist in the component/page instance already, please check and rename it!`) + } else { + this[key] = wxsModules[key] + } + }) + } + } +} diff --git a/packages/web-plugin/src/runtime/trans-rpx-style.js b/packages/web-plugin/src/runtime/trans-rpx-style.js new file mode 100644 index 0000000000..34115f381f --- /dev/null +++ b/packages/web-plugin/src/runtime/trans-rpx-style.js @@ -0,0 +1,44 @@ +export default function (style) { + const defaultTransRpxFn = function (match, $1) { + const rpx2vwRatio = +(100 / 750).toFixed(8) + return '' + ($1 * rpx2vwRatio) + 'vw' + } + const transRpxFn = global.__mpxTransRpxFn || defaultTransRpxFn + const parsedStyleObj = {} + const rpxRegExpG = /\b(\d+(\.\d+)?)rpx\b/g + const parseStyleText = (cssText) => { + const listDelimiter = /;(?![^(]*\))/g + const propertyDelimiter = /:(.+)/ + if (typeof cssText === 'string') { + cssText.split(listDelimiter).forEach((item) => { + if (item) { + const tmp = item.split(propertyDelimiter) + tmp.length > 1 && (parsedStyleObj[tmp[0].trim()] = tmp[1].trim()) + } + }) + } else if (typeof cssText === 'object') { + if (Array.isArray(cssText)) { + cssText.forEach(cssItem => { + parseStyleText(cssItem) + }) + } else { + Object.assign(parsedStyleObj, cssText) + } + } + } + const transRpxStyleFn = (val) => { + if (typeof val === 'string' && val.indexOf('rpx') > 0) { + return val.replace(rpxRegExpG, transRpxFn).replace(/"/g, '') + } + return val + } + if (style) { + style.forEach(item => { + parseStyleText(item) + for (const key in parsedStyleObj) { + parsedStyleObj[key] = transRpxStyleFn(parsedStyleObj[key]) + } + }) + } + return parsedStyleObj +} diff --git a/packages/web-plugin/src/runtime/utils.js b/packages/web-plugin/src/runtime/utils.js new file mode 100644 index 0000000000..ac0786506e --- /dev/null +++ b/packages/web-plugin/src/runtime/utils.js @@ -0,0 +1,7 @@ +const hasOwnProperty = Object.prototype.hasOwnProperty + +export function hasOwn (obj, key) { + return hasOwnProperty.call(obj, key) +} + +export const isBrowser = typeof window !== 'undefined' diff --git a/packages/web-plugin/src/types/compilation.d.ts b/packages/web-plugin/src/types/compilation.d.ts new file mode 100644 index 0000000000..dd01ccdc98 --- /dev/null +++ b/packages/web-plugin/src/types/compilation.d.ts @@ -0,0 +1,12 @@ +import { MpxWithOptions } from '../mpx' +import 'webpack' + +declare module 'webpack' { + interface Compilation { + __mpx__: MpxWithOptions + } + + interface NormalModule { + wxs: boolean + } +} diff --git a/packages/web-plugin/src/types/patch.d.ts b/packages/web-plugin/src/types/patch.d.ts new file mode 100644 index 0000000000..c4873f375f --- /dev/null +++ b/packages/web-plugin/src/types/patch.d.ts @@ -0,0 +1,122 @@ + +/* eslint-disable @typescript-eslint/no-explicit-any */ +declare module 'webpack/lib/InitFragment' { + export default class InitFragment { + static STAGE_CONSTANTS: any + constructor(...args: any[]) + } +} +declare module 'webpack/lib/util/makeSerializable' { + export default function makeSerializable(...args: any[]): any +} +declare module 'webpack/lib/dependencies/ModuleDependency' {} +declare module '@mpxjs/compiler/template-compiler/parser' { + import { CompilerResult } from "@mpxjs/compiler"; + export default function parser(...args: any[]): CompilerResult +} +declare module 'webpack/lib/NullFactory' { + export default class NullFactory { + constructor() + } +} +declare module 'webpack/lib/dependencies/HarmonyImportDependencyParserPlugin' {} +declare module 'webpack/lib/FlagEntryExportAsUsedPlugin' { + export default class FlagEntryExportAsUsedPlugin { + constructor(nsObjectUsed:boolean, explanation:string) + apply(compiler: any):this + } +} +declare module 'webpack/lib/FileSystemInfo' { + export default function FileSystemInfo(): any +} + +declare module '@mpxjs/webpack-plugin/lib/dependencies/ResolveDependency' { + export default class ResolveDependency{ + static Template: typeof ResolveDependencyTemplate + constructor(resource:string, packageName:string, issuerResource:string, range: number[]) + } +} +declare class InjectDependencyTemplate { + constructor(); +} +declare module '@mpxjs/webpack-plugin/lib/dependencies/InjectDependency' { + export default class InjectDependency { + static Template: typeof InjectDependencyTemplate + constructor(options : { content: string; index: number } ) + } +} +declare class CommonJsVariableDependencyTemplate { + constructor(); +} +declare module '@mpxjs/webpack-plugin/lib/dependencies/CommonJsVariableDependency' { + export default class CommonJsVariableDependency { + name: string + static Template: typeof CommonJsVariableDependencyTemplate + constructor(request: string, name: string) + } +} +declare class ReplaceDependencyTemplate { + constructor(); +} +declare module '@mpxjs/webpack-plugin/lib/dependencies/ReplaceDependency' { + export default class ReplaceDependency { + static Template: typeof ReplaceDependencyTemplate + constructor(replacement: string, range: number[]) + } +} +declare class RecordResourceMapDependencyTemplate { + constructor(); +} +declare module '@mpxjs/webpack-plugin/lib/dependencies/RecordResourceMapDependency' { + export default class RecordResourceMapDependency { + static Template: typeof RecordResourceMapDependencyTemplate + constructor(resourcePath: string, resourceType: string, outputPath: string, packageRoot: string) + } +} +declare class RecordVueContentDependencyTemplate { + constructor(); +} +declare module '@mpxjs/webpack-plugin/lib/dependencies/RecordVueContentDependency' { + export default class RecordVueContentDependency { + static Template: typeof RecordVueContentDependencyTemplate + constructor(resourcePath: string, content: string) + } +} + +declare module '@mpxjs/webpack-plugin/lib/resolver/AddModePlugin' { + export default class AddModePlugin { + constructor(source: string, env: string, fileConditionRules, target) + apply(...args: any): void + } +} + +declare module '@mpxjs/webpack-plugin/lib/resolver/AddEnvPlugin' { + export default class AddEnvPlugin { + constructor(source: string, mode: string, fileConditionRules, target) + apply(...args: any): void + } +} + +declare interface DepConstructor { + new (...args: any[]): any; +} + +declare class DependencyTemplate { + constructor(); + apply(...args: any[]): void; +} + +declare abstract class ModuleFactory { + create(...args: any[]): void; +} + +declare module 'lru-cache' { + class LruCache { + constructor(cache: number); + + get(cacheKey: string): T; + + set(cacheKey: string, content: string): void; + } + export default LruCache +} diff --git a/packages/web-plugin/src/types/query.d.ts b/packages/web-plugin/src/types/query.d.ts new file mode 100644 index 0000000000..fe33284dae --- /dev/null +++ b/packages/web-plugin/src/types/query.d.ts @@ -0,0 +1,34 @@ +import 'loader-utils' +declare module 'loader-utils' { + export interface OptionObject { + vue?: boolean + mpx?: boolean + app?: boolean + page?: boolean + component?: boolean + resolve?: boolean + src?: string + type?: + | 'script' + | 'template' + | 'style' + | 'custom' + | 'global' + | 'main' + | 'globalDefine' + | 'hot' + index?: string + lang?: string + raw?: string + componentId?: string + async?: boolean + root?: string + outputPath?: string + mpxStyleOptions?: string + isPage?: boolean + isComponent?: boolean + isApp?: boolean + mode?: string + packageRoot?: string + } +} diff --git a/packages/web-plugin/src/utils/get-output-path.ts b/packages/web-plugin/src/utils/get-output-path.ts new file mode 100644 index 0000000000..d5651a9763 --- /dev/null +++ b/packages/web-plugin/src/utils/get-output-path.ts @@ -0,0 +1,17 @@ +import path from 'path' +import { Options } from '../options' +import pathHash from './path-hash' + +export default function getOutputPath ( + resourcePath:string, + type: string, + options: Options, + { ext = '', conflictPath = '' } = {}): string { + const name = path.parse(resourcePath).name + const hash = pathHash(resourcePath) + const customOutputPath = options.customOutputPath + if (conflictPath) return conflictPath.replace(/(\.[^\\/]+)?$/, match => hash + match) + if (typeof customOutputPath === 'function') return customOutputPath(type, name, hash, ext).replace(/^\//, '') + if (type === 'component' || type === 'page') return path.join(type + 's', name + hash, 'index' + ext) + return path.join(type, name + hash + ext) +} diff --git a/packages/web-plugin/src/utils/path-hash.ts b/packages/web-plugin/src/utils/path-hash.ts new file mode 100644 index 0000000000..31d9a77523 --- /dev/null +++ b/packages/web-plugin/src/utils/path-hash.ts @@ -0,0 +1,19 @@ +import hash from 'hash-sum' +import path from 'path' +import { Options } from '../options' + +export default function pathHash ( + resourcePath: string, + options?: Options +): string { + let hashPath = resourcePath + const pathHashMode = options?.pathHashMode + const projectRoot = options?.projectRoot || '' + if (pathHashMode === 'relative') { + hashPath = path.relative(projectRoot, resourcePath) + } + if (typeof pathHashMode === 'function') { + hashPath = pathHashMode(resourcePath, projectRoot) || resourcePath + } + return hash(hashPath) +} diff --git a/packages/web-plugin/src/vite.ts b/packages/web-plugin/src/vite.ts new file mode 100644 index 0000000000..e46ea88c4b --- /dev/null +++ b/packages/web-plugin/src/vite.ts @@ -0,0 +1,2 @@ +export * from './vite/index' +export { default } from './vite/index' diff --git a/packages/web-plugin/src/vite/config.ts b/packages/web-plugin/src/vite/config.ts new file mode 100644 index 0000000000..e31daf32b6 --- /dev/null +++ b/packages/web-plugin/src/vite/config.ts @@ -0,0 +1,9 @@ +export type ResolvedConfig = { + sourceMap?: boolean + isProduction: boolean + base?: string +} + +export const resolvedConfig: ResolvedConfig = { + isProduction: true +} diff --git a/packages/web-plugin/src/vite/handle-hot-update.ts b/packages/web-plugin/src/vite/handle-hot-update.ts new file mode 100644 index 0000000000..62849219e8 --- /dev/null +++ b/packages/web-plugin/src/vite/handle-hot-update.ts @@ -0,0 +1,27 @@ +import { addQuery } from '@mpxjs/compile-utils' +import { HmrContext } from 'vite' +import { getDescriptor, setPrevDescriptor } from './utils/descriptor-cache' + +export default async function handleHotUpdate (ctx: HmrContext) { + const prevDescriptor = getDescriptor(ctx.file) + if (!prevDescriptor) return + // 有descriptor缓存的是mpx文件或者外联json文件 + setPrevDescriptor(ctx.file, prevDescriptor) + + // 改写read方法,vue内部热更新会调用 + ctx.read = async function () { + // 增加type令mpx转换为一个默认的空的js并跳过vue插件转换 + const id = addQuery(ctx.file, { + type: 'hot', + vue: true, + isPage: prevDescriptor?.isPage, + app: prevDescriptor?.app, + isComponent: prevDescriptor?.isComponent + }) + // 插件转换mpx文件并缓存代码到vueSfc + await ctx.server.transformRequest(id) + const descriptor = getDescriptor(ctx.file) + // 给vue热更新返回转换后的代码,让其对比 + return descriptor?.vueSfc || '' + } +} diff --git a/packages/web-plugin/src/vite/helper.ts b/packages/web-plugin/src/vite/helper.ts new file mode 100644 index 0000000000..c776cb8c37 --- /dev/null +++ b/packages/web-plugin/src/vite/helper.ts @@ -0,0 +1,238 @@ +import { + addQuery, + genImport, + isUrlRequest, + parseRequest, + shallowStringify, + stringify +} from '@mpxjs/compile-utils' +import { PluginContext } from 'rollup' +import { OPTION_PROCESSOR_PATH, TAB_BAR_PATH } from '../constants' +import { Options } from '../options' +import { TabBarItem } from '@mpxjs/compiler' +import { resolvedConfig } from './config' +import mpxGlobal from './mpx' +import { genComponentCode } from './transformer/script' +import { SFCDescriptor } from './utils/descriptor-cache' + +export const ENTRY_HELPER_CODE = '\0/vite/mpx-entry-helper' +export const APP_HELPER_CODE = '\0/vite/mpx-app-helper' +export const I18N_HELPER_CODE = '\0/vite/mpx-i18n-helper' +export const TAB_BAR_PAGE_HELPER_CODE = '\0/vite/mpx-tab-bar-page-helper' + +export const renderPageRouteCode = ( + _options: Options, + importer: string +): string => { + return `export default ${stringify( + resolvedConfig.base + mpxGlobal.pagesMap[importer] + )}` +} + +export const renderEntryCode = async ( + importer: string, + options: Options +): Promise => { + return ` + ${genImport(addQuery(importer, { app: true }), 'App')} + ${genImport('@mpxjs/web-plugin/src/runtime/base.styl')} + ${genImport('vue', 'Vue')} + ${genImport('vue-router', 'VueRouter')} + ${genImport('@better-scroll/core', 'BScroll')} + ${genImport('@better-scroll/pull-down', 'PullDown')} + ${genImport('@better-scroll/observe-dom', 'ObserveDOM')} + Vue.use(VueRouter) + BScroll.use(ObserveDOM) + BScroll.use(PullDown) + global.BScroll = BScroll + new Vue({ + el: ${options.webConfig?.el || '"#app"'}, + render: function(h){ + return h(App) + } + }) + ` +} + +export function renderI18nCode (options: Options): string { + const content = [] + const { i18n } = options + if (i18n) { + content.push( + genImport('vue', 'Vue'), + genImport('vue-i18n', 'VueI18n'), + genImport('vue-i18n-bridge', '{ createI18n }'), + genImport('@mpxjs/core', 'Mpx'), + 'Vue.use(VueI18n, { bridge: true })' + ) + const i18nObj = { ...i18n } + const requestObj: Record = {} + const i18nKeys = ['messages', 'dateTimeFormats', 'numberFormats'] + i18nKeys.forEach(key => { + const keyPath = `${key}Path` as keyof typeof i18nObj + if (i18nObj[keyPath]) { + requestObj[key] = stringify(i18nObj[keyPath]) + delete i18nObj[keyPath] + } + }) + Object.keys(requestObj).forEach(key => { + content.push(`import __mpx__i18n__${key} from ${requestObj[key]}`) + }) + content.push(`const i18nCfg = ${stringify(i18nObj)}`) + Object.keys(requestObj).forEach(key => { + content.push(`i18nCfg.${key} = __mpx__i18n__${key}`) + }) + content.push( + 'i18nCfg.legacy = false', + 'const i18n = createI18n(i18nCfg, VueI18n)', + 'Vue.use(i18n)', + 'Mpx.i18n = i18n' + ) + } + return content.join('\n') +} + +/** + * app初始化代码,主要是初始化所有的global对象 + * @param descriptor - SFCDescriptor + * @returns + */ +export async function renderAppHelpCode ( + options: Options, + descriptor: SFCDescriptor, + pluginContext: PluginContext +) { + const { jsonConfig } = descriptor + const content = [] + const tabBar = { + ...jsonConfig.tabBar + } + const list = tabBar.list || [] + const newList = [] + const needReplaceName: string[] = [] + async function resolveTabBarItemIcon ( + item: TabBarItem, + key: 'iconPath' | 'selectedIconPath' + ) { + const iconPath = item[key] + if ( + iconPath && + isUrlRequest(iconPath, jsonConfig.path, options.externals) + ) { + const varName = `__mpx_icon_${needReplaceName.length}__` + needReplaceName.push(varName) + const resolveIcon = await pluginContext.resolve(iconPath, jsonConfig.path) + if (resolveIcon) { + item[key] = varName + content.push(`import ${varName} from "${resolveIcon.id}"`) + } + } + } + for (let i = 0; i < list.length; i++) { + const item = { + ...list[i] + } + await resolveTabBarItemIcon(item, 'iconPath') + await resolveTabBarItemIcon(item, 'selectedIconPath') + newList.push(item) + } + tabBar.list = newList + let tabBarStr = stringify(tabBar) + needReplaceName.forEach(v => { + tabBarStr = tabBarStr.replace(`"${v}"`, v) + }) + content.push( + `global.__networkTimeout = ${stringify(jsonConfig.networkTimeout)}`, + `global.__style = ${stringify(jsonConfig.style || 'v1')}`, + `global.__mpxPageConfig = ${stringify(jsonConfig.window || {})}`, + `global.__tabBar = ${tabBarStr}`, + `global.currentSrcMode = "${options.srcMode}"`, + 'global.getApp = function(){ return {} }', + `global.getCurrentPages = function(){ + if(!global.__mpxRouter) return [] + // @ts-ignore + return global.__mpxRouter.stack.map(item => { + let page + const vnode = item.vnode + if(vnode && vnode.componentInstance) { + page = vnode.tag.endsWith('mpx-tab-bar-container') ? vnode.componentInstance.$refs.tabBarPage : vnode.componentInstance + } + return page || { route: item.path.slice(1) } + }) + }`, + 'global.__mpxGenericsMap = {}' + ) + return content.join('\n') +} + +/** + * TabBar,mpx-tab-bar-container依赖global.__tabBarPagesMap + * @param options - + * @param descriptor - + * @param pluginContext - + * @returns + */ +export const renderTabBarPageCode = async ( + _options: Options, + descriptor: SFCDescriptor, + pluginContext: PluginContext +): Promise => { + const customBarPath = './custom-tab-bar/index?isComponent' + const tabBars: string[] = [] + const { filename, jsonConfig, tabBarMap, localPagesMap } = descriptor + const { tabBar } = jsonConfig + + const tabBarPagesMap: Record = {} + + const emitWarning = (msg: string) => { + pluginContext.warn('[script processor]: ' + msg) + } + + if (tabBar && tabBarMap) { + const varName = '__mpxTabBar' + let tabBarPath = TAB_BAR_PATH + if (tabBar.custom) { + const customBarPathResolved = await pluginContext.resolve( + customBarPath, + filename + ) + tabBarPath = customBarPathResolved?.id || TAB_BAR_PATH + } + tabBars.push(genImport(tabBarPath, varName)) + tabBarPagesMap['mpx-tab-bar'] = genComponentCode(varName, tabBarPath) + Object.keys(tabBarMap).forEach((tarbarName, index) => { + const tabBarId = localPagesMap[tarbarName].resource + if (tabBarId) { + const varName = `__mpx_tabBar__${index}` + const { queryObj: query } = parseRequest(tabBarId) + const async = !!query.async + !async && tabBars.push(genImport(tabBarId, varName)) + tabBarPagesMap[tarbarName] = genComponentCode( + varName, + tabBarId, + { + async + }, + { + __mpxPageroute: tarbarName + } + ) + } else { + emitWarning( + `TabBar page path ${tarbarName} is not exist in local page map, please check!` + ) + } + }) + } + + const content = [ + genImport('vue', 'Vue'), + genImport(OPTION_PROCESSOR_PATH, 'processOption, { getComponent }'), + tabBars.join('\n'), + 'global.__tabBar && Vue.observable(global.__tabBar)', + tabBarPagesMap && + `// @ts-ignore + global.__tabBarPagesMap = ${shallowStringify(tabBarPagesMap)}` + ] + return content.join('\n') +} diff --git a/packages/web-plugin/src/vite/index.ts b/packages/web-plugin/src/vite/index.ts new file mode 100644 index 0000000000..fccace0e50 --- /dev/null +++ b/packages/web-plugin/src/vite/index.ts @@ -0,0 +1,163 @@ +import { parseRequest, stringify, stringifyObject } from '@mpxjs/compile-utils' +import createVuePlugin from '@vitejs/plugin-vue2' +import { createFilter, Plugin, UserConfig } from 'vite' +import { Options, processOptions } from '../options' +import { resolvedConfig } from './config' +import handleHotUpdate from './handle-hot-update' +import { + APP_HELPER_CODE, + I18N_HELPER_CODE, + renderAppHelpCode, + renderI18nCode, + renderTabBarPageCode, + TAB_BAR_PAGE_HELPER_CODE +} from './helper' +import mpxGlobal from './mpx' +import { + customExtensionsPlugin, + esbuildCustomExtensionsPlugin +} from './plugins/add-extensions-plugin' +import { createMpxOutSideJsPlugin } from './plugins/outside-js' +import { createResolveEntryPlugin } from './plugins/resolve-entry-plugin' +import { createSplitPackageChunkPlugin } from './plugins/split-package-chunk-plugin' +import { createWxsPlugin } from './plugins/wxs-plugin' +import { transformMain } from './transformer/main' +import { transformStyle } from './transformer/style' +import { getDescriptor } from './utils/descriptor-cache' + +function createMpxWebPlugin(options: Options, userConfig?: UserConfig): Plugin { + const { include, exclude } = options + const filter = createFilter(include, exclude) + + return { + name: 'vite:mpx', + + config() { + return { + ...userConfig, + define: { + global: 'globalThis', // polyfill node global + 'process.env.NODE_ENV': stringify( + resolvedConfig.isProduction ? '"production"' : '"development"' + ), + ...userConfig?.define, + ...stringifyObject(options.defs) + } + } + }, + + configResolved(c) { + Object.assign(resolvedConfig, { + base: c.base, + sourceMap: c.command === 'build' ? !!c.build.sourcemap : true, + isProduction: c.isProduction + }) + }, + + handleHotUpdate(ctx) { + return handleHotUpdate(ctx) + }, + + async resolveId(id) { + if ( + id === APP_HELPER_CODE || + id === I18N_HELPER_CODE || + id === TAB_BAR_PAGE_HELPER_CODE + ) { + return id + } + }, + + load(id) { + if (id === APP_HELPER_CODE && mpxGlobal.entry) { + const { resourcePath: filename } = parseRequest(mpxGlobal.entry) + const descriptor = getDescriptor(filename) + if (descriptor) { + return renderAppHelpCode(options, descriptor, this) + } + } + if (id === TAB_BAR_PAGE_HELPER_CODE && mpxGlobal.entry) { + const { resourcePath: filename } = parseRequest(mpxGlobal.entry) + const descriptor = getDescriptor(filename) + if (descriptor) { + return renderTabBarPageCode(options, descriptor, this) + } + } + if (id === I18N_HELPER_CODE) { + return renderI18nCode(options) + } + }, + + async transform(code, id) { + const { queryObj: query, resourcePath: filename } = parseRequest(id) + if (!filter(filename)) return + if (!!query.resolve) return + if (query.vue === undefined) { + // mpx file => vue file + return await transformMain(code, filename, query, options, this) + } else { + if (query.type === 'style') { + // mpx style => vue style + const descriptor = getDescriptor(filename) + if (descriptor) { + return await transformStyle( + code, + filename, + descriptor, + options, + this + ) + } + } + if (query.type === 'hot') { + // 来自于热更新的请求,转换新的代码并缓存vueSfc到descriptor + await transformMain(code, filename, query, options, this) + return 'export default {}' + } + } + } + } +} + +export default function mpx (options: Partial = {}): Plugin[] { + const baseOptions = processOptions({ ...options }) + const { mode = '', env = '', fileConditionRules } = baseOptions + const customExtensions = [mode, env, env && `${mode}.${env}`].filter(Boolean) + const plugins = [ + // split subpackage chunk + createSplitPackageChunkPlugin(), + // add custom extensions + customExtensionsPlugin({ + include: /@mpxjs|\.mpx/, + fileConditionRules, + extensions: customExtensions + }), + // ensure mpx entry point + createResolveEntryPlugin(baseOptions), + // wxs => js + createWxsPlugin(), + // 外联js/ts增加globalDefine + createMpxOutSideJsPlugin(), + // mpx => vue + createMpxWebPlugin(baseOptions, { + optimizeDeps: { + esbuildOptions: { + plugins: [ + // prebuild for addExtensions + esbuildCustomExtensionsPlugin({ + include: /@mpxjs|api-proxy|core/, + fileConditionRules, + extensions: customExtensions + }) + ] + } + } + }), + // vue support for mpxjs/rumtime + createVuePlugin({ + include: /\.vue|\.mpx$/ + }) + ] + + return plugins +} diff --git a/packages/web-plugin/src/vite/mpx.ts b/packages/web-plugin/src/vite/mpx.ts new file mode 100644 index 0000000000..b08ce47b41 --- /dev/null +++ b/packages/web-plugin/src/vite/mpx.ts @@ -0,0 +1,15 @@ +interface Mpx { + entry?: string + pagesMap: Record + componentsMap: Record + pagesEntryMap: Record +} + +const mpx: Mpx = { + entry: undefined, + pagesMap: {}, + componentsMap: {}, + pagesEntryMap: {} +} + +export default mpx diff --git a/packages/web-plugin/src/vite/plugins/add-extensions-plugin.ts b/packages/web-plugin/src/vite/plugins/add-extensions-plugin.ts new file mode 100644 index 0000000000..06f2592efb --- /dev/null +++ b/packages/web-plugin/src/vite/plugins/add-extensions-plugin.ts @@ -0,0 +1,75 @@ +import { matchCondition, parseRequest } from '@mpxjs/compile-utils' +import { Plugin as EsbuildPlugin } from 'esbuild' +import fs from 'fs' +import path from 'path' +import { createFilter, Plugin } from 'vite' +import { Options } from '../../options' + +export interface CustomExtensionsOptions { + include: RegExp + extensions: string[] + fileConditionRules: Options['fileConditionRules'] +} + +/** + * generate file path with mode + * @param originPath - path/to/index.js + * @param extendsion - string + * @returns path/to/index.extendsion.js + */ +function genExtensionsFilePath (filename: string, extendsion: string): string { + const parseResult = path.parse(filename) + return path.format({ + ...parseResult, + name: `${parseResult.name}.${extendsion}`, + base: undefined + }) +} + +export function esbuildCustomExtensionsPlugin( + options: CustomExtensionsOptions +): EsbuildPlugin { + return { + name: 'esbuild:mpx-custom-estensions', + setup(build) { + build.onLoad({ filter: options.include }, async args => { + if (!matchCondition(args.path, options.fileConditionRules)) return + for (const extendsion of options.extensions) { + try { + const filePath = genExtensionsFilePath(args.path, extendsion) + await fs.promises.access(filePath) + return { + contents: await fs.promises.readFile(filePath, 'utf-8') + } + } catch {} + } + }) + } + } +} + +/** + * add custom extensions plugin + * @param options - options + * @returns vite plugin options + */ +export function customExtensionsPlugin ( + options: CustomExtensionsOptions +): Plugin { + const filter = createFilter(options.include) + return { + name: 'vite:mpx-custom-estensions', + async load (id) { + if (!filter(id) || !matchCondition(id, options.fileConditionRules)) return + const { resourcePath: filename, queryObj: query } = parseRequest(id) + if (query.vue) return + for (const extendsion of options.extensions) { + try { + const filePath = genExtensionsFilePath(filename, extendsion) + await fs.promises.access(filePath) + return await fs.promises.readFile(filePath, 'utf-8') + } catch {} + } + } + } +} diff --git a/packages/web-plugin/src/vite/plugins/outside-js.ts b/packages/web-plugin/src/vite/plugins/outside-js.ts new file mode 100644 index 0000000000..ca66e9d689 --- /dev/null +++ b/packages/web-plugin/src/vite/plugins/outside-js.ts @@ -0,0 +1,33 @@ +import { parseRequest, stringify } from '@mpxjs/compile-utils' +import MagicString from 'magic-string' +import { createFilter, Plugin } from 'vite' +import { resolvedConfig } from '../config' +import { getDescriptor } from '../utils/descriptor-cache' + +/** + * 给外联的js加上global配置 + * @returns + */ +export function createMpxOutSideJsPlugin (): Plugin { + const filter = createFilter([/\.(js|ts)$/]) + return { + name: 'vite:mpx-outside-js', + async transform (code, id) { + const { resourcePath: filename } = parseRequest(id) + if (!filter(filename)) return + const descriptor = getDescriptor(filename) + if (!descriptor) return + const s = new MagicString(code) + !resolvedConfig.isProduction && + s.prepend(`global.currentResource = ${stringify(filename)}\n`) + s.prepend(`global.currentModuleId = ${stringify(descriptor.id)}\n`) + return { + code: s.toString(), + map: s.generateMap({ + file: filename + '.map', + source: filename + }) + } + } + } +} diff --git a/packages/web-plugin/src/vite/plugins/resolve-entry-plugin.ts b/packages/web-plugin/src/vite/plugins/resolve-entry-plugin.ts new file mode 100644 index 0000000000..2eb69593d8 --- /dev/null +++ b/packages/web-plugin/src/vite/plugins/resolve-entry-plugin.ts @@ -0,0 +1,66 @@ +import { createFilter, Plugin } from 'vite' +import { Options } from '../../options' +import { addQuery, parseRequest } from '@mpxjs/compile-utils' +import { ENTRY_HELPER_CODE, renderEntryCode, renderPageRouteCode } from '../helper' +import mpxGlobal from '../mpx' + +/** + * 推断mpx入口文件并记录的插件 + * @param options - Options + * @returns + */ +export function createResolveEntryPlugin (options: Options): Plugin { + const filter = createFilter([/\.mpx$/]) + return { + name: 'vite:mpx-resolve-entry', + enforce: 'pre', + async resolveId (source, importer, options) { + const { queryObj: query, resourcePath: filename } = parseRequest(source) + if (!filter(filename)) return + if ( + query.resolve === undefined && + query.vue === undefined && + query.app === undefined && + query.isPage === undefined && + query.isComponent === undefined + ) { + // entry mpx + const resolution = await this.resolve(source, importer, { + skipSelf: true, + ...options + }) + if (resolution) { + if (mpxGlobal.entry === undefined) { + mpxGlobal.entry = resolution.id + } + if (mpxGlobal.entry === resolution.id) { + return ENTRY_HELPER_CODE + } + } + } + if (query.resolve) { + const resolution = await this.resolve(source, importer, { + skipSelf: true, + ...options + }) + if (resolution) { + // 跳过vue-plugin + return addQuery(resolution.id, { + raw: true + }) + } + } + }, + load (id) { + if (id === ENTRY_HELPER_CODE && mpxGlobal.entry) { + return renderEntryCode(mpxGlobal.entry, options) + } + const { resourcePath: filename, queryObj: query } = parseRequest(id) + if (!filter(filename)) return + if (!!query.resolve) { + // 强制改raw + return renderPageRouteCode(options, filename) + } + } + } +} diff --git a/packages/web-plugin/src/vite/plugins/split-package-chunk-plugin.ts b/packages/web-plugin/src/vite/plugins/split-package-chunk-plugin.ts new file mode 100644 index 0000000000..10d9fd97f3 --- /dev/null +++ b/packages/web-plugin/src/vite/plugins/split-package-chunk-plugin.ts @@ -0,0 +1,65 @@ +import { ManualChunksOption } from 'rollup' +import { Plugin } from 'vite' +import mpxGlobal from '../mpx' +import { getDescriptor } from '../utils/descriptor-cache' + +/** + * 将分包分离到额外的chunk里 + * @returns + */ +function createSplitPackageChunk () { + const manualChunksOption: ManualChunksOption = (id: string) => { + if (/plugin-vue2:normalizer/.test(id)) { + // 强制将normalizer分到vendor里去,否则会引起TDZ + return 'vendor' + } + if (mpxGlobal.entry) { + const descriptor = getDescriptor(mpxGlobal.entry) + if (descriptor) { + const { jsonConfig } = descriptor + const { subpackages = [] } = jsonConfig + for (const { root } of subpackages) { + if (root && (id.includes(root))) { + return root + } + } + } + } + } + return manualChunksOption +} + +export function createSplitPackageChunkPlugin (): Plugin { + return { + name: 'vite:mpx-split-package-chunk', + config (config) { + const output = config?.build?.rollupOptions?.output + if (output) { + const outputs = Array.isArray(output) ? output : [output] + for (const output of outputs) { + const splitPackageChunk = createSplitPackageChunk() + if (output && output.manualChunks) { + if (typeof output.manualChunks === 'function') { + const userManualChunks = output.manualChunks + output.manualChunks = (...args) => { + return userManualChunks(...args) ?? splitPackageChunk(...args) + } + } + } else { + output.manualChunks = splitPackageChunk + } + } + } else { + return { + build: { + rollupOptions: { + output: { + manualChunks: createSplitPackageChunk() + } + } + } + } + } + } + } +} diff --git a/packages/web-plugin/src/vite/plugins/wxs-plugin.ts b/packages/web-plugin/src/vite/plugins/wxs-plugin.ts new file mode 100644 index 0000000000..02ab5400db --- /dev/null +++ b/packages/web-plugin/src/vite/plugins/wxs-plugin.ts @@ -0,0 +1,21 @@ +import { createFilter, Plugin, transformWithEsbuild } from 'vite' +import { parseRequest } from '@mpxjs/compile-utils' + +/** + * wxs文件支持 + * @returns + */ +export function createWxsPlugin (): Plugin { + const filter = createFilter([/\.wxs$/]) + return { + name: 'vite:mpx-wxs', + async transform (code, id) { + const { resourcePath: filename } = parseRequest(id) + if (!filter(filename)) return + return await transformWithEsbuild(code, '', { + format: 'esm', + sourcefile: filename + }) + } + } +} diff --git a/packages/web-plugin/src/vite/transformer/json.ts b/packages/web-plugin/src/vite/transformer/json.ts new file mode 100644 index 0000000000..3944cbdd40 --- /dev/null +++ b/packages/web-plugin/src/vite/transformer/json.ts @@ -0,0 +1,35 @@ +import { TransformPluginContext } from 'rollup' +import { Options } from '../../options' +import { jsonProcess } from '../../processor/json-process' +import { jsonCompiler } from '@mpxjs/compiler' +import mpx from '../mpx' +import { SFCDescriptor } from '../utils/descriptor-cache' +import { proxyPluginContext } from '@mpxjs/plugin-proxy' + +export async function processJSON ( + descriptor: SFCDescriptor, + options: Options, + pluginContext: TransformPluginContext +): Promise { + const jsonConfig = (descriptor.jsonConfig = await jsonCompiler.parse( + descriptor, + descriptor.filename, + proxyPluginContext(pluginContext), + options + )) + try { + const jsonResult = await jsonProcess({ + jsonConfig, + pluginContext, + context: jsonConfig.path || descriptor.filename, + options, + mode: 'vite', + mpx + }) + descriptor.localPagesMap = jsonResult.localPagesMap + descriptor.localComponentsMap = jsonResult.localComponentsMap + descriptor.tabBarMap = jsonResult.tabBarMap + } catch (error) { + pluginContext.error(`[mpx] process json error: ${error}`) + } +} diff --git a/packages/web-plugin/src/vite/transformer/main.ts b/packages/web-plugin/src/vite/transformer/main.ts new file mode 100644 index 0000000000..a08b7955cd --- /dev/null +++ b/packages/web-plugin/src/vite/transformer/main.ts @@ -0,0 +1,44 @@ +import { OptionObject } from 'loader-utils' +import { TransformPluginContext, TransformResult } from 'rollup' +import { Options } from '../../options' +import { createDescriptor } from '../utils/descriptor-cache' +import { processJSON } from './json' +import { genScriptBlock, transformScript } from './script' +import { genStylesBlock } from './style' +import { genTemplateBlock } from './template' + +export async function transformMain ( + code: string, + filename: string, + query: OptionObject, + options: Options, + pluginContext: TransformPluginContext +): Promise { + const descriptor = createDescriptor(filename, code, query, options) + if (descriptor) { + // set pages/component to descriptor + await processJSON(descriptor, options, pluginContext) + // generate template block, delay transform template + const templateBlock = await genTemplateBlock( + descriptor, + options, + pluginContext + ) + // transform script + const { code, map } = await transformScript(descriptor, options, pluginContext) + // generate script block + const scriptBlock = await genScriptBlock(descriptor, code) + // generate styles block, delay transform style + const stylesBlock = await genStylesBlock(descriptor) + const vueSfc = genVueSfc(templateBlock, scriptBlock, stylesBlock) + if (query.type === 'hot') descriptor.vueSfc = vueSfc + return { + code: vueSfc, + map: map + } + } +} + +function genVueSfc (...args: { output: string }[]) { + return args.map(v => v.output).join() +} diff --git a/packages/web-plugin/src/vite/transformer/script.ts b/packages/web-plugin/src/vite/transformer/script.ts new file mode 100644 index 0000000000..cc94b95a2b --- /dev/null +++ b/packages/web-plugin/src/vite/transformer/script.ts @@ -0,0 +1,290 @@ +import { + genComponentTag, + genImport, + omit, + parseRequest, + shallowStringify, + stringify +} from '@mpxjs/compile-utils' +import { scriptSetupCompiler } from '@mpxjs/compiler' +import MagicString from 'magic-string' +import { SourceMap, TransformPluginContext } from 'rollup' +import { Options } from 'src/options' +import remapping, { SourceMapInput } from '@ampproject/remapping' +import { transformWithEsbuild } from 'vite' +import { OPTION_PROCESSOR_PATH, TAB_BAR_CONTAINER_PATH } from '../../constants' +import { resolvedConfig } from '../config' +import { + APP_HELPER_CODE, + I18N_HELPER_CODE, + TAB_BAR_PAGE_HELPER_CODE +} from '../helper' +import { setDescriptor, SFCDescriptor } from '../utils/descriptor-cache' + +export const genComponentCode = ( + varName: string, + resource: string, + { async = false } = {}, + params: unknown = {} +): string => { + if (!async) { + return `getComponent(${varName}, ${stringify(params)})` + } else { + return `() => import(${stringify(resource)}).then(${varName} => + getComponent(${varName}.default, ${stringify(params)}) + )` + } +} + +/** + * transform mpx script + * @param code - mpx script content + * @param descriptor - SFCDescriptor + * @param options - ResolvedOptions + * @param pluginContext - TransformPluginContext + * @returns script content + */ +export async function transformScript ( + descriptor: SFCDescriptor, + options: Options, + pluginContext: TransformPluginContext +): Promise<{ + code: string + map?: SourceMap +}> { + const { + id: componentId, + filename, + app, + isPage, + jsonConfig, + script, + wxsModuleMap, + wxsContentMap, + tabBarMap, + builtInComponentsMap, + genericsInfo, + localPagesMap, + localComponentsMap + } = descriptor + + if (!script?.content) { + return { + code: '' + } + } + + const mappings: SourceMapInput[] = [] + const ctorType = app ? 'app' : isPage ? 'page' : 'component' + const { i18n } = options + const componentGenerics = jsonConfig.componentGenerics + const pagesMap: Record = {} + const componentsMap: Record = {} + + if (script.setup) { + const res = scriptSetupCompiler( + script, + descriptor.app ? 'app' : descriptor.isPage ? 'page' : '', + descriptor.filename + ) + script.content = res.content + mappings.push(res.map as SourceMapInput) + } + + const s = new MagicString(script.content) + + if (script.src) { + s.prepend(`${genImport(script.src)}\n`) + const resolvedId = await pluginContext.resolve(script.src, filename) + if (resolvedId?.id) setDescriptor(resolvedId.id, descriptor) + } + + !resolvedConfig.isProduction && + s.prepend(`global.currentResource = ${stringify(filename)}\n`) + s.prepend(`global.currentModuleId = ${stringify(descriptor.id)}\n`) + + // import page by page json config + Object.keys(localPagesMap).forEach((pageName, index) => { + const pageCfg = localPagesMap[pageName] + const varName = `__mpx__page__${index}` + const isTabBar = tabBarMap && tabBarMap[pageName] + const newPagePath = isTabBar ? TAB_BAR_CONTAINER_PATH : pageCfg.resource + const async = pageCfg.async + !async && s.prepend(`${genImport(newPagePath, varName)}\n`) + pagesMap[pageName] = genComponentCode( + varName, + newPagePath, + { + async + }, + isTabBar + ? { __mpxBuiltIn: true } + : { + __mpxPageRoute: pageName + } + ) + }) + // import component by component json config + Object.keys(localComponentsMap).forEach((componentName, index) => { + const componentCfg: { + resource: string + async: boolean + } = localComponentsMap[componentName] + const componentId = componentCfg.resource + const varName = `__mpx__component__${index}` + const async = componentCfg.async + !async && s.prepend(`${genImport(componentId, varName)}\n`) + componentsMap[componentName] = genComponentCode(varName, componentId, { + async + }) + }) + + // import runtime component + Object.keys(builtInComponentsMap).forEach((componentName, index) => { + const componentCfg = builtInComponentsMap[componentName] + const varName = `__mpx__builtInComponent__${index}` + s.prepend(`${genImport(componentCfg.resource, varName)}\n`) + componentsMap[componentName] = genComponentCode( + varName, + componentCfg.resource, + {}, + { __mpxBuiltIn: true } + ) + }) + + s.prepend( + `${genImport( + OPTION_PROCESSOR_PATH, + 'processOption, { getComponent, getWxsMixin }' + )}\n` + ) + + if (i18n) { + s.prepend(`${genImport(I18N_HELPER_CODE, '')}\n`) + } + + if (app) { + s.prepend( + `${genImport(APP_HELPER_CODE)} + ${genImport(TAB_BAR_PAGE_HELPER_CODE)} + ${genImport('vue', 'Vue')} + ${genImport('vue-router', 'VueRouter')}\n` + ) + } + + // after source code + s.append('\nconst wxsModules = {}\n') + + if (wxsModuleMap) { + const wxsModuleKeys = Object.keys(wxsModuleMap) + for (let i = 0; i < wxsModuleKeys.length; i++) { + const key = wxsModuleKeys[i] + const wxsModuleId = wxsModuleMap[key] + // inline wxs module, transform to iife + if (wxsModuleId.startsWith('~')) { + const mpxWxsPath = wxsModuleId.split('!=!')[1] + const { resourcePath: filename, queryObj: query } = + parseRequest(mpxWxsPath) + const wxsContent = wxsContentMap[`${filename}~${query.wxsModule}`] + if (wxsContent) { + const varName = `__mpx__wxs__${i}` + const result = await transformWithEsbuild(wxsContent, '', { + globalName: varName, + format: 'iife' + }) + s.append(`${result.code}\n`) + s.append(`wxsModules.${key} = ${varName}\n`) + } + } else { + // wxs file, tranfrom to esm with wxsPlugin + const varName = `__mpx__wxs__${i}` + s.append(`${genImport(wxsModuleId, varName)}\n`) + s.append(`wxsModules.${key} = ${varName}\n`) + } + } + } + + s.append( + `const currentOption = global.__mpxOptionsMap[${stringify( + descriptor.id + )}]\n` + ) + + s.append( + `export default processOption({ + option: currentOption, + ctorType: ${stringify(ctorType)}, + firstPage: ${stringify(Object.keys(localPagesMap)[0])}, + outputPath: ${stringify(componentId)}, + pageConfig: ${stringify( + isPage + ? omit(jsonConfig, ['usingComponents', 'style', 'singlePage']) + : {} + )}, + pagesMap: ${shallowStringify(pagesMap)}, + componentsMap: ${shallowStringify(componentsMap)}, + tabBarMap: ${stringify(tabBarMap)}, + componentGenerics: ${stringify(componentGenerics)}, + genericsInfo: ${stringify(genericsInfo)}, + mixin: getWxsMixin(wxsModules), + ...${app ? `{ Vue: Vue, VueRouter: VueRouter }` : '{}'} + })\n` + ) + + // transform ts + if (script?.attrs.lang === 'ts' && !script.src && !script.setup) { + const result = transformWithEsbuild( + s.toString(), + filename, + { loader: 'ts' }, + s.generateMap({ + file: filename + '.map', + source: filename + }) + ) + return result + } + + let index = 0 + return { + code: s.toString(), + map: remapping( + [ + s.generateMap({ + file: filename + '.map', + source: filename + }) as SourceMapInput + ], + () => { + return mappings[index++] || null + }, + true + ) as SourceMap + } +} + +/** + * generate script block and transform script content + * @param descriptor - SFCDescriptor + * @param options - ResolvedOptions + * @param pluginContext - TransformPluginContext + */ +export async function genScriptBlock ( + descriptor: SFCDescriptor, + code: string +): Promise<{ output: string }> { + return { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + output: genComponentTag(descriptor.script!, { + attrs (script: { attrs: { src?: string; setup?: boolean } }) { + const attrs = Object.assign({}, script.attrs) + delete attrs.src + delete attrs.setup + return attrs + }, + content () { + return code + } + }) + } +} diff --git a/packages/web-plugin/src/vite/transformer/style.ts b/packages/web-plugin/src/vite/transformer/style.ts new file mode 100644 index 0000000000..c22330f540 --- /dev/null +++ b/packages/web-plugin/src/vite/transformer/style.ts @@ -0,0 +1,48 @@ +import { genComponentTag } from '@mpxjs/compile-utils' +import { styleCompiler } from '@mpxjs/compiler' +import { proxyPluginContext } from '@mpxjs/plugin-proxy' +import { TransformPluginContext } from 'rollup' +import { Options } from '../../options' +import { TransformResult } from 'vite' +import pathHash from '../../utils/path-hash' +import { SFCDescriptor } from '../utils/descriptor-cache' +import { resolvedConfig } from '../config' +import mpx from '../mpx' + +/** + * transform style + * @param code - style code + * @param filename - filename + * @param descriptor - SFCDescriptor + * @param options - ResolvedOptions + * @param pluginContext - TransformPluginContext + */ +export async function transformStyle ( + code: string, + filename: string, + descriptor: SFCDescriptor, + options: Options, + pluginContext: TransformPluginContext +): Promise { + return styleCompiler.transform(code, proxyPluginContext(pluginContext), { + sourceMap: resolvedConfig.sourceMap, + map: pluginContext.getCombinedSourcemap(), + resource: filename, + mpx: { + ...mpx, + ...options, + pathHash: pathHash, + isApp: descriptor.app + } + }) +} + +/** + * generate style block + * @param descriptor - SFCDescriptor + * @returns + */ +export function genStylesBlock (descriptor: SFCDescriptor): { output: string } { + const { styles } = descriptor + return { output: styles.map(style => genComponentTag(style)).join('\n') } +} diff --git a/packages/web-plugin/src/vite/transformer/template.ts b/packages/web-plugin/src/vite/transformer/template.ts new file mode 100644 index 0000000000..2f61aa24fa --- /dev/null +++ b/packages/web-plugin/src/vite/transformer/template.ts @@ -0,0 +1,80 @@ +import { genComponentTag } from '@mpxjs/compile-utils' +import { TransformPluginContext } from 'rollup' +import { TransformResult } from 'vite' +import { Options } from '../../options' +import { SFCDescriptor } from '../utils/descriptor-cache' +import { templateProcess } from '../../processor/template-process' + +/** + * transform mpx template to vue template + * @param code - mpx template code + * @param filename - filename + * @param descriptor - SFCDescriptor + * @param options - ResolvedOptions + * @param pluginContext - TransformPluginContext + */ + +export async function transformTemplate ( + descriptor: SFCDescriptor, + options: Options, + pluginContext?: TransformPluginContext +): Promise { + const { id, filename, jsonConfig, app, template } = descriptor + let builtInComponentsMap: SFCDescriptor['builtInComponentsMap'] = {} + let genericsInfo: SFCDescriptor['genericsInfo'] + let wxsContentMap: SFCDescriptor['wxsContentMap'] = {} + let wxsModuleMap: SFCDescriptor['wxsModuleMap'] = {} + let templateContent = '' + if (template) { + ({ + wxsModuleMap, + wxsContentMap, + genericsInfo, + builtInComponentsMap, + templateContent + } = templateProcess({ + template, + options, + pluginContext, + jsonConfig, + app, + resource: filename, + moduleId: id + })) + } + descriptor.wxsModuleMap = wxsModuleMap + descriptor.wxsContentMap = wxsContentMap + descriptor.genericsInfo = genericsInfo + descriptor.builtInComponentsMap = builtInComponentsMap + + return { + code: templateContent, + map: null + } +} + +/** + * gen template block + * @param descriptor - SFCDescriptor + * @returns + */ +export async function genTemplateBlock ( + descriptor: SFCDescriptor, + options: Options, + pluginContext?: TransformPluginContext +): Promise<{ + output: string +}> { + const templateContent = await transformTemplate( + descriptor, + options, + pluginContext + ) + return { + output: genComponentTag({ + content: templateContent?.code || '', + tag: 'template', + attrs: descriptor.template?.attrs + }) + } +} diff --git a/packages/web-plugin/src/vite/utils/descriptor-cache.ts b/packages/web-plugin/src/vite/utils/descriptor-cache.ts new file mode 100644 index 0000000000..eeaae51f0e --- /dev/null +++ b/packages/web-plugin/src/vite/utils/descriptor-cache.ts @@ -0,0 +1,147 @@ +import { CompilerResult, templateCompiler } from '@mpxjs/compiler' +import path from 'path' +import slash from 'slash' +import { Options } from '../../options' +import { JsonProcessResult } from '../../processor/json-process' +import { TemplateProcessResult } from '../../processor/template-process' +import { OptionObject } from 'loader-utils' +import pathHash from '../../utils/path-hash' +import { resolvedConfig } from '../config' + +export interface SFCDescriptor + extends CompilerResult, + Omit, + JsonProcessResult { + id: string + filename: string + app: boolean + isPage: boolean + isComponent: boolean + vueSfc?: string +} + +const cache = new Map() +const prevCache = new Map() + +function genDescriptorTemplate () { + const template: SFCDescriptor['template'] = { + tag: 'template', + type: 'template', + content: + '
', + attrs: {}, + start: 0, + end: 0 + } + return template +} + +function genDescriptorScript (descriptor: SFCDescriptor) { + const script: SFCDescriptor['script'] = { + tag: 'script', + type: 'script', + content: '', + attrs: {}, + start: 0, + end: 0 + } + if (descriptor.app) { + script.content = ` +import { createApp } from "@mpxjs/core" +createApp({})` + } + if (descriptor.isPage) { + script.content = ` +import { createPage } from "@mpxjs/core" +createPage({})` + } + if (descriptor.isComponent) { + script.content = ` +import { createComponent } from "@mpxjs/core" +createComponent({})` + } + return script +} + +export function createDescriptor ( + filename: string, + code: string, + query: OptionObject, + options: Options +): SFCDescriptor { + const { projectRoot = '', mode = 'web', defs, env } = options + const { isProduction, sourceMap } = resolvedConfig + const normalizedPath = slash( + path.normalize(path.relative(projectRoot, filename)) + ) + const isPage = !!query.isPage + const isComponent = !!query.isComponent + const compilerResult = templateCompiler.compiler.parseComponent(code, { + mode, + defs, + env, + filePath: filename, + pad: 'line', + needMap: sourceMap + }) + if (compilerResult.script && compilerResult.script.map) { + const sources = compilerResult.script.map.sources || [] + compilerResult.script.map.sources = sources.map( + (v: string) => v.split('?')[0] + ) + } + const descriptor: SFCDescriptor = { + ...compilerResult, + id: pathHash(normalizedPath + (isProduction ? code : '')), + filename, + isPage, + isComponent, + app: !(isPage || isComponent), + wxsModuleMap: {}, + wxsContentMap: {}, + builtInComponentsMap: {}, + genericsInfo: undefined, + jsonConfig: {}, + localPagesMap: {}, + localComponentsMap: {}, + tabBarMap: {} + } + if (descriptor.app) { + descriptor.template = genDescriptorTemplate() + } + if (!descriptor.script) { + descriptor.script = genDescriptorScript(descriptor) + } + setDescriptor(filename, descriptor) + return descriptor +} + +export function getPrevDescriptor (filename: string): SFCDescriptor | undefined { + return prevCache.get(filename) +} + +export function setPrevDescriptor ( + filename: string, + entry: SFCDescriptor +): void { + prevCache.set(filename, entry) +} + +export function getDescriptor ( + filename: string, + code?: string, + query?: OptionObject, + options?: Options, + createIfNotFound = true +): SFCDescriptor | undefined { + if (cache.has(filename)) { + return cache.get(filename) + } + if (createIfNotFound && code && query && options) { + return createDescriptor(filename, code, query, options) + } +} + +export function setDescriptor (filename: string, entry: SFCDescriptor): void { + cache.set(filename, entry) +} diff --git a/packages/web-plugin/src/webpack.ts b/packages/web-plugin/src/webpack.ts new file mode 100644 index 0000000000..c9f7a09671 --- /dev/null +++ b/packages/web-plugin/src/webpack.ts @@ -0,0 +1,2 @@ +export * from './webpack/index' +export { default } from './webpack/index' diff --git a/packages/web-plugin/src/webpack/index.ts b/packages/web-plugin/src/webpack/index.ts new file mode 100644 index 0000000000..0ce9e4ed08 --- /dev/null +++ b/packages/web-plugin/src/webpack/index.ts @@ -0,0 +1,720 @@ +/* eslint-disable @typescript-eslint/no-var-requires */ +'use strict' + +import { + addQuery, + matchCondition, + parseRequest, + stringify, + stringifyLoadersAndResource, + toPosix +} from '@mpxjs/compile-utils' +import CommonJsVariableDependency from '@mpxjs/webpack-plugin/lib/dependencies/CommonJsVariableDependency' +import InjectDependency from '@mpxjs/webpack-plugin/lib/dependencies/InjectDependency' +import RecordResourceMapDependency from '@mpxjs/webpack-plugin/lib/dependencies/RecordResourceMapDependency' +import RecordVueContentDependency from '@mpxjs/webpack-plugin/lib/dependencies/RecordVueContentDependency' +import ReplaceDependency from '@mpxjs/webpack-plugin/lib/dependencies/ReplaceDependency' +import ResolveDependency from '@mpxjs/webpack-plugin/lib/dependencies/ResolveDependency' +import AddEnvPlugin from '@mpxjs/webpack-plugin/lib/resolver/AddEnvPlugin' +import AddModePlugin from '@mpxjs/webpack-plugin/lib/resolver/AddModePlugin' +import async from 'async' +import { + Compiler, + DefinePlugin, + Dependency, + ExternalsPlugin, + Module, + NormalModule, + WebpackError +} from 'webpack' +import harmonySpecifierTag from 'webpack/lib/dependencies/HarmonyImportDependencyParserPlugin' +import FileSystemInfo from 'webpack/lib/FileSystemInfo' +import FlagEntryExportAsUsedPlugin from 'webpack/lib/FlagEntryExportAsUsedPlugin' +import NullFactory from 'webpack/lib/NullFactory' +import { Options, processOptions } from '../options' +import getOutputPath from '../utils/get-output-path' +import mpx, { Mpx } from './mpx' + +const styleCompilerPath = require.resolve('@mpxjs/loaders/style-loader.js') +const isProductionLikeMode = (options: { + mode?: 'production' | 'development' | 'none' | undefined +}) => { + return options.mode === 'production' || !options.mode +} + +const warnings: Array = [] +const errors: Array = [] + +class MpxWebpackPlugin { + options: Options + + constructor (options: Partial) { + options = options || {} + this.options = processOptions(options) + // Hack for buildDependencies + const rawResolveBuildDependencies = + FileSystemInfo.prototype.resolveBuildDependencies + FileSystemInfo.prototype.resolveBuildDependencies = function ( + context: string, + deps: Dependency, + rawCallback: (err: string, result: string) => void + ) { + return rawResolveBuildDependencies.call( + this, + context, + deps, + (err: string, result: string) => { + if ( + result && + typeof options.hackResolveBuildDependencies === 'function' + ){ + options.hackResolveBuildDependencies(result) + } + return rawCallback(err, result) + } + ) + } + } + + static loader (options: { [k: string]: unknown }) { + if (options.transRpx) { + warnings.push( + 'Mpx loader option [transRpx] is deprecated now, please use mpx webpack plugin config [transRpxRules] instead!' + ) + } + return { + loader: '@mpxjs/web-plugin/dist/webpack/loader/web-loader', + options + } + } + + static wxsPreLoader (options = {}) { + return { + loader: '@mpxjs/loaders/pre-loader', + options + } + } + + static urlLoader (options = {}) { + return { + loader: '@mpxjs/loaders/url-loader', + options + } + } + + static fileLoader (options = {}) { + return { + loader: '@mpxjs/loaders/file-loader', + options + } + } + + runModeRules (data: { [k: string]: any }) { + const { resourcePath, queryObj } = parseRequest(data.resource) + if (queryObj.mode) { + return + } + const mode = this.options.mode + const modeRule = this.options?.modeRules?.mode + if (!modeRule) { + return + } + if (matchCondition(resourcePath, modeRule)) { + data.resource = addQuery(data.resource, { mode }) + data.request = addQuery(data.request, { mode }) + } + } + + apply (compiler: { [k: string]: unknown } & Compiler) { + if (!compiler.__mpx__) { + compiler.__mpx__ = true + } else { + errors.push( + 'Multiple MpxWebpackPlugin instances exist in webpack compiler, please check webpack plugins config!' + ) + } + + // 将entry export标记为used且不可mangle,避免require.async生成的js chunk在生产环境下报错 + new FlagEntryExportAsUsedPlugin(true, 'entry').apply(compiler) + if (!compiler.options.node || !compiler.options.node.global) { + compiler.options.node = compiler.options.node || {} + compiler.options.node.global = true + } + + const addModePlugin = new AddModePlugin( + 'before-file', + this.options.mode || 'web', + this.options.fileConditionRules, + 'file' + ) + const addEnvPlugin = new AddEnvPlugin( + 'before-file', + this.options.env || '', + this.options.fileConditionRules, + 'file' + ) + if (Array.isArray(compiler.options.resolve.plugins)) { + compiler.options.resolve.plugins.push(addModePlugin) + } else { + compiler.options.resolve.plugins = [addModePlugin] + } + if (this.options.env) { + compiler.options.resolve.plugins.push(addEnvPlugin) + } + // 代理writeFile + if (this.options.writeMode === 'changed') { + const writedFileContentMap = new Map() + const originalWriteFile = compiler.outputFileSystem.writeFile + compiler.outputFileSystem.writeFile = ( + filePath: string, + content: any, + callback: () => void + ) => { + const lastContent = writedFileContentMap.get(filePath) + if ( + Buffer.isBuffer(lastContent) + ? lastContent.equals(content) + : lastContent === content + ) { + return callback() + } + writedFileContentMap.set(filePath, content) + originalWriteFile(filePath, content, callback) + return '' + } + } + + const defs = this.options.defs || {} + + const defsOpt: { [k: string]: any } = { + __mpx_wxs__: DefinePlugin.runtimeValue(({ module }) => { + return stringify(!!module.wxs) + }) + } + + Object.keys(defs).forEach(key => { + defsOpt[key] = stringify(defs[key]) + }) + // define mode & defs + new DefinePlugin(defsOpt).apply(compiler) + + new ExternalsPlugin('commonjs2', this.options.externals || []).apply( + compiler + ) + + compiler.hooks.compilation.tap( + 'MpxWebpackPlugin ', + (compilation, { normalModuleFactory }) => { + NormalModule.getCompilationHooks(compilation).loader.tap( + 'MpxWebpackPlugin', + (loaderContext: any) => { + // 设置loaderContext的minimize + if (isProductionLikeMode(compiler.options)) { + mpx.minimize = true + } + loaderContext.getMpx = () => { + return mpx + } + } + ) + compilation.dependencyFactories.set( + ResolveDependency, + new NullFactory() + ) + compilation.dependencyTemplates.set( + ResolveDependency, + new ResolveDependency.Template() + ) + + compilation.dependencyFactories.set( + InjectDependency, + new NullFactory() + ) + compilation.dependencyTemplates.set( + InjectDependency, + new InjectDependency.Template() + ) + + compilation.dependencyFactories.set( + ReplaceDependency, + new NullFactory() + ) + compilation.dependencyTemplates.set( + ReplaceDependency, + new ReplaceDependency.Template() + ) + compilation.dependencyFactories.set( + CommonJsVariableDependency, + normalModuleFactory + ) + compilation.dependencyTemplates.set( + CommonJsVariableDependency, + new CommonJsVariableDependency.Template() + ) + compilation.dependencyFactories.set( + RecordResourceMapDependency, + new NullFactory() + ) + compilation.dependencyTemplates.set( + RecordResourceMapDependency, + new RecordResourceMapDependency.Template() + ) + compilation.dependencyFactories.set( + RecordVueContentDependency, + new NullFactory() + ) + compilation.dependencyTemplates.set( + RecordVueContentDependency, + new RecordVueContentDependency.Template() + ) + } + ) + + compiler.hooks.thisCompilation.tap( + 'MpxWebpackPlugin', + (compilation, { normalModuleFactory }) => { + compilation.warnings = compilation.warnings.concat( + warnings + ) + compilation.errors = compilation.errors.concat(errors) + const moduleGraph = compilation.moduleGraph + if (!compilation.__mpx__) { + Object.assign(mpx, { + ...this.options, + appInfo: {}, + // pages全局记录,无需区分主包分包 + pagesMap: {}, + // 组件资源记录,依照所属包进行记录 + componentsMap: { + main: {} + }, + staticResourcesMap: { + main: {} + }, + usingComponents: {}, + currentPackageRoot: '', + wxsContentMap: {}, + minimize: false, + // 输出web专用配置 + appTitle: 'Index homepage', + vueContentCache: new Map(), + recordResourceMap: ({ + resourcePath, + resourceType, + outputPath, + packageRoot = '', + recordOnly, + warn, + error + }: { + resourcePath: string + resourceType: string + outputPath: string + packageRoot: string + recordOnly: boolean + warn: (warn?: Error | string) => void + error: (error?: Error | string) => void + }) => { + const packageName = packageRoot || 'main' + const resourceMap = mpx[`${resourceType}sMap` as keyof Mpx] + const currentResourceMap = resourceMap.main + ? (resourceMap[packageName] = resourceMap[packageName] || {}) + : resourceMap + let alreadyOutputted = false + if (outputPath) { + if ( + !currentResourceMap[resourcePath] || + currentResourceMap[resourcePath] === true + ) { + if (!recordOnly) { + // 在非recordOnly的模式下,进行输出路径冲突检测,如果存在输出路径冲突,则对输出路径进行重命名 + for (const key in currentResourceMap) { + // todo 用outputPathMap来检测输出路径冲突 + if ( + currentResourceMap[key] === outputPath && + key !== resourcePath + ) { + outputPath = + getOutputPath(resourcePath, resourceType, mpx, { + conflictPath: outputPath + }) || '' + warn && + warn( + new Error( + `Current ${resourceType} [${resourcePath}] is registered with conflicted outputPath [${currentResourceMap[key]}] which is already existed in system, will be renamed with [${outputPath}], use ?resolve to get the real outputPath!` + ) + ) + break + } + } + } + currentResourceMap[resourcePath] = outputPath + } else { + if (currentResourceMap[resourcePath] === outputPath) { + alreadyOutputted = true + } else { + error && + error( + new Error( + `Current ${resourceType} [${resourcePath}] is already registered with outputPath [${currentResourceMap[resourcePath]}], you can not register it with another outputPath [${outputPath}]!` + ) + ) + } + } + } else if (!currentResourceMap[resourcePath]) { + currentResourceMap[resourcePath] = true + } + + return { + outputPath, + alreadyOutputted + } + } + }) + compilation.__mpx__ = mpx + } + const rawProcessModuleDependencies = + compilation.processModuleDependencies + compilation.processModuleDependencies = (module, callback) => { + const presentationalDependencies = + module.presentationalDependencies || [] + async.forEach( + presentationalDependencies.filter( + (dep: Dependency & { [k: string]: any }) => dep.mpxAction + ), + (dep: Dependency & { [k: string]: any }, callback) => { + dep.mpxAction(module, compilation, callback) + }, + err => { + rawProcessModuleDependencies.call( + compilation, + module, + innerErr => { + const cbError: any = err || innerErr + return callback(cbError) + } + ) + } + ) + } + const normalModuleFactoryParserCallback = ( + parser: Record + ) => { + parser.hooks.call + .for('__mpx_resolve_path__') + .tap('MpxWebpackPlugin', (expr: Record) => { + if (expr.arguments[0]) { + const resource = expr.arguments[0].value + const packageName = mpx.currentPackageRoot || 'main' + const module: Module = parser.state.module + const moduleGraphReturn = moduleGraph.getIssuer( + module + ) as Module & { resource: any } + const resource1: string = moduleGraphReturn.resource + const issuerResource = resource1 + const range = expr.range + const dep = new ResolveDependency( + resource, + packageName, + issuerResource, + range + ) + parser.state.current.addPresentationalDependency(dep) + return true + } + }) + + // hack babel polyfill global + parser.hooks.statementIf.tap( + 'MpxWebpackPlugin', + (expr: Record) => { + if (/core-js.+microtask/.test(parser.state.module.resource)) { + if ( + expr.test.left && + (expr.test.left.name === 'Observer' || + expr.test.left.name === 'MutationObserver') + ) { + const current = parser.state.current + current.addPresentationalDependency( + new InjectDependency({ + content: 'document && ', + index: expr.test.range[0] + }) + ) + } + } + } + ) + + parser.hooks.evaluate + .for('CallExpression') + .tap('MpxWebpackPlugin', (expr: Record) => { + const current = parser.state.current + const arg0 = expr.arguments[0] + const arg1 = expr.arguments[1] + const callee = expr.callee + // todo 该逻辑在corejs3中不需要,等corejs3比较普及之后可以干掉 + if (/core-js.+global/.test(parser.state.module.resource)) { + if ( + callee.name === 'Function' && + arg0 && + arg0.value === 'return this' + ) { + current.addPresentationalDependency( + new InjectDependency({ + content: '(function() { return this })() || ', + index: expr.range[0] + }) + ) + } + } + if (/regenerator/.test(parser.state.module.resource)) { + if ( + callee.name === 'Function' && + arg0 && + arg0.value === 'r' && + arg1 && + arg1.value === 'regeneratorRuntime = r' + ) { + current.addPresentationalDependency( + new ReplaceDependency('(function () {})', expr.range) + ) + } + } + }) + + // 处理跨平台转换 + if (mpx.srcMode !== mpx.mode) { + // 处理跨平台全局对象转换 + const transGlobalObject = (expr: Record) => { + const module = parser.state.module + const current = parser.state.current + const { queryObj, resourcePath } = parseRequest(module.resource) + const localSrcMode = queryObj.mode + const globalSrcMode = mpx.srcMode + const srcMode = localSrcMode || globalSrcMode + const mode = mpx.mode + + let target + if (expr.type === 'Identifier') { + target = expr + } else if (expr.type === 'MemberExpression') { + target = expr.object + } + + if ( + !matchCondition(resourcePath, this.options.transMpxRules) || + resourcePath.indexOf('@mpxjs') !== -1 || + !target || + mode === srcMode + ) { + return + } + + const type = target.name + const name = type === 'wx' ? 'mpx' : 'createFactory' + const replaceContent = + type === 'wx' ? 'mpx' : `createFactory(${stringify(type)})` + + const dep = new ReplaceDependency(replaceContent, target.range) + current.addPresentationalDependency(dep) + + let needInject = true + for (const dep of module.dependencies) { + if ( + dep instanceof CommonJsVariableDependency && + dep.name === name + ) { + needInject = false + break + } + } + if (needInject) { + const dep = new CommonJsVariableDependency( + `@mpxjs/core/src/runtime/${name}`, + name + ) + module.addDependency(dep) + } + } + + // 转换wx全局对象 + parser.hooks.expression + .for('wx') + .tap('MpxWebpackPlugin', transGlobalObject) + + // 为跨平台api调用注入srcMode参数指导api运行时转换 + const apiBlackListMap = [ + 'createApp', + 'createPage', + 'createComponent', + 'createStore', + 'createStoreWithThis', + 'mixin', + 'injectMixins', + 'toPureObject', + 'observable', + 'watch', + 'use', + 'set', + 'remove', + 'delete', + 'setConvertRule', + 'getMixin', + 'getComputed', + 'implement' + ].reduce((map: Record, api: string) => { + map[api] = true + return map + }, {}) + + const injectSrcModeForTransApi = ( + expr: Record, + members: Array + ) => { + // members为空数组时,callee并不是memberExpression + if (!members.length) return + const callee = expr.callee + const args = expr.arguments + const name = callee.object.name + const { queryObj, resourcePath } = parseRequest( + parser.state.module.resource + ) + const localSrcMode = queryObj.mode + const globalSrcMode = mpx.srcMode + const srcMode = localSrcMode || globalSrcMode + + if ( + srcMode === globalSrcMode || + apiBlackListMap[ + callee.property.name || callee.property.value + ] || + (name !== 'mpx' && name !== 'wx') || + (name === 'wx' && + !matchCondition(resourcePath, this.options.transMpxRules)) + ){ + return + } + + const srcModeString = `__mpx_src_mode_${srcMode}__` + const dep = new InjectDependency({ + content: args.length + ? `, ${stringify(srcModeString)}` + : stringify(srcModeString), + index: expr.end - 1 + }) + parser.state.current.addPresentationalDependency(dep) + } + + parser.hooks.callMemberChain + .for(harmonySpecifierTag) + .tap('MpxWebpackPlugin', injectSrcModeForTransApi) + parser.hooks.callMemberChain + .for('mpx') + .tap('MpxWebpackPlugin', injectSrcModeForTransApi) + parser.hooks.callMemberChain + .for('wx') + .tap('MpxWebpackPlugin', injectSrcModeForTransApi) + } + } + normalModuleFactory.hooks.parser + .for('javascript/auto') + .tap('MpxWebpackPlugin', normalModuleFactoryParserCallback) + normalModuleFactory.hooks.parser + .for('javascript/dynamic') + .tap('MpxWebpackPlugin', normalModuleFactoryParserCallback) + normalModuleFactory.hooks.parser + .for('javascript/esm') + .tap('MpxWebpackPlugin', normalModuleFactoryParserCallback) + } + ) + + compiler.hooks.normalModuleFactory.tap( + 'MpxWebpackPlugin', + normalModuleFactory => { + // resolve前修改原始request + normalModuleFactory.hooks.beforeResolve.tap( + 'MpxWebpackPlugin', + data => { + const request = data.request + const { queryObj, resource } = parseRequest(request) + if (queryObj.resolve) { + // 此处的query用于将资源引用的当前包信息传递给resolveDependency + const resolveLoaderPath = '@mpxjs/loaders/resolve-loader' + data.request = `!!${resolveLoaderPath}!${resource}` + } + } + ) + // 应用过rules后,注入mpx相关资源编译loader + normalModuleFactory.hooks.afterResolve.tap( + 'MpxWebpackPlugin', + ({ createData }) => { + const { queryObj } = parseRequest(createData.request || '') + const loaders = createData.loaders + const mpxStyleOptions = queryObj.mpxStyleOptions + const firstLoader = + loaders && loaders[0] ? toPosix(loaders[0].loader) : '' + const isPitcherRequest = firstLoader.includes( + 'vue-loader/lib/loaders/pitcher' + ) + let cssLoaderIndex = -1 + let vueStyleLoaderIndex = -1 + let mpxStyleLoaderIndex = -1 + loaders && + loaders.forEach((loader, index) => { + const currentLoader = toPosix(loader.loader) + if ( + currentLoader.includes('css-loader') && + cssLoaderIndex === -1 + ) { + cssLoaderIndex = index + } else if ( + currentLoader.includes( + 'vue-loader/lib/loaders/stylePostLoader' + ) && + vueStyleLoaderIndex === -1 + ) { + vueStyleLoaderIndex = index + } else if ( + currentLoader.includes(styleCompilerPath) && + mpxStyleLoaderIndex === -1 + ) { + mpxStyleLoaderIndex = index + } + }) + if (mpxStyleLoaderIndex === -1) { + let loaderIndex = -1 + if (cssLoaderIndex > -1 && vueStyleLoaderIndex === -1) { + loaderIndex = cssLoaderIndex + } else if ( + cssLoaderIndex > -1 && + vueStyleLoaderIndex > -1 && + !isPitcherRequest + ) { + loaderIndex = vueStyleLoaderIndex + } + if (loaderIndex > -1) { + // @ts-ignore + loaders && + loaders.splice(loaderIndex + 1, 0, { + loader: styleCompilerPath, + options: + (mpxStyleOptions && JSON.parse(mpxStyleOptions)) || {} + } as any) + } + } + + createData.request = stringifyLoadersAndResource( + loaders, + createData.resource || '' + ) + // 根据用户传入的modeRules对特定资源添加mode query + this.runModeRules(createData) + } + ) + } + ) + } +} + +export default MpxWebpackPlugin diff --git a/packages/web-plugin/src/webpack/loader/web-loader.ts b/packages/web-plugin/src/webpack/loader/web-loader.ts new file mode 100644 index 0000000000..fc541f2654 --- /dev/null +++ b/packages/web-plugin/src/webpack/loader/web-loader.ts @@ -0,0 +1,230 @@ +import { templateCompiler, jsonCompiler } from '@mpxjs/compiler' +import { + addQuery, + matchCondition, + parseRequest, + getEntryName, + tsWatchRunLoaderFilter +} from '@mpxjs/compile-utils' +import RecordResourceMapDependency from '@mpxjs/webpack-plugin/lib/dependencies/RecordResourceMapDependency' +import RecordVueContentDependency from '@mpxjs/webpack-plugin/lib/dependencies/RecordVueContentDependency' +import async from 'async' +import loaderUtils from 'loader-utils' +import path from 'path' +import { MPX_APP_MODULE_ID } from '../../constants' +import mpx, { getOptions } from '../mpx' +import processJSON from '../web/process-json' +import processScript from '../web/process-script' +import processStyles from '../web/process-styles' +import processTemplate from '../web/process-template' +import pathHash from '../../utils/path-hash' +import getOutputPath from '../../utils/get-output-path' +import { Dependency, LoaderContext } from 'webpack' +import { proxyPluginContext } from '@mpxjs/plugin-proxy' + +export default function ( + this: LoaderContext, + content: string +): string | undefined { + this.cacheable() + + // 兼容处理处理ts-loader中watch-run/updateFile逻辑,直接跳过当前loader及后续的loader返回内容 + const pathExtname = path.extname(this.resourcePath) + if (!['.vue', '.mpx'].includes(pathExtname)) { + this.loaderIndex = tsWatchRunLoaderFilter(this.loaders, this.loaderIndex) + return content + } + + if (!mpx) { + return content + } + const { resourcePath, queryObj } = parseRequest(this.resource) + + const packageRoot = queryObj.packageRoot || mpx.currentPackageRoot + const packageName = packageRoot || 'main' + const pagesMap = mpx.pagesMap + const componentsMap = mpx.componentsMap[packageName] + const mode = mpx.mode + const env = mpx.env + const autoScope = matchCondition(resourcePath, mpx.autoScopeRules) + + let ctorType = 'app' + if (pagesMap[resourcePath]) { + // page + ctorType = 'page' + } else if (componentsMap[resourcePath]) { + // component + ctorType = 'component' + } + // 支持资源query传入isPage或isComponent支持页面/组件单独编译 + if (ctorType === 'app' && (queryObj.isComponent || queryObj.isPage)) { + const entryName = + getEntryName(this) || + getOutputPath( + resourcePath, + queryObj.isComponent ? 'component' : 'page', + mpx + ) || + '' + ctorType = queryObj.isComponent ? 'component' : 'page' + this._module?.addPresentationalDependency( + ( + new RecordResourceMapDependency( + resourcePath, + ctorType, + entryName, + packageRoot! + ) + ) + ) + } + + if (ctorType === 'app') { + if (!mpx.appInfo?.name) { + mpx.appInfo = { + resourcePath, + name: getEntryName(this) + } + } + } + // eslint-disable-next-line @typescript-eslint/no-this-alias + const loaderContext: any = this + const stringifyRequest = (r: string) => loaderUtils.stringifyRequest(loaderContext, r) + const filePath = this.resourcePath + const moduleId = ctorType === 'app' ? MPX_APP_MODULE_ID : 'm' + ((pathHash && pathHash(filePath)) || '') + + // 将mpx文件 分成四部分 + const parts = templateCompiler.parser(content, { + filePath, + needMap: this.sourceMap, + mode, + env + }) + + let output = '' + const callback = this.async() + + jsonCompiler + .parse( + parts, + loaderContext.context, + proxyPluginContext(loaderContext), + getOptions(), + loaderContext._compilation?.inputFileSystem + ) + .then(jsonConfig => { + let componentGenerics = {} + if (jsonConfig.componentGenerics) { + componentGenerics = Object.assign({}, jsonConfig.componentGenerics) + } + // 处理mode为web时输出vue格式文件 + if (ctorType === 'app' && !queryObj.isApp) { + const request = addQuery(this.resource, { isApp: true }) + const el = (mpx.webConfig && mpx.webConfig.el) || '#app' + output += ` +import App from ${stringifyRequest(request)} +import Vue from 'vue' +new Vue({ + el: '${el}', + render: function(h){ + return h(App) + } +})\n +` + // 直接结束loader进入parse + this.loaderIndex = -1 + return callback(null, output) + } + // 通过RecordVueContentDependency和vueContentCache确保子request不再重复生成vueContent + const cacheContent = mpx.vueContentCache && mpx.vueContentCache.get(filePath) + if (cacheContent) return callback(null, cacheContent) + return async.waterfall( + [ + (callback: (err?: Error | null, result?: any) => void) => { + async.parallel( + [ + callback => { + processTemplate( + parts.template, + { + loaderContext, + moduleId, + ctorType, + jsonConfig + }, + callback + ) + }, + callback => { + processStyles( + parts.styles, + { + ctorType, + autoScope, + moduleId + }, + callback + ) + }, + callback => { + processJSON( + jsonConfig, + { + loaderContext + }, + callback + ) + } + ], + (err, res) => { + callback(err, res) + } + ) + }, + ( + [templateRes, stylesRes, jsonRes]: any, + callback: (err?: Error | null, result?: any) => void + ) => { + output += templateRes.output + output += stylesRes.output + output += jsonRes.output + if ( + ctorType === 'app' && + jsonRes.jsonConfig.window && + jsonRes.jsonConfig.window.navigationBarTitleText + ) { + mpx.appTitle = jsonRes.jsonConfig.window.navigationBarTitleText + } + + processScript( + parts.script!, + { + loaderContext, + ctorType, + moduleId, + componentGenerics, + jsonConfig: jsonRes.jsonConfig, + outputPath: queryObj.outputPath || '', + tabBarMap: jsonRes.tabBarMap, + builtInComponentsMap: templateRes.builtInComponentsMap, + genericsInfo: templateRes.genericsInfo, + wxsModuleMap: templateRes.wxsModuleMap, + localComponentsMap: jsonRes.localComponentsMap, + localPagesMap: jsonRes.localPagesMap + }, + callback + ) + } + ], + (err, scriptRes: any) => { + if (err) return callback(err) + output += scriptRes.output + this._module && + this._module.addPresentationalDependency( + new RecordVueContentDependency(filePath, output) + ) + callback(null, output) + } + ) + }) +} diff --git a/packages/web-plugin/src/webpack/mpx.ts b/packages/web-plugin/src/webpack/mpx.ts new file mode 100644 index 0000000000..9483a64530 --- /dev/null +++ b/packages/web-plugin/src/webpack/mpx.ts @@ -0,0 +1,34 @@ +import pick from 'lodash/pick' +import { Options, optionKeys } from '../options' + +export type Mpx = { + appInfo?: Record + pagesMap: any + componentsMap: any + usingComponents?: Record + currentPackageRoot?: string + wxsContentMap?: any + minimize?: boolean + staticResourcesMap?: Record + vueContentCache?: Map + appTitle?: string + recordResourceMap?(record: { + resourcePath: string + resourceType: 'page' | 'component' + outputPath: string + packageRoot: string + recordOnly: boolean + warn(e: Error): void + error(e: Error): void + }): void +} + +export type MpxWithOptions = Mpx & Options + +const mpx: MpxWithOptions = {} as MpxWithOptions + +export function getOptions (): Options { + return pick(mpx, optionKeys) +} + +export default mpx diff --git a/packages/web-plugin/src/webpack/web/process-json.ts b/packages/web-plugin/src/webpack/web/process-json.ts new file mode 100644 index 0000000000..cd5729e6fa --- /dev/null +++ b/packages/web-plugin/src/webpack/web/process-json.ts @@ -0,0 +1,42 @@ +import { LoaderContext } from 'webpack' +import { jsonProcess } from '../../processor/json-process' +import { JsonConfig } from '@mpxjs/compiler' +import mpx, { getOptions } from '../mpx' + +export default async function ( + jsonConfig: JsonConfig, + { + loaderContext + }: { + loaderContext: LoaderContext + }, + rawCallback: (err?: Error | null, result?: any) => void +) { + const output = '/* json */\n' + let localPagesMap = {} + let localComponentsMap = {} + let tabBarMap = {} + + const context = loaderContext.context + + const callback = (err?: Error) => { + return rawCallback(err, { + output, + jsonConfig, + localPagesMap, + localComponentsMap, + tabBarMap + }) + } + + ({ jsonConfig, localPagesMap, localComponentsMap, tabBarMap } = + await jsonProcess({ + jsonConfig, + pluginContext: loaderContext, + context, + options: getOptions(), + mode: 'webpack', + mpx + })) + callback() +} diff --git a/packages/web-plugin/src/webpack/web/process-script.ts b/packages/web-plugin/src/webpack/web/process-script.ts new file mode 100644 index 0000000000..163f278f60 --- /dev/null +++ b/packages/web-plugin/src/webpack/web/process-script.ts @@ -0,0 +1,335 @@ +import { + addQuery, + createHelpers, + genComponentTag, + genImport, + isUrlRequest, + shallowStringify, + stringify +} from '@mpxjs/compile-utils' +import { CompilerResult, JsonConfig } from '@mpxjs/compiler' +import { proxyPluginContext } from '@mpxjs/plugin-proxy' +import { + stringifyRequest as _stringifyRequest, + urlToRequest +} from 'loader-utils' +import MagicString from 'magic-string' +import { LoaderContext } from 'webpack' +import { Options } from '../../options' +import { JsonProcessResult } from '../../processor/json-process' +import { TemplateProcessResult } from '../../processor/template-process' +import mpx, { getOptions } from '../mpx' + +const optionProcessorPath = '@mpxjs/web-plugin/src/runtime/option-processor' +const tabBarContainerPath = '@mpxjs/web-plugin/src/runtime/components/web/mpx-tab-bar-container.vue' +const tabBarPath = '@mpxjs/web-plugin/src/runtime/components/web/mpx-tab-bar.vue' + +function getAsyncChunkName (chunkName: boolean | string) { + if (chunkName && typeof chunkName !== 'boolean') { + return `/* webpackChunkName: "${chunkName}" */` + } + return '' +} + +export default function ( + script: CompilerResult['script'] | null, + { + loaderContext, + ctorType, + moduleId, + componentGenerics, + jsonConfig, + outputPath, + tabBarMap, + builtInComponentsMap, + genericsInfo, + wxsModuleMap, + localComponentsMap, + localPagesMap + }: { + loaderContext: LoaderContext | any + moduleId: string + ctorType: string + outputPath: string + componentGenerics: JsonConfig['componentGenerics'] + tabBarMap: JsonProcessResult['tabBarMap'] + jsonConfig: JsonConfig + builtInComponentsMap: TemplateProcessResult['builtInComponentsMap'] + genericsInfo: TemplateProcessResult['genericsInfo'] + wxsModuleMap: TemplateProcessResult['wxsModuleMap'] + localComponentsMap: JsonProcessResult['localPagesMap'] + localPagesMap: JsonProcessResult['localPagesMap'] + }, + callback: (err?: Error | null, result?: any) => void +) { + const { i18n, projectRoot, webConfig = {}, appInfo, srcMode, minimize } = mpx + + const mpxPluginContext = proxyPluginContext(loaderContext) + const { getRequire } = createHelpers(loaderContext) + const tabBar = jsonConfig.tabBar + const tabBarPagesMap: Record = {} + const isProduction = minimize || process.env.NODE_ENV === 'production' + const stringifyRequest = (r: string) => _stringifyRequest(loaderContext, r) + const genComponentCode = ( + resource: string, + { async = false } = {}, + params = {} + ) => { + const resourceRequest = stringifyRequest(resource) + if (!async) { + return `getComponent(require(${resourceRequest}), ${stringify(params)})` + } else { + return `()=>import(${getAsyncChunkName( + async + )}${resourceRequest}).then(res => getComponent(res, ${stringify( + params + )}))` + } + } + + if (tabBar && tabBarMap) { + // 挂载tabBar组件 + const tabBarRequest = addQuery( + tabBar.custom ? './custom-tab-bar/index' : tabBarPath, + { isComponent: true } + ) + tabBarPagesMap['mpx-tab-bar'] = genComponentCode(tabBarRequest) + // 挂载tabBar页面 + Object.keys(tabBarMap).forEach(pagePath => { + const pageCfg = localPagesMap[pagePath] + const { resource, async } = pageCfg + if (pageCfg) { + tabBarPagesMap[pagePath] = genComponentCode( + resource, + { async }, + { __mpxPageRoute: stringify(pagePath) } + ) + } else { + mpxPluginContext.warn( + new Error( + `[script processor][${loaderContext.resource}]: TabBar page path ${pagePath} is not exist in local page map, please check!` + ) + ) + } + }) + } + + let output = '/* script */\n' + + let scriptSrcMode = srcMode + if (script) { + scriptSrcMode = script.mode || scriptSrcMode + } else { + script = { + tag: 'script', + type: 'script', + content: '', + attrs: {}, + start: 0, + end: 0 + } + } + // @ts-ignore + output += genComponentTag(script, { + attrs (script: { attrs: { src?: string; setup?: boolean } }) { + const attrs = Object.assign({}, script.attrs) + // src改为内联require,删除 + delete attrs.src + // script setup通过mpx处理,删除该属性避免vue报错 + delete attrs.setup + return attrs + }, + content: function (script: { content?: string }) { + const content = new MagicString( + `\n import processOption, { getComponent, getWxsMixin } from ${stringifyRequest( + optionProcessorPath + )}\n` + ) + // add import + if (ctorType === 'app') { + content.append( + `${genImport('@mpxjs/web-plugin/src/runtime/base.styl')} + ${genImport('vue', 'Vue')} + ${genImport('vue-router', 'VueRouter')} + ${genImport('@mpxjs/core', 'Mpx')} + Vue.use(VueRouter) + global.getApp = function(){} + global.getCurrentPages = function(){ + if(!global.__mpxRouter) return [] + // @ts-ignore + return global.__mpxRouter.stack.map(item => { + let page + const vnode = item.vnode + if(vnode && vnode.componentInstance) { + page = vnode.tag.endsWith('mpx-tab-bar-container') ? vnode.componentInstance.$refs.tabBarPage : vnode.componentInstance + } + return page || { route: item.path.slice(1) } + }) + } + global.__networkTimeout = ${stringify(jsonConfig.networkTimeout)} + global.__mpxGenericsMap = {} + global.__mpxOptionsMap = {} + global.__style = ${stringify(jsonConfig.style || 'v1')} + global.__mpxPageConfig = ${stringify(jsonConfig.window)} + global.__mpxTransRpxFn = ${webConfig.transRpxFn}\n` + ) + if (i18n) { + const i18nObj = Object.assign({}, i18n) + content.append( + `${genImport('vue-i18n', 'VueI18n')} + import { createI18n } from 'vue-i18n-bridge' + Vue.use(VueI18n , { bridge: true })\n + ` + ) + const requestObj: Record = {} + const i18nKeys = ['messages', 'dateTimeFormats', 'numberFormats'] + i18nKeys.forEach(key => { + const i18nKey = `${key}Path` as keyof Options['i18n'] + if (i18nObj[i18nKey]) { + requestObj[key] = stringifyRequest(i18nObj[i18nKey]) + delete i18nObj[i18nKey] + } + }) + content.append(` const i18nCfg = ${stringify(i18nObj)}\n`) + Object.keys(requestObj).forEach(key => { + content.append(` i18nCfg.${key} = require(${requestObj[key]})\n`) + }) + content.append(' i18nCfg.legacy = false\n') + content.append(` const i18n = createI18n(i18nCfg, VueI18n) + Vue.use(i18n) + Mpx.i18n = i18n + \n`) + } + } + let hasApp = true + if (!appInfo || !appInfo.name) { + hasApp = false + } + // 注入wxs模块 + content.append(' const wxsModules = {}\n') + if (wxsModuleMap) { + Object.keys(wxsModuleMap).forEach(module => { + const src = urlToRequest(wxsModuleMap[module], projectRoot) + content.append( + ` wxsModules.${module} = require(${stringifyRequest(src)})\n` + ) + }) + } + const pagesMap: Record = {} + const componentsMap: Record = {} + Object.keys(localPagesMap).forEach(pagePath => { + const pageCfg = localPagesMap[pagePath] + const { resource, async } = pageCfg + const isTabBar = tabBarMap && tabBarMap[pagePath] + pagesMap[pagePath] = genComponentCode( + isTabBar ? tabBarContainerPath : resource, + { + async: isTabBar ? false : async + }, + isTabBar + ? { __mpxBuiltIn: true } + : { __mpxPageRoute: stringify(pagePath) } + ) + }) + Object.keys(localComponentsMap).forEach(componentName => { + const componentCfg = localComponentsMap[componentName] + const { resource, async } = componentCfg + componentsMap[componentName] = genComponentCode(resource, { async }) + }) + + Object.keys(builtInComponentsMap).forEach(componentName => { + const componentCfg = builtInComponentsMap[componentName] + componentsMap[componentName] = genComponentCode( + componentCfg.resource, + { async: false }, + { __mpxBuiltIn: true } + ) + }) + content.append( + ` global.currentModuleId = ${stringify(moduleId)}\n + global.currentSrcMode = ${stringify(scriptSrcMode)}\n + ` + ) + if (!isProduction) { + content.append( + ` global.currentResource = ${stringify( + loaderContext.resourcePath + )}\n` + ) + } + content.append(' /** script content **/\n') + // 传递ctorType以补全js内容 + const extraOptions = { ctorType } + // todo 仅靠vueContentCache保障模块唯一性还是不够严谨,后续需要考虑去除原始query后构建request + // createApp/Page/Component执行完成后立刻获取当前的option并暂存 + content.append( + ` ${getRequire('script', script, extraOptions)}\n + const currentOption = global.__mpxOptionsMap[${stringify(moduleId)}]\n + ` + ) + // 获取pageConfig + const pageConfig: Record> = {} + if (ctorType === 'page') { + const uselessOptions = new Set([ + 'usingComponents', + 'style', + 'singlePage' + ]) + Object.keys(jsonConfig) + .filter(key => !uselessOptions.has(key)) + .forEach(key => { + // @ts-ignore + pageConfig[key] = jsonConfig[key] + }) + } + // 为了执行顺序正确,tabBarPagesMap在app逻辑执行完成后注入,保障小程序中app->page->component的js执行顺序 + let tabBarStr = stringify(jsonConfig.tabBar) + if (tabBarStr && tabBarPagesMap) { + tabBarStr = tabBarStr.replace( + /"(iconPath|selectedIconPath)":"([^"]+)"/g, + function (matched, $1, $2) { + // vite 引用本地路径无法识别 + if (isUrlRequest($2, projectRoot, getOptions().externals)) { + return `"${$1}":require(${stringifyRequest( + urlToRequest($2, projectRoot) + )})` + } + return matched + } + ) + content.append( + ` global.__tabBar = ${tabBarStr} + Vue.observable(global.__tabBar) + // @ts-ignore + global.__tabBarPagesMap = ${shallowStringify(tabBarPagesMap)}\n + ` + ) + } + + // 配置平台转换通过createFactory在core中convertor中定义和进行 + // 通过processOption进行组件注册和路由注入 + content.append(` export default processOption({ + option: currentOption, + ctorType: ${stringify(ctorType)}, + firstPage: ${stringify(Object.keys(localPagesMap)[0])}, + outputPath: ${stringify(outputPath)}, + pageConfig: ${stringify(pageConfig)}, + // @ts-ignore + pagesMap: ${shallowStringify(pagesMap)}, + // @ts-ignore + componentsMap: ${shallowStringify(componentsMap)}, + tabBarMap: ${stringify(tabBarMap)}, + componentGenerics: ${stringify(componentGenerics)}, + genericsInfo: ${stringify(genericsInfo)}, + mixin: getWxsMixin(wxsModules), + hasApp: ${hasApp} + ${ctorType === 'app' ? ',Vue, VueRouter' : ''} + })`) + return content.toString() + } + }) + output += '\n' + callback(null, { + output + }) +} diff --git a/packages/web-plugin/src/webpack/web/process-styles.ts b/packages/web-plugin/src/webpack/web/process-styles.ts new file mode 100644 index 0000000000..13a36aa18a --- /dev/null +++ b/packages/web-plugin/src/webpack/web/process-styles.ts @@ -0,0 +1,30 @@ +import { genComponentTag, stringify } from '@mpxjs/compile-utils' + +export default function ( + styles: Array<{ content: string; tag: string; attrs: Record }>, + options: { autoScope?: boolean; moduleId?: string; ctorType: string }, + callback: (err?: Error | null, result?: Record) => void +) { + let output = '/* styles */\n' + if (styles.length) { + styles.forEach(style => { + output += genComponentTag(style, { + attrs (style: { attrs: Record }) { + const attrs = Object.assign({}, style.attrs) + if (options.autoScope) attrs.scoped = true + attrs.mpxStyleOptions = stringify({ + // scoped: !!options.autoScope, + // query中包含module字符串会被新版vue-cli中的默认rules当做css-module处理 + mid: options.moduleId + }) + return attrs + } + }) + output += '\n' + }) + output += '\n' + } + callback(null, { + output + }) +} diff --git a/packages/web-plugin/src/webpack/web/process-template.ts b/packages/web-plugin/src/webpack/web/process-template.ts new file mode 100644 index 0000000000..b609f63ee3 --- /dev/null +++ b/packages/web-plugin/src/webpack/web/process-template.ts @@ -0,0 +1,87 @@ +import { genComponentTag, parseRequest } from '@mpxjs/compile-utils' +import { CompilerResult, JsonConfig } from '@mpxjs/compiler' +import { LoaderContext } from 'webpack' +import { templateProcess } from '../../processor/template-process' +import mpx, { getOptions } from '../mpx' + +export default function ( + template: CompilerResult['template'], + { + loaderContext, + moduleId, + ctorType, + jsonConfig + }: { + loaderContext: LoaderContext + moduleId: string + ctorType: string + jsonConfig: JsonConfig + }, + callback: (err?: Error | null, result?: any) => void +) { + const { resourcePath } = parseRequest(loaderContext.resource) + let builtInComponentsMap = {} + let wxsModuleMap + let genericsInfo + let templateContent + let wxsContentMap + let output = '/* template */\n' + const app = ctorType === 'app' + + if (app) { + template = { + type: 'template', + attrs: {}, + tag: 'template', + content: + '
' + } + } + if (template) { + // 由于远端src template资源引用的相对路径可能发生变化,暂时不支持。 + if (template.src) { + return callback( + new Error( + '[mpx loader][' + + loaderContext.resource + + ']: ' + + 'template content must be inline in .mpx files!' + ) + ) + } + if (template.lang) { + return callback( + new Error( + '[mpx loader][' + + loaderContext.resource + + ']: ' + + 'template lang is not supported in trans web mode temporarily, we will support it in the future!' + ) + ) + } + ({ + wxsModuleMap, + genericsInfo, + builtInComponentsMap, + templateContent, + wxsContentMap + } = templateProcess({ + template, + options: getOptions(), + pluginContext: loaderContext, + jsonConfig, + app, + resource: resourcePath, + moduleId + })) + Object.assign(mpx.wxsContentMap, wxsContentMap) + template.content = templateContent + output += `${genComponentTag(template)}\n\n` + } + callback(null, { + output, + builtInComponentsMap, + genericsInfo, + wxsModuleMap + }) +} diff --git a/packages/web-plugin/tsconfig.json b/packages/web-plugin/tsconfig.json new file mode 100644 index 0000000000..1cca5ad9f2 --- /dev/null +++ b/packages/web-plugin/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.root.json", + "compilerOptions": { + "baseUrl": ".", + "declaration": true, + "outDir": "dist", + "module": "commonjs" + }, + "include": ["src/**/*"], + "exclude": ["node_modules"] +} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000000..0249556127 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - 'packages/*' + - 'examples/**' diff --git a/test/e2e/miniprogram-project/package.json b/test/e2e/miniprogram-project/package.json index 86eb70131c..c5f2f4d353 100644 --- a/test/e2e/miniprogram-project/package.json +++ b/test/e2e/miniprogram-project/package.json @@ -24,7 +24,7 @@ "license": "ISC", "dependencies": { "@mpxjs/api-proxy": "^2.7.1", - "vue": "^2.6.10", + "vue": "~2.6.10", "vue-i18n": "^8.15.3", "@mpxjs/core": "^2.7.2" }, @@ -36,7 +36,7 @@ "@babel/core": "^7.10.4", "@babel/plugin-transform-runtime": "^7.10.4", "@babel/preset-env": "^7.10.4", - "@babel/runtime-corejs3": "^7.10.4", + "@babel/runtime-corejs3": "^7.20.13", "@mpxjs/miniprogram-simulate": "1.4.9", "@mpxjs/mpx-jest": "0.0.16", "@mpxjs/webpack-plugin": "^2.7.2", @@ -79,7 +79,7 @@ "vue-loader": "^15.9.3", "vue-router": "^3.1.3", "vue-style-loader": "^4.1.2", - "vue-template-compiler": "^2.6.10", + "vue-template-compiler": "~2.6.10", "webpack": "^5.48.0", "webpack-bundle-analyzer": "^4.5.0", "webpack-cli": "^4.9.2", diff --git a/test/e2e/miniprogram-wxss-loader/package.json b/test/e2e/miniprogram-wxss-loader/package.json index 7fa4cedc20..daa6ca3ace 100644 --- a/test/e2e/miniprogram-wxss-loader/package.json +++ b/test/e2e/miniprogram-wxss-loader/package.json @@ -38,7 +38,7 @@ "@babel/core": "^7.10.4", "@babel/plugin-transform-runtime": "^7.10.4", "@babel/preset-env": "^7.10.4", - "@babel/runtime-corejs3": "^7.10.4", + "@babel/runtime-corejs3": "^7.20.13", "@mpxjs/miniprogram-simulate": "1.4.9", "@mpxjs/mpx-jest": "0.0.16", "@mpxjs/webpack-plugin": "^2.7.55", diff --git a/test/e2e/plugin-project/package.json b/test/e2e/plugin-project/package.json index c458781bb3..ded56c019e 100644 --- a/test/e2e/plugin-project/package.json +++ b/test/e2e/plugin-project/package.json @@ -23,7 +23,7 @@ "license": "ISC", "dependencies": { "@mpxjs/api-proxy": "^2.7.1", - "vue": "^2.6.10", + "vue": "~2.6.10", "vue-i18n": "^8.15.3", "@mpxjs/core": "^2.7.2" }, @@ -37,7 +37,7 @@ "@babel/plugin-transform-runtime": "^7.10.4", "@babel/plugin-transform-typescript": "^7.16.1", "@babel/preset-env": "^7.10.4", - "@babel/runtime-corejs3": "^7.10.4", + "@babel/runtime-corejs3": "^7.20.13", "@mpxjs/miniprogram-simulate": "1.4.9", "@mpxjs/mpx-jest": "0.0.16", "@mpxjs/webpack-plugin": "^2.7.2", @@ -75,7 +75,7 @@ "vue-loader": "^15.9.3", "vue-router": "^3.1.3", "vue-style-loader": "^4.1.2", - "vue-template-compiler": "^2.6.10", + "vue-template-compiler": "~2.6.10", "webpack": "^5.43.0", "webpack-bundle-analyzer": "^4.5.0", "webpack-merge": "^5.8.0" diff --git a/tsconfig.root.json b/tsconfig.root.json new file mode 100644 index 0000000000..74092c3d1a --- /dev/null +++ b/tsconfig.root.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "allowJs": true, + "moduleResolution": "node", + "strict": true, + "sourceMap": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitOverride": true, + "esModuleInterop": true, + "skipLibCheck": true, + "removeComments": false, + "target": "ES2015" + } +}