diff --git a/.eslintrc.js b/.eslintrc.js index 0732923e..fc169abe 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -41,7 +41,7 @@ module.exports = { }, // Packages targeting DOM { - files: ['packages/{vue,runtime-dom}/**'], + files: ['packages/{vue,vue-compat,runtime-dom}/**'], rules: { 'no-restricted-globals': ['error', ...NodeGlobals] } diff --git a/packages/global.d.ts b/packages/global.d.ts index 8c6c57d8..72ab2fe9 100644 --- a/packages/global.d.ts +++ b/packages/global.d.ts @@ -8,6 +8,7 @@ declare var __ESM_BROWSER__: boolean declare var __NODE_JS__: boolean declare var __COMMIT__: string declare var __VERSION__: string +declare var __COMPAT__: boolean // Feature flags declare var __FEATURE_OPTIONS_API__: boolean diff --git a/packages/runtime-dom/src/index.ts b/packages/runtime-dom/src/index.ts index 77347062..b2a87a60 100644 --- a/packages/runtime-dom/src/index.ts +++ b/packages/runtime-dom/src/index.ts @@ -13,7 +13,15 @@ import { import { nodeOps } from './nodeOps' import { patchProp, forcePatchProp } from './patchProp' // Importing from the compiler, will be tree-shaken in prod -import { isFunction, isString, isHTMLTag, isSVGTag, extend } from '@vue/shared' +import { + isFunction, + isString, + isHTMLTag, + isSVGTag, + extend, + warnDeprecation, + DeprecationTypes +} from '@vue/shared' declare module '@vue/reactivity' { export interface RefUnwrapBailTypes { @@ -63,8 +71,24 @@ export const createApp = ((...args) => { app.mount = (containerOrSelector: Element | ShadowRoot | string): any => { const container = normalizeContainer(containerOrSelector) if (!container) return + + // 2.x compat check + if (__COMPAT__ && __DEV__) { + for (let i = 0; i < container.attributes.length; i++) { + const attr = container.attributes[i] + if (attr.name !== 'v-cloak' && /^(v-|:|@)/.test(attr.name)) { + warnDeprecation(DeprecationTypes.DOM_TEMPLATE_MOUNT) + break + } + } + } + const component = app._component if (!isFunction(component) && !component.render && !component.template) { + // __UNSAFE__ + // Reason: potential execution of JS expressions in in-DOM template. + // The user must make sure the in-DOM template is trusted. If it's + // rendered by the server, the template should not contain any user data. component.template = container.innerHTML } // clear content before mounting diff --git a/packages/shared/src/deprecations.ts b/packages/shared/src/deprecations.ts new file mode 100644 index 00000000..f3d23750 --- /dev/null +++ b/packages/shared/src/deprecations.ts @@ -0,0 +1,25 @@ +export const enum DeprecationTypes { + DOM_TEMPLATE_MOUNT +} + +type DeprecationData = { + message: string + link?: string +} + +const deprecations: Record = { + [DeprecationTypes.DOM_TEMPLATE_MOUNT]: { + message: + `Vue detected directives on the mount container. ` + + `In Vue 3, the container is no longer considered part of the template ` + + `and will not be processed/replaced.`, + link: `https://v3.vuejs.org/guide/migration/mount-changes.html` + } +} + +export function warnDeprecation(key: DeprecationTypes) { + const { message, link } = deprecations[key] + console.warn( + `[Deprecation]: ${message}${link ? `\nFor more details, see ${link}` : ``}` + ) +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 84b324be..8a16c7c4 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -12,6 +12,7 @@ export * from './domAttrConfig' export * from './escapeHtml' export * from './looseEqual' export * from './toDisplayString' +export * from './deprecations' /** * List of @babel/parser plugins that are used for template expression diff --git a/packages/vue-compat/README.md b/packages/vue-compat/README.md new file mode 100644 index 00000000..62c5aa57 --- /dev/null +++ b/packages/vue-compat/README.md @@ -0,0 +1 @@ +# @vue/compat \ No newline at end of file diff --git a/packages/vue-compat/api-extractor.json b/packages/vue-compat/api-extractor.json new file mode 100644 index 00000000..a8982eb0 --- /dev/null +++ b/packages/vue-compat/api-extractor.json @@ -0,0 +1,7 @@ +{ + "extends": "../../api-extractor.json", + "mainEntryPointFilePath": "./dist/packages//src/index.d.ts", + "dtsRollup": { + "publicTrimmedFilePath": "./dist/.d.ts" + } +} \ No newline at end of file diff --git a/packages/vue-compat/index.js b/packages/vue-compat/index.js new file mode 100644 index 00000000..aadbf104 --- /dev/null +++ b/packages/vue-compat/index.js @@ -0,0 +1,7 @@ +'use strict' + +if (process.env.NODE_ENV === 'production') { + module.exports = require('./dist/compat.cjs.prod.js') +} else { + module.exports = require('./dist/compat.cjs.js') +} diff --git a/packages/vue-compat/package.json b/packages/vue-compat/package.json new file mode 100644 index 00000000..60647569 --- /dev/null +++ b/packages/vue-compat/package.json @@ -0,0 +1,44 @@ +{ + "name": "@vue/compat", + "version": "3.0.11", + "description": "@vue/compat", + "main": "index.js", + "module": "dist/vue.esm-bundler.js", + "types": "dist/vue.d.ts", + "unpkg": "dist/vue.global.js", + "jsdelivr": "dist/vue.global.js", + "files": [ + "index.js", + "dist" + ], + "buildOptions": { + "name": "Vue", + "filename": "vue", + "compat": true, + "formats": [ + "esm-bundler", + "esm-bundler-runtime", + "cjs", + "global", + "global-runtime", + "esm-browser", + "esm-browser-runtime" + ] + }, + "repository": { + "type": "git", + "url": "git+https://github.com/vuejs/vue.git" + }, + "keywords": [ + "vue" + ], + "author": "Evan You", + "license": "MIT", + "bugs": { + "url": "https://github.com/vuejs/vue/issues" + }, + "homepage": "https://github.com/vuejs/vue/tree/dev/packages/vue-compat#readme", + "peerDependencies": { + "vue": "3.0.11" + } +} diff --git a/packages/vue-compat/src/apiGlobal.ts b/packages/vue-compat/src/apiGlobal.ts new file mode 100644 index 00000000..10ad71e9 --- /dev/null +++ b/packages/vue-compat/src/apiGlobal.ts @@ -0,0 +1,157 @@ +import { reactive } from '@vue/reactivity' +import { + createApp, + defineComponent, + nextTick, + App, + AppConfig, + Plugin, + Component, + ComponentOptions, + ComponentPublicInstance, + Directive, + RenderFunction, + isRuntimeOnly +} from '@vue/runtime-dom' + +// TODO make these getter/setters and trigger deprecation warnings +export type LegacyConfig = AppConfig & { + /** + * @deprecated `config.silent` option has been removed + */ + silent?: boolean + /** + * @deprecated use __VUE_PROD_DEVTOOLS__ compile-time feature flag instead + * https://github.com/vuejs/vue-next/tree/master/packages/vue#bundler-build-feature-flags + */ + devtools?: boolean + /** + * @deprecated use `config.isCustomElement` instead + * https://v3.vuejs.org/guide/migration/global-api.html#config-ignoredelements-is-now-config-iscustomelement + */ + ignoredElements?: (string | RegExp)[] + /** + * @deprecated + * https://v3.vuejs.org/guide/migration/keycode-modifiers.html + */ + keyCodes?: Record + /** + * @deprecated + * https://v3.vuejs.org/guide/migration/global-api.html#config-productiontip-removed + */ + productionTip?: boolean +} + +/** + * @deprecated the default `Vue` export has been removed in Vue 3. The type for + * the default export is provided only for migration purposes. Please use + * named imports instead - e.g. `import { createApp } from 'vue'`. + */ +export type GlobalVue = Pick & { + // no inference here since these types are not meant for actual use - they + // are merely here to provide type checks for internal implementation and + // information for migration. + new (options?: ComponentOptions): ComponentPublicInstance + + version: string + config: LegacyConfig + + extend: typeof defineComponent + nextTick: typeof nextTick + + use(plugin: Plugin, ...options: any[]): GlobalVue + mixin(mixin: ComponentOptions): GlobalVue + + component(name: string): Component | undefined + component(name: string, component: Component): GlobalVue + directive(name: string): Directive | undefined + directive(name: string, directive: Directive): GlobalVue + + compile(template: string): RenderFunction + + /** + * @deprecated Vue 3 no longer needs set() for adding new properties. + */ + set(target: any, key: string | number | symbol, value: any): void + /** + * @deprecated Vue 3 no longer needs delete() for property deletions. + */ + delete(target: any, key: string | number | symbol): void + /** + * @deprecated use `reactive` instead. + */ + observable: typeof reactive + /** + * @deprecated filters have been removed from Vue 3. + */ + filter(name: string, arg: any): null +} + +export const Vue: GlobalVue = function Vue(options: ComponentOptions = {}) { + const app = createApp(options) + // copy over global config mutations + for (const key in singletonApp.config) { + if ( + key !== 'isNativeTag' && + !(key === 'isCustomElement' && isRuntimeOnly()) + ) { + // @ts-ignore + app.config[key] = singletonApp.config[key] + } + } + if (options.el) { + return app.mount(options.el) + } +} as any + +const singletonApp = createApp({}) + +Vue.version = __VERSION__ +Vue.config = singletonApp.config + +Vue.extend = defineComponent +Vue.nextTick = nextTick + +Vue.set = (target, key, value) => { + // TODO deprecation warnings + target[key] = value +} +Vue.delete = (target, key) => { + // TODO deprecation warnings + delete target[key] +} +// TODO wrap with deprecation warning +Vue.observable = reactive + +Vue.use = (p, ...options) => { + singletonApp.use(p, ...options) + return Vue +} + +Vue.mixin = m => { + singletonApp.mixin(m) + return Vue +} + +Vue.component = ((name: string, comp: any) => { + if (comp) { + singletonApp.component(name, comp) + return Vue + } else { + return singletonApp.component(name) + } +}) as any + +Vue.directive = ((name: string, dir: any) => { + if (dir) { + singletonApp.directive(name, dir) + return Vue + } else { + return singletonApp.directive(name) + } +}) as any + +Vue.filter = ((name: string, filter: any) => { + // TODO deprecation warning + // TODO compiler warning for filters (maybe behavior compat?) +}) as any diff --git a/packages/vue-compat/src/dev.ts b/packages/vue-compat/src/dev.ts new file mode 100644 index 00000000..99ba49a2 --- /dev/null +++ b/packages/vue-compat/src/dev.ts @@ -0,0 +1,14 @@ +import { initCustomFormatter } from '@vue/runtime-dom' + +export function initDev() { + if (__BROWSER__) { + if (!__ESM_BUNDLER__) { + console.info( + `You are running a development build of Vue.\n` + + `Make sure to use the production build (*.prod.js) when deploying for production.` + ) + } + + initCustomFormatter() + } +} diff --git a/packages/vue-compat/src/index.ts b/packages/vue-compat/src/index.ts new file mode 100644 index 00000000..481f3b5f --- /dev/null +++ b/packages/vue-compat/src/index.ts @@ -0,0 +1,93 @@ +// This entry is the "full-build" that includes both the runtime +// and the compiler, and supports on-the-fly compilation of the template option. +import { initDev } from './dev' +import { compile, CompilerOptions, CompilerError } from '@vue/compiler-dom' +import { registerRuntimeCompiler, RenderFunction, warn } from '@vue/runtime-dom' +import { isString, NOOP, generateCodeFrame, extend } from '@vue/shared' +import { InternalRenderFunction } from 'packages/runtime-core/src/component' +import * as runtimeDom from '@vue/runtime-dom' +import { Vue } from './apiGlobal' + +if (__DEV__) { + initDev() +} + +const compileCache: Record = Object.create(null) + +function compileToFunction( + template: string | HTMLElement, + options?: CompilerOptions +): RenderFunction { + if (!isString(template)) { + if (template.nodeType) { + template = template.innerHTML + } else { + __DEV__ && warn(`invalid template option: `, template) + return NOOP + } + } + + const key = template + const cached = compileCache[key] + if (cached) { + return cached + } + + if (template[0] === '#') { + const el = document.querySelector(template) + if (__DEV__ && !el) { + warn(`Template element not found or is empty: ${template}`) + } + // __UNSAFE__ + // Reason: potential execution of JS expressions in in-DOM template. + // The user must make sure the in-DOM template is trusted. If it's rendered + // by the server, the template should not contain any user data. + template = el ? el.innerHTML : `` + } + + const { code } = compile( + template, + extend( + { + hoistStatic: true, + onError(err: CompilerError) { + if (__DEV__) { + const message = `Template compilation error: ${err.message}` + const codeFrame = + err.loc && + generateCodeFrame( + template as string, + err.loc.start.offset, + err.loc.end.offset + ) + warn(codeFrame ? `${message}\n${codeFrame}` : message) + } else { + /* istanbul ignore next */ + throw err + } + } + }, + options + ) + ) + + // The wildcard import results in a huge object with every export + // with keys that cannot be mangled, and can be quite heavy size-wise. + // In the global build we know `Vue` is available globally so we can avoid + // the wildcard object. + const render = (__GLOBAL__ + ? new Function(code)() + : new Function('Vue', code)(runtimeDom)) as RenderFunction + + // mark the function as runtime compiled + ;(render as InternalRenderFunction)._rc = true + + return (compileCache[key] = render) +} + +registerRuntimeCompiler(compileToFunction) + +Vue.compile = compileToFunction +extend(Vue, runtimeDom) + +export default Vue diff --git a/packages/vue-compat/src/runtime.ts b/packages/vue-compat/src/runtime.ts new file mode 100644 index 00000000..04b60b36 --- /dev/null +++ b/packages/vue-compat/src/runtime.ts @@ -0,0 +1,25 @@ +// This entry exports the runtime only, and is built as +// `dist/vue.esm-bundler.js` which is used by default for bundlers. +import { initDev } from './dev' +import { warn } from '@vue/runtime-dom' + +if (__DEV__) { + initDev() +} + +export * from '@vue/runtime-dom' + +export const compile = () => { + if (__DEV__) { + warn( + `Runtime compilation is not supported in this build of Vue.` + + (__ESM_BUNDLER__ + ? ` Configure your bundler to alias "vue" to "@vue/compat/dist/vue.esm-bundler.js".` + : __ESM_BROWSER__ + ? ` Use "vue.esm-browser.js" instead.` + : __GLOBAL__ + ? ` Use "vue.global.js" instead.` + : ``) /* should not happen */ + ) + } +} diff --git a/rollup.config.js b/rollup.config.js index e8fe8d88..eced8ac8 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,3 +1,4 @@ +// @ts-check import path from 'path' import ts from 'rollup-plugin-typescript2' import replace from '@rollup/plugin-replace' @@ -10,10 +11,10 @@ if (!process.env.TARGET) { const masterVersion = require('./package.json').version const packagesDir = path.resolve(__dirname, 'packages') const packageDir = path.resolve(packagesDir, process.env.TARGET) -const name = path.basename(packageDir) const resolve = p => path.resolve(packageDir, p) const pkg = require(resolve(`package.json`)) const packageOptions = pkg.buildOptions || {} +const name = packageOptions.filename || path.basename(packageDir) // ensure TS checks only once for each build let hasTSChecked = false @@ -89,6 +90,7 @@ function createConfig(format, output, plugins = []) { const isBrowserESMBuild = /esm-browser/.test(format) const isNodeBuild = format === 'cjs' const isGlobalBuild = /global/.test(format) + const isCompatBuild = !!packageOptions.compat if (isGlobalBuild) { output.name = packageOptions.name @@ -116,19 +118,23 @@ function createConfig(format, output, plugins = []) { const entryFile = /runtime$/.test(format) ? `src/runtime.ts` : `src/index.ts` - const external = - isGlobalBuild || isBrowserESMBuild - ? packageOptions.enableNonBrowserBranches - ? [] - : // normal browser builds - non-browser only imports are tree-shaken, - // they are only listed here to suppress warnings. - ['source-map', '@babel/parser', 'estree-walker'] - : // Node / esm-bundler builds. Externalize everything. - [ - ...Object.keys(pkg.dependencies || {}), - ...Object.keys(pkg.peerDependencies || {}), - ...['path', 'url', 'stream'] // for @vue/compiler-sfc / server-renderer - ] + let external = [] + + if (isGlobalBuild || isBrowserESMBuild) { + if (!packageOptions.enableNonBrowserBranches) { + // normal browser builds - non-browser only imports are tree-shaken, + // they are only listed here to suppress warnings. + external = ['source-map', '@babel/parser', 'estree-walker'] + } + } else if (!isCompatBuild) { + // Node / esm-bundler builds. + // externalize all deps unless it's the compat build. + external = [ + ...Object.keys(pkg.dependencies || {}), + ...Object.keys(pkg.peerDependencies || {}), + ...['path', 'url', 'stream'] // for @vue/compiler-sfc / server-renderer + ] + } // the browser builds of @vue/compiler-sfc requires postcss to be available // as a global (e.g. http://wzrd.in/standalone/postcss) @@ -139,9 +145,11 @@ function createConfig(format, output, plugins = []) { const nodePlugins = packageOptions.enableNonBrowserBranches && format !== 'cjs' ? [ + // @ts-ignore require('@rollup/plugin-commonjs')({ sourceMap: false }), + // @ts-ignore require('rollup-plugin-polyfill-node')(), require('@rollup/plugin-node-resolve').nodeResolve() ] @@ -165,7 +173,8 @@ function createConfig(format, output, plugins = []) { (isGlobalBuild || isBrowserESMBuild || isBundlerESMBuild) && !packageOptions.enableNonBrowserBranches, isGlobalBuild, - isNodeBuild + isNodeBuild, + isCompatBuild ), ...nodePlugins, ...plugins @@ -188,7 +197,8 @@ function createReplacePlugin( isBrowserESMBuild, isBrowserBuild, isGlobalBuild, - isNodeBuild + isNodeBuild, + isCompatBuild ) { const replacements = { __COMMIT__: `"${process.env.COMMIT}"`, @@ -208,6 +218,9 @@ function createReplacePlugin( // is targeting Node (SSR)? __NODE_JS__: isNodeBuild, + // 2.x compat build + __COMPAT__: isCompatBuild, + // feature flags __FEATURE_SUSPENSE__: true, __FEATURE_OPTIONS_API__: isBundlerESMBuild ? `__VUE_OPTIONS_API__` : true, @@ -231,6 +244,7 @@ function createReplacePlugin( } }) return replace({ + // @ts-ignore values: replacements, preventAssignment: true })