],
// most of the codebase are expected to be env agnostic
'no-restricted-globals': ['error', ...DOMGlobals, ...NodeGlobals],
- // since we target ES2015 for baseline support, we need to forbid object
- // rest spread usage in destructure as it compiles into a verbose helper.
- // TS now compiles assignment spread into Object.assign() calls so that
- // is allowed.
+
'no-restricted-syntax': [
'error',
+ // since we target ES2015 for baseline support, we need to forbid object
+ // rest spread usage in destructure as it compiles into a verbose helper.
'ObjectPattern > RestElement',
+ // tsc compiles assignment spread into Object.assign() calls, but esbuild
+ // still generates verbose helpers, so spread assignment is also prohiboted
+ 'ObjectExpression > SpreadElement',
'AwaitExpression'
]
},
"node": ">=16.11.0"
},
"devDependencies": {
+ "@babel/parser": "^7.20.15",
"@babel/types": "^7.20.7",
"@esbuild-plugins/node-modules-polyfill": "^0.1.4",
"@microsoft/api-extractor": "~7.20.0",
"jsdom": "^21.1.0",
"lint-staged": "^10.2.10",
"lodash": "^4.17.15",
+ "magic-string": "^0.27.0",
"marked": "^4.0.10",
"minimist": "^1.2.0",
"npm-run-all": "^4.1.5",
}
export const enum SSRErrorCodes {
- X_SSR_UNSAFE_ATTR_NAME = DOMErrorCodes.__EXTEND_POINT__,
+ X_SSR_UNSAFE_ATTR_NAME = 62 /* DOMErrorCodes.__EXTEND_POINT__ */,
X_SSR_NO_TELEPORT_TARGET,
X_SSR_INVALID_AST_NODE
}
+if (__TEST__) {
+ // esbuild cannot infer const enum increments if first value is from another
+ // file, so we have to manually keep them in sync. this check ensures it
+ // errors out if there are collisions.
+ if (SSRErrorCodes.X_SSR_UNSAFE_ATTR_NAME < DOMErrorCodes.__EXTEND_POINT__) {
+ throw new Error(
+ 'SSRErrorCodes need to be updated to match extension point from core DOMErrorCodes.'
+ )
+ }
+}
+
export const SSRErrorMessages: { [code: number]: string } = {
[SSRErrorCodes.X_SSR_UNSAFE_ATTR_NAME]: `Unsafe attribute name for SSR.`,
[SSRErrorCodes.X_SSR_NO_TELEPORT_TARGET]: `Missing the 'to' prop on teleport element.`,
dep.add(activeEffect!)
activeEffect!.deps.push(dep)
if (__DEV__ && activeEffect!.onTrack) {
- activeEffect!.onTrack({
- effect: activeEffect!,
- ...debuggerEventExtraInfo!
- })
+ activeEffect!.onTrack(
+ extend(
+ {
+ effect: activeEffect!
+ },
+ debuggerEventExtraInfo!
+ )
+ )
}
}
}
shallowReadonly,
markRaw,
toRaw,
- ReactiveFlags,
+ ReactiveFlags /* @remove */,
type Raw,
type DeepReadonly,
type ShallowReactive,
getCurrentScope,
onScopeDispose
} from './effectScope'
-export { TrackOpTypes, TriggerOpTypes } from './operations'
+export {
+ TrackOpTypes /* @remove */,
+ TriggerOpTypes /* @remove */
+} from './operations'
import { createVNode, cloneVNode, VNode } from './vnode'
import { RootHydrateFunction } from './hydration'
import { devtoolsInitApp, devtoolsUnmountApp } from './devtools'
-import { isFunction, NO, isObject } from '@vue/shared'
+import { isFunction, NO, isObject, extend } from '@vue/shared'
import { version } from '.'
import { installAppCompatProperties } from './compat/global'
import { NormalizedPropsOptions } from './componentProps'
): CreateAppFunction<HostElement> {
return function createApp(rootComponent, rootProps = null) {
if (!isFunction(rootComponent)) {
- rootComponent = { ...rootComponent }
+ rootComponent = extend({}, rootComponent)
}
if (rootProps != null && !isObject(rootProps)) {
remove,
isMap,
isSet,
- isPlainObject
+ isPlainObject,
+ extend
} from '@vue/shared'
import {
currentInstance,
return doWatch(
effect,
null,
- __DEV__ ? { ...options, flush: 'post' } : { flush: 'post' }
+ __DEV__ ? extend({}, options as any, { flush: 'post' }) : { flush: 'post' }
)
}
return doWatch(
effect,
null,
- __DEV__ ? { ...options, flush: 'sync' } : { flush: 'sync' }
+ __DEV__ ? extend({}, options as any, { flush: 'sync' }) : { flush: 'sync' }
)
}
if (validatePropName(normalizedKey)) {
const opt = raw[key]
const prop: NormalizedProp = (normalized[normalizedKey] =
- isArray(opt) || isFunction(opt) ? { type: opt } : { ...opt })
+ isArray(opt) || isFunction(opt) ? { type: opt } : extend({}, opt))
if (prop) {
const booleanIndex = getTypeIndex(Boolean, prop.type)
const stringIndex = getTypeIndex(String, prop.type)
.:
specifiers:
+ '@babel/parser': ^7.20.15
'@babel/types': ^7.20.7
'@esbuild-plugins/node-modules-polyfill': ^0.1.4
'@microsoft/api-extractor': ~7.20.0
jsdom: ^21.1.0
lint-staged: ^10.2.10
lodash: ^4.17.15
+ magic-string: ^0.27.0
marked: ^4.0.10
minimist: ^1.2.0
npm-run-all: ^4.1.5
terser: ^5.15.1
todomvc-app-css: ^2.3.0
tslib: ^2.4.0
- typescript: ^4.8.0
+ typescript: ^4.9.0
vite: ^4.0.4
vitest: ^0.28.2
vue: workspace:*
devDependencies:
+ '@babel/parser': 7.20.15
'@babel/types': 7.20.7
'@esbuild-plugins/node-modules-polyfill': 0.1.4_esbuild@0.17.5
'@microsoft/api-extractor': 7.20.1
jsdom: 21.1.0
lint-staged: 10.5.4
lodash: 4.17.21
+ magic-string: 0.27.0
marked: 4.2.12
minimist: 1.2.7
npm-run-all: 4.1.5
import esbuild from 'rollup-plugin-esbuild'
import alias from '@rollup/plugin-alias'
import { entries } from './scripts/aliases.mjs'
+import { constEnum } from './scripts/const-enum.mjs'
+import { writeFileSync } from 'node:fs'
if (!process.env.TARGET) {
throw new Error('TARGET package must be specified via --environment flag.')
const packageOptions = pkg.buildOptions || {}
const name = packageOptions.filename || path.basename(packageDir)
+const [enumPlugin, enumDefines] = await constEnum()
+
const outputConfigs = {
'esm-bundler': {
file: resolve(`dist/${name}.esm-bundler.js`),
// esbuild define is a bit strict and only allows literal json or identifiers
// so we still need replace plugin in some cases
function resolveReplace() {
- const replacements = {}
+ const replacements = { ...enumDefines }
if (isProductionBuild && isBrowserBuild) {
Object.assign(replacements, {
alias({
entries
}),
+ enumPlugin,
...resolveReplace(),
esbuild({
tsconfig: path.resolve(__dirname, 'tsconfig.json'),
--- /dev/null
+// @ts-check
+
+/**
+ * We use rollup-plugin-esbuild for faster builds, but esbuild in insolation
+ * mode compiles const enums into runtime enums, bloating bundle size.
+ *
+ * Here we pre-process all the const enums in the project and turn them into
+ * global replacements, and remove the original declarations and re-exports.
+ *
+ * This erases the const enums before the esbuild transform so that we can
+ * leverage esbuild's speed while retaining the DX and bundle size benefits
+ * of const enums.
+ *
+ * This file is expected to be executed with project root as cwd.
+ */
+
+import execa from 'execa'
+import { readFileSync } from 'node:fs'
+import { parse } from '@babel/parser'
+import path from 'node:path'
+import MagicString from 'magic-string'
+
+function evaluate(exp) {
+ return new Function(`return ${exp}`)()
+}
+
+/**
+ * @returns {Promise<[import('rollup').Plugin, Record<string, string>]>}
+ */
+export async function constEnum() {
+ /**
+ * @type {{ ranges: Record<string, [number, number][]>, defines: Record<string, string> }}
+ */
+ const enumData = {
+ ranges: {},
+ defines: {}
+ }
+
+ const knowEnums = new Set()
+
+ // 1. grep for files with exported const enum
+ const { stdout } = await execa('git', ['grep', `export const enum`])
+ const files = [...new Set(stdout.split('\n').map(line => line.split(':')[0]))]
+
+ // 2. parse matched files to collect enum info
+ for (const relativeFile of files) {
+ const file = path.resolve(process.cwd(), relativeFile)
+ const content = readFileSync(file, 'utf-8')
+ const ast = parse(content, {
+ plugins: ['typescript'],
+ sourceType: 'module'
+ })
+
+ for (const node of ast.program.body) {
+ if (
+ node.type === 'ExportNamedDeclaration' &&
+ node.declaration &&
+ node.declaration.type === 'TSEnumDeclaration'
+ ) {
+ if (file in enumData.ranges) {
+ // @ts-ignore
+ enumData.ranges[file].push([node.start, node.end])
+ } else {
+ // @ts-ignore
+ enumData.ranges[file] = [[node.start, node.end]]
+ }
+
+ const decl = node.declaration
+ let lastInitialized
+ for (let i = 0; i < decl.members.length; i++) {
+ const e = decl.members[i]
+ const id = decl.id.name
+ knowEnums.add(id)
+ const key = e.id.type === 'Identifier' ? e.id.name : e.id.value
+ const fullKey = `${id}.${key}`
+ const init = e.initializer
+ if (init) {
+ let value
+ if (
+ init.type === 'StringLiteral' ||
+ init.type === 'NumericLiteral'
+ ) {
+ value = init.value
+ }
+
+ // e.g. 1 << 2
+ if (init.type === 'BinaryExpression') {
+ // @ts-ignore assume all operands are literals
+ const exp = `${init.left.value}${init.operator}${init.right.value}`
+ value = evaluate(exp)
+ }
+
+ if (init.type === 'UnaryExpression') {
+ // @ts-ignore assume all operands are literals
+ const exp = `${init.operator}${init.argument.value}`
+ value = evaluate(exp)
+ }
+
+ if (value === undefined) {
+ throw new Error(
+ `unhandled initializer type ${init.type} for ${fullKey} in ${file}`
+ )
+ }
+ enumData.defines[fullKey] = JSON.stringify(value)
+ lastInitialized = value
+ } else {
+ if (lastInitialized === undefined) {
+ // first initialized
+ enumData.defines[fullKey] = `0`
+ lastInitialized = 0
+ } else if (typeof lastInitialized === 'number') {
+ enumData.defines[fullKey] = String(++lastInitialized)
+ } else {
+ // should not happen
+ throw new Error(`wrong enum initialization sequence in ${file}`)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // construct a regex for matching re-exports of known const enums
+ const reExportsRE = new RegExp(
+ `export {[^}]*?\\b(${[...knowEnums].join('|')})\\b[^]*?}`
+ )
+
+ // 3. during transform:
+ // 3.1 files w/ const enum declaration: remove delcaration
+ // 3.2 files using const enum: inject into esbuild define
+ /**
+ * @type {import('rollup').Plugin}
+ */
+ const plugin = {
+ name: 'remove-const-enum',
+ transform(code, id) {
+ let s
+
+ if (id in enumData.ranges) {
+ s = s || new MagicString(code)
+ for (const [start, end] of enumData.ranges[id]) {
+ s.remove(start, end)
+ }
+ }
+
+ // check for const enum re-exports that must be removed
+ if (reExportsRE.test(code)) {
+ s = s || new MagicString(code)
+ const ast = parse(code, {
+ plugins: ['typescript'],
+ sourceType: 'module'
+ })
+ for (const node of ast.program.body) {
+ if (
+ node.type === 'ExportNamedDeclaration' &&
+ node.exportKind !== 'type' &&
+ node.source
+ ) {
+ for (let i = 0; i < node.specifiers.length; i++) {
+ const spec = node.specifiers[i]
+ if (
+ spec.type === 'ExportSpecifier' &&
+ spec.exportKind !== 'type' &&
+ knowEnums.has(spec.local.name)
+ ) {
+ if (i === 0) {
+ // first
+ const next = node.specifiers[i + 1]
+ // @ts-ignore
+ s.remove(spec.start, next ? next.start : spec.end)
+ } else {
+ // locate the end of prev
+ // @ts-ignore
+ s.remove(node.specifiers[i - 1].end, spec.end)
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (s) {
+ return {
+ code: s.toString(),
+ map: s.generateMap()
+ }
+ }
+ }
+ }
+
+ return [plugin, enumData.defines]
+}