From ae22205f484298d64d5b090e5e047fb6503ccb38 Mon Sep 17 00:00:00 2001 From: fxy060608 Date: Thu, 28 Jan 2021 20:38:08 +0800 Subject: [PATCH] feat(cli): add inject --- .../src/configResolved/plugins/index.ts | 27 +- .../src/configResolved/plugins/inject.ts | 273 ++++++++++++++++++ .../src/configResolved/plugins/pre.ts | 4 +- .../src/configResolved/plugins/preCss.ts | 2 +- packages/vite-plugin-uni/src/resolveId.ts | 2 +- 5 files changed, 304 insertions(+), 4 deletions(-) create mode 100644 packages/vite-plugin-uni/src/configResolved/plugins/inject.ts diff --git a/packages/vite-plugin-uni/src/configResolved/plugins/index.ts b/packages/vite-plugin-uni/src/configResolved/plugins/index.ts index 591a07988..4ea06c2e8 100644 --- a/packages/vite-plugin-uni/src/configResolved/plugins/index.ts +++ b/packages/vite-plugin-uni/src/configResolved/plugins/index.ts @@ -6,6 +6,7 @@ import { uniPrePlugin } from './pre' import { uniJsonPlugin } from './json' import { uniPreCssPlugin } from './preCss' import { uniEasycomPlugin } from './easycom' +import { InjectOptions, uniInjectPlugin } from './inject' const debugPlugin = debug('uni:plugin') export interface UniPluginFilterOptions extends VitePluginUniResolvedOptions { @@ -24,6 +25,9 @@ const uniPrePluginOptions: Partial = { /\/vue-router\//, /\/vuex\//, /@dcloudio\/uni-shared/, + /vite\/preload-helper/, + /vite\/dynamic-import-polyfill/, + /\.html$/, UNI_H5_RE, ], } @@ -32,7 +36,21 @@ const uniPreCssPluginOptions: Partial = { } const uniEasycomPluginOptions: Partial = { - exclude: [UNI_H5_RE], + exclude: [/App.vue$/, UNI_H5_RE], +} + +const uniInjectPluginOptions: Partial = { + exclude: [ + /@dcloudio\/uni-h5-vue/, + /@dcloudio\/uni-shared/, + /\/vue-router\//, + /@vue\/shared/, + ], + '__GLOBAL__.': '@dcloudio/uni-h5', + 'uni.': '@dcloudio/uni-h5', + getApp: ['@dcloudio/uni-h5', 'getApp'], + getCurrentPages: ['@dcloudio/uni-h5', 'getCurrentPages'], + UniServiceJSBridge: ['@dcloudio/uni-h5', 'UniServiceJSBridge'], } export function resolvePlugins( @@ -51,6 +69,13 @@ export function resolvePlugins( uniPreCssPlugin(Object.assign(uniPreCssPluginOptions, options)), 'vite:css' ) + if (!options.devServer) { + addPlugin( + plugins, + uniInjectPlugin(Object.assign(uniInjectPluginOptions, options)), + 'vite:vue' + ) + } addPlugin( plugins, uniEasycomPlugin(Object.assign(uniEasycomPluginOptions, options)), diff --git a/packages/vite-plugin-uni/src/configResolved/plugins/inject.ts b/packages/vite-plugin-uni/src/configResolved/plugins/inject.ts new file mode 100644 index 000000000..c0d884583 --- /dev/null +++ b/packages/vite-plugin-uni/src/configResolved/plugins/inject.ts @@ -0,0 +1,273 @@ +import path, { sep } from 'path' +import debug from 'debug' +import { Plugin, ViteDevServer } from 'vite' + +import { + BaseNode, + Program, + Property, + Identifier, + MemberExpression, + MethodDefinition, + ExportSpecifier, +} from 'estree' + +import { + attachScopes, + createFilter, + makeLegalIdentifier, +} from '@rollup/pluginutils' +import { AcornNode } from 'rollup' + +import { walk } from 'estree-walker' + +import MagicString from 'magic-string' + +import { UniPluginFilterOptions } from '.' + +interface Scope { + parent: Scope + contains: (name: string) => boolean +} + +type Injectment = string | [string, string] + +export interface InjectOptions extends UniPluginFilterOptions { + sourceMap?: boolean + [str: string]: Injectment | InjectOptions['include'] | Boolean | ViteDevServer +} + +const debugInject = debug('uni:inject') +const debugInjectTry = debug('uni:inject-try') + +export function uniInjectPlugin(options: InjectOptions): Plugin { + if (!options) throw new Error('Missing options') + + const filter = createFilter(options.include, options.exclude) + const modules = Object.assign({}, options) as { [str: string]: Injectment } + delete modules.include + delete modules.exclude + delete modules.sourceMap + delete modules.devServer + + const modulesMap = new Map() + const namespaceModulesMap = new Map() + Object.keys(modules).forEach((name) => { + if (name.endsWith('.')) { + namespaceModulesMap.set(name, modules[name]) + } + modulesMap.set(name, modules[name]) + }) + const hasNamespace = namespaceModulesMap.size > 0 + + // Fix paths on Windows + if (sep !== '/') { + normalizeModulesMap(modulesMap) + normalizeModulesMap(namespaceModulesMap) + } + + const firstpass = new RegExp( + `(?:${Array.from(modulesMap.keys()).map(escape).join('|')})`, + 'g' + ) + const EXTNAMES = ['.js', '.ts', '.vue', '.nvue'] + const sourceMap = options.sourceMap !== false + return { + name: 'uni:inject', + transform(code, id) { + if (!filter(id)) return null + if (!EXTNAMES.includes(path.extname(id))) { + return null + } + debugInjectTry(id) + if (code.search(firstpass) === -1) return null + if (sep !== '/') id = id.split(sep).join('/') + let ast = null + try { + ast = this.parse(code) + } catch (err) { + this.warn({ + code: 'PARSE_ERROR', + message: `plugin-inject: failed to parse ${id}. Consider restricting the plugin to particular files via options.include`, + }) + } + if (!ast) { + return null + } + + const imports = new Set() + ;((ast as unknown) as Program).body.forEach((node) => { + if (node.type === 'ImportDeclaration') { + node.specifiers.forEach((specifier) => { + imports.add(specifier.local.name) + }) + } + }) + + // analyse scopes + let scope = attachScopes(ast, 'scope') as Scope + + const magicString = new MagicString(code) + + const newImports = new Map() + + function handleReference(node: BaseNode, name: string, keypath: string) { + let mod = modulesMap.get(keypath) + if (!mod && hasNamespace) { + const mods = keypath.split('.') + if (mods.length === 2) { + mod = namespaceModulesMap.get(mods[0] + '.') + if (mod) { + mod = [mod as string, mods[1]] + } + } + } + if (mod && !imports.has(name) && !scope.contains(name)) { + if (typeof mod === 'string') mod = [mod, 'default'] + if (mod[0] === id) return false + + const hash = `${keypath}:${mod[0]}:${mod[1]}` + + const importLocalName = + name === keypath ? name : makeLegalIdentifier(`$inject_${keypath}`) + + if (!newImports.has(hash)) { + if (mod[1] === '*') { + newImports.set( + hash, + `import * as ${importLocalName} from '${mod[0]}';` + ) + } else { + newImports.set( + hash, + `import { ${mod[1]} as ${importLocalName} } from '${mod[0]}';` + ) + } + } + + if (name !== keypath) { + magicString.overwrite( + (node as AcornNode).start, + (node as AcornNode).end, + importLocalName, + { + storeName: true, + } + ) + } + return true + } + return false + } + + walk(ast, { + enter(node, parent) { + if (sourceMap) { + magicString.addSourcemapLocation((node as AcornNode).start) + magicString.addSourcemapLocation((node as AcornNode).end) + } + + if ((node as any).scope) { + scope = (node as any).scope + } + + if (isProperty(node) && node.shorthand) { + const { name } = node.key as Identifier + handleReference(node, name, name) + this.skip() + return + } + + if (isReference(node, parent)) { + const { name, keypath } = flatten(node) + const handled = handleReference(node, name, keypath) + if (handled) { + this.skip() + } + } + }, + leave(node) { + if ((node as any).scope) { + scope = scope.parent + } + }, + }) + debugInject(id, newImports.size) + if (newImports.size === 0) { + return { + code, + ast, + map: sourceMap ? magicString.generateMap({ hires: true }) : null, + } + } + const importBlock = Array.from(newImports.values()).join('\n\n') + + magicString.prepend(`${importBlock}\n\n`) + + return { + code: magicString.toString(), + map: sourceMap ? magicString.generateMap({ hires: true }) : null, + } + }, + } +} + +const escape = (str: string) => str.replace(/[-[\]/{}()*+?.\\^$|]/g, '\\$&') + +const isProperty = (node: BaseNode): node is Property => + node.type === 'Property' + +const isIdentifier = (node: BaseNode): node is Identifier => + node.type === 'Identifier' + +const isMemberExpression = (node: BaseNode): node is MemberExpression => + node.type === 'MemberExpression' + +const isMethodDefinition = (node: BaseNode): node is MethodDefinition => + node.type === 'MethodDefinition' + +const isExportSpecifier = (node: BaseNode): node is ExportSpecifier => + node.type === 'ExportSpecifier' + +const isReference = (node: BaseNode, parent: BaseNode): boolean => { + if (isMemberExpression(node)) { + return !node.computed && isReference(node.object, node) + } + if (isIdentifier(node)) { + if (isMemberExpression(parent)) + return parent.computed || node === parent.object + // `bar` in { bar: foo } + if (isProperty(parent) && node !== parent.value) return false + // `bar` in `class Foo { bar () {...} }` + if (isMethodDefinition(parent)) return false + // `bar` in `export { foo as bar }` + if (isExportSpecifier(parent) && node !== parent.local) return false + return true + } + return false +} + +const flatten = (startNode: BaseNode) => { + const parts = [] + let node = startNode + while (isMemberExpression(node)) { + parts.unshift((node.property as Identifier).name) + node = node.object + } + const { name } = node as Identifier + parts.unshift(name) + return { name, keypath: parts.join('.') } +} + +function normalizeModulesMap( + modulesMap: Map +) { + modulesMap.forEach((mod, key) => { + modulesMap.set( + key, + Array.isArray(mod) + ? [mod[0].split(sep).join('/'), mod[1]] + : mod.split(sep).join('/') + ) + }) +} diff --git a/packages/vite-plugin-uni/src/configResolved/plugins/pre.ts b/packages/vite-plugin-uni/src/configResolved/plugins/pre.ts index 2c43dbadc..d159cb784 100644 --- a/packages/vite-plugin-uni/src/configResolved/plugins/pre.ts +++ b/packages/vite-plugin-uni/src/configResolved/plugins/pre.ts @@ -21,11 +21,13 @@ export function uniPrePlugin(options: UniPluginFilterOptions): Plugin { if (!filter(id)) { return code } - debugPreJsTry(id) const extname = path.extname(id) const isHtml = PRE_HTML_EXTNAME.includes(extname) const isJs = PRE_JS_EXTNAME.includes(extname) const isPre = isHtml || isJs + if (isPre) { + debugPreJsTry(id) + } const hasEndif = isPre && code.includes('#endif') if (isHtml && hasEndif) { code = preHtml(code) diff --git a/packages/vite-plugin-uni/src/configResolved/plugins/preCss.ts b/packages/vite-plugin-uni/src/configResolved/plugins/preCss.ts index 286782070..1602084ee 100644 --- a/packages/vite-plugin-uni/src/configResolved/plugins/preCss.ts +++ b/packages/vite-plugin-uni/src/configResolved/plugins/preCss.ts @@ -8,7 +8,7 @@ const { preJs } = require('@dcloudio/uni-cli-shared') const debugPre = debug('uni:pre-css') const debugPreTry = debug('uni:pre-css-try') -const cssLangs = `\\.(css|less|sass|scss|styl|stylus|postcss)($|\\?)` +const cssLangs = `\\.(less|sass|scss|styl|stylus|postcss)($|\\?)` const cssLangRE = new RegExp(cssLangs) /** * preprocess css diff --git a/packages/vite-plugin-uni/src/resolveId.ts b/packages/vite-plugin-uni/src/resolveId.ts index 94c998eb6..33b46e23d 100644 --- a/packages/vite-plugin-uni/src/resolveId.ts +++ b/packages/vite-plugin-uni/src/resolveId.ts @@ -26,7 +26,7 @@ export function createResolveId( } if (id.startsWith('@/')) { debugResolve(id) - return id.replace('@/', '/src/') + return path.join(options.inputDir, id.replace('@/', '')) } } } -- GitLab