diff --git a/packages/uni-app-vite/dist/plugin/index.js b/packages/uni-app-vite/dist/plugin/index.js index 1b74875407a5ebeac688217b858fd7901566cd29..9013e397604cc84b4f1e685c2f1671904e9295c5 100644 --- a/packages/uni-app-vite/dist/plugin/index.js +++ b/packages/uni-app-vite/dist/plugin/index.js @@ -7,7 +7,7 @@ const build_1 = require("./build"); const configResolved_1 = require("./configResolved"); exports.UniAppPlugin = { name: 'vite:uni-app', - uni: uni_1.uniOptions, + uni: uni_1.uniOptions(), config() { return { build: build_1.buildOptions(), diff --git a/packages/uni-app-vite/dist/plugin/uni.js b/packages/uni-app-vite/dist/plugin/uni.js index 408edc33e11d03a5c3ee8fb833134ce54316e127..cee00b5432489dd6be7270050dd7f730fd40c7d1 100644 --- a/packages/uni-app-vite/dist/plugin/uni.js +++ b/packages/uni-app-vite/dist/plugin/uni.js @@ -1,14 +1,30 @@ "use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; Object.defineProperty(exports, "__esModule", { value: true }); exports.uniOptions = void 0; +const path_1 = __importDefault(require("path")); const uni_shared_1 = require("@dcloudio/uni-shared"); -exports.uniOptions = { - copyOptions: { assets: ['hybrid/html'] }, - compilerOptions: { - isNativeTag: uni_shared_1.isServiceNativeTag, - isCustomElement: uni_shared_1.isServiceCustomElement, - }, - transformEvent: { - tap: 'click', - }, -}; +const uni_cli_shared_1 = require("@dcloudio/uni-cli-shared"); +function uniOptions() { + return { + copyOptions: { + assets: ['hybrid/html'], + targets: [ + { + src: uni_cli_shared_1.normalizePath(path_1.default.resolve(process.env.UNI_INPUT_DIR, 'androidPrivacy.json')), + dest: process.env.UNI_OUTPUT_DIR, + }, + ], + }, + compilerOptions: { + isNativeTag: uni_shared_1.isServiceNativeTag, + isCustomElement: uni_shared_1.isServiceCustomElement, + }, + transformEvent: { + tap: 'click', + }, + }; +} +exports.uniOptions = uniOptions; diff --git a/packages/uni-app-vite/src/plugin/index.ts b/packages/uni-app-vite/src/plugin/index.ts index 0a17c892ad6fbc6a7b81d0e411521cfffada2b3d..3e0f73a87d196946571c527e75a4f1c59f29a297 100644 --- a/packages/uni-app-vite/src/plugin/index.ts +++ b/packages/uni-app-vite/src/plugin/index.ts @@ -6,7 +6,7 @@ import { configResolved } from './configResolved' export const UniAppPlugin: UniVitePlugin = { name: 'vite:uni-app', - uni: uniOptions, + uni: uniOptions(), config() { return { build: buildOptions(), diff --git a/packages/uni-app-vite/src/plugin/uni.ts b/packages/uni-app-vite/src/plugin/uni.ts index f718621e742e76cea1cbc108fa3ccfb2a8e8af5a..f3b1e5d3420162cfeb4c1530680940ce027e250e 100644 --- a/packages/uni-app-vite/src/plugin/uni.ts +++ b/packages/uni-app-vite/src/plugin/uni.ts @@ -1,16 +1,29 @@ +import path from 'path' import { isServiceNativeTag, isServiceCustomElement, } from '@dcloudio/uni-shared' -import { UniVitePlugin } from '@dcloudio/uni-cli-shared' +import { normalizePath, UniVitePlugin } from '@dcloudio/uni-cli-shared' -export const uniOptions: UniVitePlugin['uni'] = { - copyOptions: { assets: ['hybrid/html'] }, - compilerOptions: { - isNativeTag: isServiceNativeTag, - isCustomElement: isServiceCustomElement, - }, - transformEvent: { - tap: 'click', - }, +export function uniOptions(): UniVitePlugin['uni'] { + return { + copyOptions: { + assets: ['hybrid/html'], + targets: [ + { + src: normalizePath( + path.resolve(process.env.UNI_INPUT_DIR, 'androidPrivacy.json') + ), + dest: process.env.UNI_OUTPUT_DIR, + }, + ], + }, + compilerOptions: { + isNativeTag: isServiceNativeTag, + isCustomElement: isServiceCustomElement, + }, + transformEvent: { + tap: 'click', + }, + } } diff --git a/packages/uni-cli-shared/__tests__/stylePluginScoped.spec.ts b/packages/uni-cli-shared/__tests__/stylePluginScoped.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..96b3f5ed8ca9852d8947ee264b13d5c372f41c56 --- /dev/null +++ b/packages/uni-cli-shared/__tests__/stylePluginScoped.spec.ts @@ -0,0 +1,55 @@ +import postcss, { ProcessOptions } from 'postcss' + +import scopedPlugin from '../src/postcss/plugins/stylePluginScoped' + +const styleCode = `{color:red;}` +const deepSelectors = [ + ['>>> a', ' a', ':deep(a)'], + ['a >>> b', 'a b', 'a :deep(b)'], + ['a >>> b>c', 'a b>c', 'a :deep(b>c)'], + ['>>> a b', ' a b', ':deep(a b)'], + ['/deep/ a', ' a', ':deep(a)'], + ['a /deep/ b', 'a b', 'a :deep(b)'], + ['a /deep/ b>c', 'a b>c', 'a :deep(b>c)'], + ['/deep/ a b', ' a b', ':deep(a b)'], + ['::v-deep a', ' a', ':deep(a)'], + ['a ::v-deep b', 'a b', 'a :deep(b)'], + ['a ::v-deep b>c', 'a b>c', 'a :deep(b>c)'], + ['::v-deep a b', ' a b', ':deep(a b)'], + ['::v-deep a b,::v-deep a', ' a b, a', ':deep(a b),:deep(a)'], +] +const removeDeepCssCodes = Object.create(null) +const rewriteDeepCssCodes = Object.create(null) +deepSelectors.forEach(([selector, removeDeepSelector, rewriteDeepSelector]) => { + removeDeepCssCodes[selector + styleCode] = removeDeepSelector + styleCode + rewriteDeepCssCodes[selector + styleCode] = rewriteDeepSelector + styleCode +}) + +const processor = postcss([scopedPlugin]) + +const processorWithVueSfcScoped = postcss([ + scopedPlugin, + { + postcssPlugin: 'vue-sfc-scoped', + }, +]) + +const options: ProcessOptions = { from: 'a.css', map: false } +describe('cssScoped', () => { + Object.keys(removeDeepCssCodes).forEach((cssCode) => { + test('remove ' + cssCode, async () => { + return processor.process(cssCode, options).then((result) => { + expect(result.css).toBe(removeDeepCssCodes[cssCode]) + }) + }) + }) + Object.keys(rewriteDeepCssCodes).forEach((cssCode) => { + test('rewrite ' + cssCode, async () => { + return processorWithVueSfcScoped + .process(cssCode, options) + .then((result) => { + expect(result.css).toBe(rewriteDeepCssCodes[cssCode]) + }) + }) + }) +}) diff --git a/packages/uni-cli-shared/src/index.ts b/packages/uni-cli-shared/src/index.ts index 0ad5c1d5906bfdf5a5bfa8b1efc46c7106dba700..44cedbaf74837351fbbcf0c889a93953bc76864e 100644 --- a/packages/uni-cli-shared/src/index.ts +++ b/packages/uni-cli-shared/src/index.ts @@ -10,4 +10,6 @@ export * from './utils' export * from './easycom' export * from './constants' export * from './preprocess' +export * from './postcss' + export { checkUpdate } from './checkUpdate' diff --git a/packages/uni-cli-shared/src/postcss/index.ts b/packages/uni-cli-shared/src/postcss/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..fb49749e95199babce9bb05f8cdb17d487acf10d --- /dev/null +++ b/packages/uni-cli-shared/src/postcss/index.ts @@ -0,0 +1,2 @@ +import uniPostcssScopedPlugin from './plugins/stylePluginScoped' +export { uniPostcssScopedPlugin } diff --git a/packages/uni-cli-shared/src/postcss/plugins/stylePluginScoped.ts b/packages/uni-cli-shared/src/postcss/plugins/stylePluginScoped.ts new file mode 100644 index 0000000000000000000000000000000000000000..2968eac469ac08b0f3271d9b383d4da5e534b194 --- /dev/null +++ b/packages/uni-cli-shared/src/postcss/plugins/stylePluginScoped.ts @@ -0,0 +1,196 @@ +import { Plugin, PluginCreator, Rule, AtRule } from 'postcss' +import selectorParser from 'postcss-selector-parser' + +const scopedPlugin: PluginCreator = () => { + return { + postcssPlugin: 'uni-sfc-scoped', + prepare({ processor: { plugins } }) { + const hasVueSfcScoped = !!plugins.find( + (plugin) => (plugin as Plugin).postcssPlugin === 'vue-sfc-scoped' + ) + return { + Rule(rule) { + processRule(rule, hasVueSfcScoped) + }, + } + }, + } +} + +const processedRules = new WeakSet() + +function processRule(rule: Rule, hasVueSfcScoped: boolean) { + if ( + processedRules.has(rule) || + (rule.parent && + rule.parent.type === 'atrule' && + /-?keyframes$/.test((rule.parent as AtRule).name)) + ) { + return + } + processedRules.add(rule) + rule.selector = selectorParser((selectorRoot) => { + selectorRoot.each((selector) => { + hasVueSfcScoped + ? rewriteDeprecatedSelector(selector) + : rewriteSelector(selector, selectorRoot) + }) + }).processSync(rule.selector) +} + +/** + * @param selector + * @returns + */ +function rewriteDeprecatedSelector(selector: selectorParser.Selector) { + const nodes: selectorParser.Node[] = [] + let deepNode: selectorParser.Pseudo | selectorParser.Combinator | undefined + selector.each((n) => { + if (deepNode) { + nodes.push(n) + selector.removeChild(n) + } else { + const { type, value } = n + if (type === 'pseudo' && value === '::v-deep') { + deepNode = n as selectorParser.Pseudo + } else if ( + type === 'combinator' && + (value === '>>>' || value === '/deep/') + ) { + deepNode = n as selectorParser.Combinator + } + } + }) + if (!deepNode) { + return + } + if (deepNode.type === 'combinator') { + const index = selector.index(deepNode) + if (index > 0) { + selector.insertBefore(deepNode, selectorParser.combinator({ value: ' ' })) + } + } + // remove first combinator + // ::v-deep a{color:red;} => :deep(a){color:red;} + const firstNode = nodes[0] + if (firstNode && firstNode.type === 'combinator' && firstNode.value === ' ') { + nodes.shift() + } + selector.insertBefore( + deepNode, + selectorParser.pseudo({ + value: ':deep', + nodes: [selectorParser.selector({ value: '', nodes })], + }) + ) + selector.removeChild(deepNode) +} + +function rewriteSelector( + selector: selectorParser.Selector, + selectorRoot: selectorParser.Root +) { + let node: selectorParser.Node | null = null + // find the last child node to insert attribute selector + selector.each((n) => { + // DEPRECATED ">>>" and "/deep/" combinator + if ( + n.type === 'combinator' && + (n.value === '>>>' || n.value === '/deep/') + ) { + n.value = ' ' + n.spaces.before = n.spaces.after = '' + // warn( + // `the >>> and /deep/ combinators have been deprecated. ` + + // `Use :deep() instead.` + // ) + return false + } + + if (n.type === 'pseudo') { + const { value } = n + // deep: inject [id] attribute at the node before the ::v-deep + // combinator. + if (value === ':deep' || value === '::v-deep') { + if (n.nodes.length) { + // .foo ::v-deep(.bar) -> .foo[xxxxxxx] .bar + // replace the current node with ::v-deep's inner selector + let last: selectorParser.Selector['nodes'][0] = n + n.nodes[0].each((ss) => { + selector.insertAfter(last, ss) + last = ss + }) + // insert a space combinator before if it doesn't already have one + const prev = selector.at(selector.index(n) - 1) + if (!prev || !isSpaceCombinator(prev)) { + selector.insertAfter( + n, + selectorParser.combinator({ + value: ' ', + }) + ) + } + selector.removeChild(n) + } else { + // DEPRECATED usage + // .foo ::v-deep .bar -> .foo[xxxxxxx] .bar + // warn( + // `::v-deep usage as a combinator has ` + + // `been deprecated. Use :deep() instead.` + // ) + const prev = selector.at(selector.index(n) - 1) + if (prev && isSpaceCombinator(prev)) { + selector.removeChild(prev) + } + selector.removeChild(n) + } + return false + } + + // slot: use selector inside `::v-slotted` and inject [id + '-s'] + // instead. + // ::v-slotted(.foo) -> .foo[xxxxxxx-s] + if (value === ':slotted' || value === '::v-slotted') { + rewriteSelector(n.nodes[0], selectorRoot) + let last: selectorParser.Selector['nodes'][0] = n + n.nodes[0].each((ss) => { + selector.insertAfter(last, ss) + last = ss + }) + // selector.insertAfter(n, n.nodes[0]) + selector.removeChild(n) + // since slotted attribute already scopes the selector there's no + // need for the non-slot attribute. + return false + } + + // global: replace with inner selector and do not inject [id]. + // ::v-global(.foo) -> .foo + if (value === ':global' || value === '::v-global') { + selectorRoot.insertAfter(selector, n.nodes[0]) + selectorRoot.removeChild(selector) + return false + } + } + + if (n.type !== 'pseudo' && n.type !== 'combinator') { + node = n + } + }) + + if (node) { + ;(node as selectorParser.Node).spaces.after = '' + } else { + // For deep selectors & standalone pseudo selectors, + // the attribute selectors are prepended rather than appended. + // So all leading spaces must be eliminated to avoid problems. + selector.first.spaces.before = '' + } +} + +function isSpaceCombinator(node: selectorParser.Node) { + return node.type === 'combinator' && /^\s+$/.test(node.value) +} + +scopedPlugin.postcss = true +export default scopedPlugin diff --git a/packages/vite-plugin-uni/src/vue/options.ts b/packages/vite-plugin-uni/src/vue/options.ts index 0adebd1293bbace6d499ae62b13f0f6eda658232..2864060d0fc9233cdde967695797d5e189e8db73 100644 --- a/packages/vite-plugin-uni/src/vue/options.ts +++ b/packages/vite-plugin-uni/src/vue/options.ts @@ -3,8 +3,8 @@ import { SFCTemplateCompileOptions } from '@vue/compiler-sfc' import { isCustomElement } from '@dcloudio/uni-shared' import { EXTNAME_VUE_RE, - // parseCompatConfigOnce, UniVitePlugin, + uniPostcssScopedPlugin, } from '@dcloudio/uni-cli-shared' import { VitePluginUniResolvedOptions } from '..' @@ -49,6 +49,13 @@ export function initPluginVueOptions( } vueOptions.include.push(EXTNAME_VUE_RE) + const styleOptions = vueOptions.style || (vueOptions.style = {}) + if (!styleOptions.postcssPlugins) { + styleOptions.postcssPlugins = [] + } + // 解析 scoped 中 deep 等特殊语法 + styleOptions.postcssPlugins.push(uniPostcssScopedPlugin()) + const templateOptions = vueOptions.template || (vueOptions.template = {}) templateOptions.transformAssetUrls = createUniVueTransformAssetUrls(