const DOMGlobals = ['window', 'document']
const NodeGlobals = ['module', 'require']
+const banConstEnum = {
+ selector: 'TSEnumDeclaration[const=true]',
+ message:
+ 'Please use non-const enums. This project automatically inlines enums.'
+}
+
+/**
+ * @type {import('eslint-define-config').ESLintConfig}
+ */
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
'no-restricted-syntax': [
'error',
+ banConstEnum,
// 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',
files: ['packages/{compiler-sfc,compiler-ssr,server-renderer}/**'],
rules: {
'no-restricted-globals': ['error', ...DOMGlobals],
- 'no-restricted-syntax': 'off'
+ 'no-restricted-syntax': ['error', banConstEnum]
}
},
// Private package, browser only + no syntax restrictions
files: ['packages/template-explorer/**', 'packages/sfc-playground/**'],
rules: {
'no-restricted-globals': ['error', ...NodeGlobals],
- 'no-restricted-syntax': 'off'
+ 'no-restricted-syntax': ['error', banConstEnum]
}
},
// JavaScript files
files: ['scripts/**', '*.{js,ts}', 'packages/**/index.js'],
rules: {
'no-restricted-globals': 'off',
- 'no-restricted-syntax': 'off'
+ 'no-restricted-syntax': ['error', banConstEnum]
}
}
]
"@rollup/plugin-replace": "^5.0.4",
"@rollup/plugin-terser": "^0.4.4",
"@types/hash-sum": "^1.0.2",
+ "@types/minimist": "^1.2.5",
"@types/node": "^20.10.0",
+ "@types/semver": "^7.5.5",
"@typescript-eslint/parser": "^6.13.0",
"@vitest/coverage-istanbul": "^0.34.6",
"@vue/consolidate": "0.17.3",
"esbuild": "^0.19.5",
"esbuild-plugin-polyfill-node": "^0.3.0",
"eslint": "^8.54.0",
+ "eslint-define-config": "^1.24.1",
"eslint-plugin-jest": "^27.6.0",
"estree-walker": "^2.0.2",
"execa": "^8.0.1",
// More namespaces can be declared by platform specific compilers.
export type Namespace = number
-export const enum Namespaces {
+export enum Namespaces {
HTML,
SVG,
MATH_ML
}
-export const enum NodeTypes {
+export enum NodeTypes {
ROOT,
ELEMENT,
TEXT,
JS_RETURN_STATEMENT
}
-export const enum ElementTypes {
+export enum ElementTypes {
ELEMENT,
COMPONENT,
SLOT,
* Higher levels implies lower levels. e.g. a node that can be stringified
* can always be hoisted and skipped for patch.
*/
-export const enum ConstantTypes {
+export enum ConstantTypes {
NOT_CONSTANT = 0,
CAN_SKIP_PATCH,
CAN_HOIST,
map?: RawSourceMap
}
-const enum NewlineType {
+enum NewlineType {
Start = 0,
End = -1,
None = -2,
compatConfig?: CompilerCompatConfig
}
-export const enum CompilerDeprecationTypes {
+export enum CompilerDeprecationTypes {
COMPILER_IS_ON_ELEMENT = 'COMPILER_IS_ON_ELEMENT',
COMPILER_V_BIND_SYNC = 'COMPILER_V_BIND_SYNC',
COMPILER_V_BIND_OBJECT_ORDER = 'COMPILER_V_BIND_OBJECT_ORDER',
return error
}
-export const enum ErrorCodes {
+export enum ErrorCodes {
// parse errors
ABRUPT_CLOSING_OF_EMPTY_COMMENT,
CDATA_IN_HTML_CONTENT,
parent: ParentNode
) => void
-export const enum BindingTypes {
+export enum BindingTypes {
/**
* returned from data()
*/
fromCodePoint
} from 'entities/lib/decode.js'
-export const enum ParseMode {
+export enum ParseMode {
BASE,
HTML,
SFC
}
-export const enum CharCodes {
+export enum CharCodes {
Tab = 0x9, // "\t"
NewLine = 0xa, // "\n"
FormFeed = 0xc, // "\f"
UpperZ = 0x5a, // "Z"
LowerZ = 0x7a, // "z"
LowerX = 0x78, // "x"
- OpeningSquareBracket = 0x5b, // "["
LowerV = 0x76, // "v"
Dot = 0x2e, // "."
Colon = 0x3a, // ":"
const defaultDelimitersClose = new Uint8Array([125, 125]) // "}}"
/** All the states the tokenizer can be in. */
-export const enum State {
+export enum State {
Text = 1,
// interpolation
}
}
private stateBeforeDeclaration(c: number): void {
- if (c === CharCodes.OpeningSquareBracket) {
+ if (c === CharCodes.LeftSqaure) {
this.state = State.CDATASequence
this.sequenceIndex = 0
} else {
export const isSimpleIdentifier = (name: string): boolean =>
!nonIdentifierRE.test(name)
-const enum MemberExpLexState {
+enum MemberExpLexState {
inMemberExp,
inBrackets,
inParens,
) as DOMCompilerError
}
-export const enum DOMErrorCodes {
+export enum DOMErrorCodes {
X_V_HTML_NO_EXPRESSION = 53 /* ErrorCodes.__EXTEND_POINT__ */,
X_V_HTML_WITH_CHILDREN,
X_V_TEXT_NO_EXPRESSION,
}
if (__TEST__) {
- // esbuild cannot infer const enum increments if first value is from another
+ // esbuild cannot infer 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 (DOMErrorCodes.X_V_HTML_NO_EXPRESSION < ErrorCodes.__EXTEND_POINT__) {
isBooleanAttr
} from '@vue/shared'
-export const enum StringifyThresholds {
+export enum StringifyThresholds {
ELEMENT_WITH_BINDING_COUNT = 5,
NODE_COUNT = 20
}
return vars
}
-const enum LexerState {
+enum LexerState {
inParens,
inSingleQuoteString,
inDoubleQuoteString
return createCompilerError(code, loc, SSRErrorMessages) as SSRCompilerError
}
-export const enum SSRErrorCodes {
+export enum SSRErrorCodes {
X_SSR_UNSAFE_ATTR_NAME = 65 /* 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
+ // esbuild cannot infer 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__) {
// using literal strings instead of numbers so that it's easier to inspect
// debugger events
-export const enum TrackOpTypes {
+export enum TrackOpTypes {
GET = 'get',
HAS = 'has',
ITERATE = 'iterate'
}
-export const enum TriggerOpTypes {
+export enum TriggerOpTypes {
SET = 'set',
ADD = 'add',
DELETE = 'delete',
CLEAR = 'clear'
}
-export const enum ReactiveFlags {
+export enum ReactiveFlags {
SKIP = '__v_skip',
IS_REACTIVE = '__v_isReactive',
IS_READONLY = '__v_isReadonly',
RAW = '__v_raw'
}
-export const enum DirtyLevels {
+export enum DirtyLevels {
NotDirty = 0,
ComputedValueMaybeDirty = 1,
ComputedValueDirty = 2,
getCurrentScope,
onScopeDispose
} from './effectScope'
-export {
- TrackOpTypes /* @remove */,
- TriggerOpTypes /* @remove */,
- ReactiveFlags /* @remove */
-} from './constants'
+export { TrackOpTypes, TriggerOpTypes, ReactiveFlags } from './constants'
export const readonlyMap = new WeakMap<Target, any>()
export const shallowReadonlyMap = new WeakMap<Target, any>()
-const enum TargetType {
+enum TargetType {
INVALID = 0,
COMMON = 1,
COLLECTION = 2
} from '../component'
import { warn } from '../warning'
-export const enum DeprecationTypes {
+export enum DeprecationTypes {
GLOBAL_MOUNT = 'GLOBAL_MOUNT',
GLOBAL_MOUNT_CONTAINER = 'GLOBAL_MOUNT_CONTAINER',
GLOBAL_EXTEND = 'GLOBAL_EXTEND',
Defaults: Defaults
}
-const enum OptionTypes {
+enum OptionTypes {
PROPS = 'Props',
DATA = 'Data',
COMPUTED = 'Computed',
[K in keyof Pick<O, PublicOptionalKeys<O>>]?: InferPropType<O[K]>
}
-const enum BooleanFlags {
+enum BooleanFlags {
shouldCast,
shouldCastTrue
}
installCompatInstanceProperties(publicPropertiesMap)
}
-const enum AccessTypes {
+enum AccessTypes {
OTHER,
SETUP,
DATA,
hydrate: hydrateTeleport
}
-export const enum TeleportMoveTypes {
+export enum TeleportMoveTypes {
TARGET_CHANGE,
TOGGLE, // enable / disable
REORDER // moved in the main view
types: Record<string, string | Symbol>
}
-const enum DevtoolsHooks {
+enum DevtoolsHooks {
APP_INIT = 'app:init',
APP_UNMOUNT = 'app:unmount',
COMPONENT_UPDATED = 'component:updated',
-export const enum LifecycleHooks {
+export enum LifecycleHooks {
BEFORE_CREATE = 'bc',
CREATED = 'c',
BEFORE_MOUNT = 'bm',
// contexts where user provided function may be executed, in addition to
// lifecycle hooks.
-export const enum ErrorCodes {
+export enum ErrorCodes {
SETUP_FUNCTION,
RENDER_FUNCTION,
WATCH_GETTER,
container: (Element | ShadowRoot) & { _vnode?: VNode }
) => void
-const enum DOMNodeTypes {
+enum DOMNodeTypes {
ELEMENT = 1,
TEXT = 3,
COMMENT = 8
// 2.x COMPAT ------------------------------------------------------------------
-export { DeprecationTypes } from './compat/compatConfig'
+import { DeprecationTypes as _DeprecationTypes } from './compat/compatConfig'
export type { CompatVue } from './compat/global'
export type { LegacyConfig } from './compat/globalConfig'
export const compatUtils = (
__COMPAT__ ? _compatUtils : null
) as typeof _compatUtils
+
+export const DeprecationTypes = (
+ __COMPAT__ ? _DeprecationTypes : null
+) as typeof _DeprecationTypes
optimized: boolean
) => void
-export const enum MoveType {
+export enum MoveType {
ENTER,
LEAVE,
REORDER
import { markRaw } from '@vue/reactivity'
-export const enum TestNodeTypes {
+export enum TestNodeTypes {
TEXT = 'text',
ELEMENT = 'element',
COMMENT = 'comment'
}
-export const enum NodeOpTypes {
+export enum NodeOpTypes {
CREATE = 'create',
INSERT = 'insert',
REMOVE = 'remove',
* Check the `patchElement` function in '../../runtime-core/src/renderer.ts' to see how the
* flags are handled during diff.
*/
-export const enum PatchFlags {
+export enum PatchFlags {
/**
* Indicates an element with dynamic textContent (children fast path)
*/
-export const enum ShapeFlags {
+export enum ShapeFlags {
ELEMENT = 1,
FUNCTIONAL_COMPONENT = 1 << 1,
STATEFUL_COMPONENT = 1 << 2,
-export const enum SlotFlags {
+export enum SlotFlags {
/**
* Stable slots that only reference slot props or context state. The slot
* can fully capture its own dependencies so when passed down the parent won't
export declare const RefType: unique symbol
-export declare const enum RefTypes {
+export declare enum RefTypes {
Ref = 1,
ComputedRef = 2,
WritableComputedRef = 3
'@types/hash-sum':
specifier: ^1.0.2
version: 1.0.2
+ '@types/minimist':
+ specifier: ^1.2.5
+ version: 1.2.5
'@types/node':
specifier: ^20.10.0
version: 20.10.0
+ '@types/semver':
+ specifier: ^7.5.5
+ version: 7.5.5
'@typescript-eslint/parser':
specifier: ^6.13.0
version: 6.13.0(eslint@8.54.0)(typescript@5.2.2)
eslint:
specifier: ^8.54.0
version: 8.54.0
+ eslint-define-config:
+ specifier: ^1.24.1
+ version: 1.24.1
eslint-plugin-jest:
specifier: ^27.6.0
version: 27.6.0(eslint@8.54.0)(typescript@5.2.2)
resolution: {integrity: sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==}
dev: true
+ /@types/minimist@1.2.5:
+ resolution: {integrity: sha512-hov8bUuiLiyFPGyFPE1lwWhmzYbirOXQNNo40+y3zow8aFVTeyn3VWL0VFFfdNddA8S4Vf0Tc062rzyNr7Paag==}
+ dev: true
+
/@types/node@20.10.0:
resolution: {integrity: sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ==}
dependencies:
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
dev: true
- /@types/semver@7.5.4:
- resolution: {integrity: sha512-MMzuxN3GdFwskAnb6fz0orFvhfqi752yjaXylr0Rp4oDg5H0Zn1IuyRhDVvYOwAXoJirx2xuS16I3WjxnAIHiQ==}
+ /@types/semver@7.5.5:
+ resolution: {integrity: sha512-+d+WYC1BxJ6yVOgUgzK8gWvp5qF8ssV5r4nsDcZWKRWcDQLQ619tvWAxJQYGgBrO1MnLJC7a5GtiYsAoQ47dJg==}
dev: true
/@types/yauzl@2.10.2:
dependencies:
'@eslint-community/eslint-utils': 4.4.0(eslint@8.54.0)
'@types/json-schema': 7.0.14
- '@types/semver': 7.5.4
+ '@types/semver': 7.5.5
'@typescript-eslint/scope-manager': 5.62.0
'@typescript-eslint/types': 5.62.0
'@typescript-eslint/typescript-estree': 5.62.0(typescript@5.2.2)
source-map: 0.6.1
dev: true
+ /eslint-define-config@1.24.1:
+ resolution: {integrity: sha512-o36vBhPSWyIQlHoMqGhhcGmOOm2A2ccBVIdLTG/AWdm9YmjpsLpf+5ntf9LlHR6dduLREgxtGwvwPwSt7vnXJg==}
+ engines: {node: '>=18.0.0', npm: '>=9.0.0', pnpm: '>= 8.6.0'}
+ dev: true
+
/eslint-plugin-jest@27.6.0(eslint@8.54.0)(typescript@5.2.2):
resolution: {integrity: sha512-MTlusnnDMChbElsszJvrwD1dN3x6nZl//s4JD23BxB6MgR66TZlL064su24xEIS3VACfAoHV1vgyMgPw8nkdng==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
import esbuild from 'rollup-plugin-esbuild'
import alias from '@rollup/plugin-alias'
import { entries } from './scripts/aliases.js'
-import { constEnum } from './scripts/const-enum.js'
+import { inlineEnums } from './scripts/inline-enums.js'
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] = constEnum()
+const [enumPlugin, enumDefines] = inlineEnums()
const outputConfigs = {
'esm-bundler': {
? packages.filter(pkg => targets.includes(pkg))
: packages
-export default targetPackages.map(pkg => {
- return {
- input: `./temp/packages/${pkg}/src/index.d.ts`,
- output: {
- file: `packages/${pkg}/dist/${pkg}.d.ts`,
- format: 'es'
- },
- plugins: [dts(), patchTypes(pkg), ...(pkg === 'vue' ? [copyMts()] : [])],
- onwarn(warning, warn) {
- // during dts rollup, everything is externalized by default
- if (
- warning.code === 'UNRESOLVED_IMPORT' &&
- !warning.exporter.startsWith('.')
- ) {
- return
+export default targetPackages.map(
+ /** @returns {import('rollup').RollupOptions} */
+ pkg => {
+ return {
+ input: `./temp/packages/${pkg}/src/index.d.ts`,
+ output: {
+ file: `packages/${pkg}/dist/${pkg}.d.ts`,
+ format: 'es'
+ },
+ plugins: [dts(), patchTypes(pkg), ...(pkg === 'vue' ? [copyMts()] : [])],
+ onwarn(warning, warn) {
+ // during dts rollup, everything is externalized by default
+ if (
+ warning.code === 'UNRESOLVED_IMPORT' &&
+ !warning.exporter?.startsWith('.')
+ ) {
+ return
+ }
+ warn(warning)
}
- warn(warning)
}
}
-})
+)
/**
* Patch the dts generated by rollup-plugin-dts
* otherwise it gets weird in vitepress `defineComponent` call with
* "the inferred type cannot be named without a reference"
* 2. Append custom augmentations (jsx, macros)
+ *
+ * @param {string} pkg
* @returns {import('rollup').Plugin}
*/
function patchTypes(pkg) {
import { cpus } from 'node:os'
import { createRequire } from 'node:module'
import { targets as allTargets, fuzzyMatchTarget } from './utils.js'
-import { scanEnums } from './const-enum.js'
+import { scanEnums } from './inline-enums.js'
import prettyBytes from 'pretty-bytes'
const require = createRequire(import.meta.url)
+++ /dev/null
-// @ts-check
-
-/**
- * We use rollup-plugin-esbuild for faster builds, but esbuild in isolation
- * 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 { execaSync } from 'execa'
-import {
- existsSync,
- mkdirSync,
- readFileSync,
- rmSync,
- writeFileSync
-} from 'node:fs'
-import { parse } from '@babel/parser'
-import path from 'node:path'
-import MagicString from 'magic-string'
-
-const ENUM_CACHE_PATH = 'temp/enum.json'
-
-function evaluate(exp) {
- return new Function(`return ${exp}`)()
-}
-
-// this is called in the build script entry once
-// so the data can be shared across concurrent Rollup processes
-export function scanEnums() {
- /**
- * @type {{ ranges: Record<string, [number, number][]>, defines: Record<string, string>, ids: string[] }}
- */
- const enumData = {
- ranges: {},
- defines: {},
- ids: []
- }
-
- // 1. grep for files with exported const enum
- const { stdout } = execaSync('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
- if (!enumData.ids.includes(id)) {
- enumData.ids.push(id)
- }
- const key = e.id.type === 'Identifier' ? e.id.name : e.id.value
- const fullKey = `${id}.${key}`
- const saveValue = value => {
- if (fullKey in enumData.defines) {
- throw new Error(`name conflict for enum ${id} in ${file}`)
- }
- enumData.defines[fullKey] = JSON.stringify(value)
- }
- 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') {
- const resolveValue = node => {
- if (
- node.type === 'NumericLiteral' ||
- node.type === 'StringLiteral'
- ) {
- return node.value
- } else if (node.type === 'MemberExpression') {
- const exp = content.slice(node.start, node.end)
- if (!(exp in enumData.defines)) {
- throw new Error(
- `unhandled enum initialization expression ${exp} in ${file}`
- )
- }
- return enumData.defines[exp]
- } else {
- throw new Error(
- `unhandled BinaryExpression operand type ${node.type} in ${file}`
- )
- }
- }
- const exp = `${resolveValue(init.left)}${
- init.operator
- }${resolveValue(init.right)}`
- value = evaluate(exp)
- }
-
- if (init.type === 'UnaryExpression') {
- if (
- init.argument.type === 'StringLiteral' ||
- init.argument.type === 'NumericLiteral'
- ) {
- const exp = `${init.operator}${init.argument.value}`
- value = evaluate(exp)
- } else {
- throw new Error(
- `unhandled UnaryExpression argument type ${init.argument.type} in ${file}`
- )
- }
- }
-
- if (value === undefined) {
- throw new Error(
- `unhandled initializer type ${init.type} for ${fullKey} in ${file}`
- )
- }
- saveValue(value)
- lastInitialized = value
- } else {
- if (lastInitialized === undefined) {
- // first initialized
- saveValue((lastInitialized = 0))
- } else if (typeof lastInitialized === 'number') {
- saveValue(++lastInitialized)
- } else {
- // should not happen
- throw new Error(`wrong enum initialization sequence in ${file}`)
- }
- }
- }
- }
- }
- }
-
- // 3. save cache
- if (!existsSync('temp')) mkdirSync('temp')
- writeFileSync(ENUM_CACHE_PATH, JSON.stringify(enumData))
-
- return () => {
- rmSync(ENUM_CACHE_PATH, { force: true })
- }
-}
-
-/**
- * @returns {[import('rollup').Plugin, Record<string, string>]}
- */
-export function constEnum() {
- if (!existsSync(ENUM_CACHE_PATH)) {
- throw new Error('enum cache needs to be initialized before creating plugin')
- }
- /**
- * @type {{ ranges: Record<string, [number, number][]>, defines: Record<string, string>, ids: string[] }}
- */
- const enumData = JSON.parse(readFileSync(ENUM_CACHE_PATH, 'utf-8'))
-
- // construct a regex for matching re-exports of known const enums
- const reExportsRE = new RegExp(
- `export {[^}]*?\\b(${enumData.ids.join('|')})\\b[^]*?}`
- )
-
- // 3. during transform:
- // 3.1 files w/ const enum declaration: remove declaration
- // 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' &&
- enumData.ids.includes(spec.local.name)
- ) {
- const next = node.specifiers[i + 1]
- if (next) {
- // @ts-ignore
- s.remove(spec.start, next.start)
- } else {
- // last one
- const prev = node.specifiers[i - 1]
- // @ts-ignore
- s.remove(prev ? prev.end : spec.start, spec.end)
- }
- }
- }
- }
- }
- }
-
- if (s) {
- return {
- code: s.toString(),
- map: s.generateMap()
- }
- }
- }
- }
-
- return [plugin, enumData.defines]
-}
// resolve externals
// TODO this logic is largely duplicated from rollup.config.js
+/** @type {string[]} */
let external = []
if (!inlineDeps) {
// cjs & esm-bundler: external all deps
]
}
}
-
+/** @type {Array<import('esbuild').Plugin>} */
const plugins = [
{
name: 'log-rebuild',
--- /dev/null
+// @ts-check
+
+/**
+ * We used const enums before, but it caused some issues: #1228, so we
+ * switched to regular enums. But we still want to keep the zero-cost benefit
+ * of const enums, and minimize the impact on bundle size as much as possible.
+ *
+ * Here we pre-process all the enums in the project and turn them into
+ * global replacements, and rewrite the original declarations as object literals.
+ *
+ * This file is expected to be executed with project root as cwd.
+ */
+
+import * as assert from 'node:assert'
+import {
+ existsSync,
+ mkdirSync,
+ readFileSync,
+ rmSync,
+ writeFileSync
+} from 'node:fs'
+import * as path from 'node:path'
+import { parse } from '@babel/parser'
+import { execaSync } from 'execa'
+import MagicString from 'magic-string'
+
+/**
+ * @typedef {{ readonly name: string, readonly value: string | number }} EnumMember
+ * @typedef {{ readonly id: string, readonly range: readonly [start: number, end: number], readonly members: ReadonlyArray<EnumMember>}} EnumDeclaration
+ * @typedef {{ readonly declarations: { readonly [file: string] : ReadonlyArray<EnumDeclaration>}, readonly defines: { readonly [ id_key: `${string}.${string}`]: string } }} EnumData
+ */
+
+const ENUM_CACHE_PATH = 'temp/enum.json'
+
+/**
+ * @param {string} exp
+ * @returns {string | number}
+ */
+function evaluate(exp) {
+ return new Function(`return ${exp}`)()
+}
+
+// this is called in the build script entry once
+// so the data can be shared across concurrent Rollup processes
+export function scanEnums() {
+ /** @type {{ [file: string]: EnumDeclaration[] }} */
+ const declarations = Object.create(null)
+ /** @type {{ [id_key: `${string}.${string}`]: string; }} */
+ const defines = Object.create(null)
+
+ // 1. grep for files with exported enum
+ const { stdout } = execaSync('git', ['grep', `export 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'
+ })
+
+ /** @type {Set<string>} */
+ const enumIds = new Set()
+ for (const node of ast.program.body) {
+ if (
+ node.type === 'ExportNamedDeclaration' &&
+ node.declaration &&
+ node.declaration.type === 'TSEnumDeclaration'
+ ) {
+ const decl = node.declaration
+ const id = decl.id.name
+ if (enumIds.has(id)) {
+ throw new Error(
+ `not support declaration merging for enum ${id} in ${file}`
+ )
+ }
+ enumIds.add(id)
+ /** @type {string | number | undefined} */
+ let lastInitialized
+ /** @type {Array<EnumMember>} */
+ const members = []
+
+ for (let i = 0; i < decl.members.length; i++) {
+ const e = decl.members[i]
+ const key = e.id.type === 'Identifier' ? e.id.name : e.id.value
+ const fullKey = /** @type {const} */ (`${id}.${key}`)
+ const saveValue = (/** @type {string | number} */ value) => {
+ // We need allow same name enum in different file.
+ // For example: enum ErrorCodes exist in both @vue/compiler-core and @vue/runtime-core
+ // But not allow `ErrorCodes.__EXTEND_POINT__` appear in two same name enum
+ if (fullKey in defines) {
+ throw new Error(`name conflict for enum ${id} in ${file}`)
+ }
+ members.push({
+ name: key,
+ value
+ })
+ defines[fullKey] = JSON.stringify(value)
+ }
+ const init = e.initializer
+ if (init) {
+ /** @type {string | number} */
+ let value
+ if (
+ init.type === 'StringLiteral' ||
+ init.type === 'NumericLiteral'
+ ) {
+ value = init.value
+ }
+ // e.g. 1 << 2
+ else if (init.type === 'BinaryExpression') {
+ const resolveValue = (
+ /** @type {import('@babel/types').Expression | import('@babel/types').PrivateName} */ node
+ ) => {
+ assert.ok(typeof node.start === 'number')
+ assert.ok(typeof node.end === 'number')
+ if (
+ node.type === 'NumericLiteral' ||
+ node.type === 'StringLiteral'
+ ) {
+ return node.value
+ } else if (node.type === 'MemberExpression') {
+ const exp = /** @type {`${string}.${string}`} */ (
+ content.slice(node.start, node.end)
+ )
+ if (!(exp in defines)) {
+ throw new Error(
+ `unhandled enum initialization expression ${exp} in ${file}`
+ )
+ }
+ return defines[exp]
+ } else {
+ throw new Error(
+ `unhandled BinaryExpression operand type ${node.type} in ${file}`
+ )
+ }
+ }
+ const exp = `${resolveValue(init.left)}${
+ init.operator
+ }${resolveValue(init.right)}`
+ value = evaluate(exp)
+ } else if (init.type === 'UnaryExpression') {
+ if (
+ init.argument.type === 'StringLiteral' ||
+ init.argument.type === 'NumericLiteral'
+ ) {
+ const exp = `${init.operator}${init.argument.value}`
+ value = evaluate(exp)
+ } else {
+ throw new Error(
+ `unhandled UnaryExpression argument type ${init.argument.type} in ${file}`
+ )
+ }
+ } else {
+ throw new Error(
+ `unhandled initializer type ${init.type} for ${fullKey} in ${file}`
+ )
+ }
+ lastInitialized = value
+ saveValue(lastInitialized)
+ } else {
+ if (lastInitialized === undefined) {
+ // first initialized
+ lastInitialized = 0
+ saveValue(lastInitialized)
+ } else if (typeof lastInitialized === 'number') {
+ lastInitialized++
+ saveValue(lastInitialized)
+ } else {
+ // should not happen
+ throw new Error(`wrong enum initialization sequence in ${file}`)
+ }
+ }
+ }
+
+ if (!(file in declarations)) {
+ declarations[file] = []
+ }
+ assert.ok(typeof node.start === 'number')
+ assert.ok(typeof node.end === 'number')
+ declarations[file].push({
+ id,
+ range: [node.start, node.end],
+ members
+ })
+ }
+ }
+ }
+
+ // 3. save cache
+ if (!existsSync('temp')) mkdirSync('temp')
+
+ /** @type {EnumData} */
+ const enumData = {
+ declarations,
+ defines
+ }
+
+ writeFileSync(ENUM_CACHE_PATH, JSON.stringify(enumData))
+
+ return () => {
+ rmSync(ENUM_CACHE_PATH, { force: true })
+ }
+}
+
+/**
+ * @returns {[import('rollup').Plugin, Record<string, string>]}
+ */
+export function inlineEnums() {
+ if (!existsSync(ENUM_CACHE_PATH)) {
+ throw new Error('enum cache needs to be initialized before creating plugin')
+ }
+ /**
+ * @type {EnumData}
+ */
+ const enumData = JSON.parse(readFileSync(ENUM_CACHE_PATH, 'utf-8'))
+
+ // 3. during transform:
+ // 3.1 files w/ enum declaration: rewrite declaration as object literal
+ // 3.2 files using enum: inject into esbuild define
+ /**
+ * @type {import('rollup').Plugin}
+ */
+ const plugin = {
+ name: 'inline-enum',
+ transform(code, id) {
+ /**
+ * @type {MagicString | undefined}
+ */
+ let s
+
+ if (id in enumData.declarations) {
+ s = s || new MagicString(code)
+ for (const declaration of enumData.declarations[id]) {
+ const {
+ range: [start, end],
+ id,
+ members
+ } = declaration
+ s.update(
+ start,
+ end,
+ `export const ${id} = {${members
+ .flatMap(({ name, value }) => {
+ const forwardMapping =
+ JSON.stringify(name) + ': ' + JSON.stringify(value)
+ const reverseMapping =
+ JSON.stringify(value.toString()) + ': ' + JSON.stringify(name)
+
+ // see https://www.typescriptlang.org/docs/handbook/enums.html#reverse-mappings
+ return typeof value === 'string'
+ ? [
+ forwardMapping
+ // string enum members do not get a reverse mapping generated at all
+ ]
+ : [
+ forwardMapping,
+ // other enum members should support enum reverse mapping
+ reverseMapping
+ ]
+ })
+ .join(',\n')}}`
+ )
+ }
+ }
+
+ if (s) {
+ return {
+ code: s.toString(),
+ map: s.generateMap()
+ }
+ }
+ }
+ }
+
+ return [plugin, enumData.defines]
+}
"packages/runtime-test",
"packages/template-explorer",
"packages/sfc-playground",
- "packages/dts-test"
+ "packages/dts-test",
+ "rollup.config.js",
+ "scripts/*"
]
}
"useDefineForClassFields": false,
"module": "esnext",
"moduleResolution": "bundler",
- "allowJs": false,
+ "allowJs": true,
"strict": true,
"noUnusedLocals": true,
"experimentalDecorators": true,
"packages/*/__tests__",
"packages/dts-test",
"packages/vue/jsx-runtime",
- "scripts/setupVitest.ts"
- ]
+ "scripts/*",
+ "rollup.*.js"
+ ],
+ "exclude": ["rollup.config.js", "scripts/*"]
}