Skip to content

feat: use enum to replace const enum #9261

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Nov 29, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions .eslintrc.cjs
Original file line number Diff line number Diff line change
@@ -3,6 +3,15 @@
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: {
@@ -16,6 +25,7 @@ module.exports = {

'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',
@@ -55,15 +65,15 @@ module.exports = {
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
@@ -79,7 +89,7 @@ module.exports = {
files: ['scripts/**', '*.{js,ts}', 'packages/**/index.js'],
rules: {
'no-restricted-globals': 'off',
'no-restricted-syntax': 'off'
'no-restricted-syntax': ['error', banConstEnum]
}
}
]
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -66,7 +66,9 @@
"@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",
@@ -75,6 +77,7 @@
"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",
8 changes: 4 additions & 4 deletions packages/compiler-core/src/ast.ts
Original file line number Diff line number Diff line change
@@ -19,13 +19,13 @@ import { ImportItem, TransformContext } from './transform'
// 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,
@@ -59,7 +59,7 @@ export const enum NodeTypes {
JS_RETURN_STATEMENT
}

export const enum ElementTypes {
export enum ElementTypes {
ELEMENT,
COMPONENT,
SLOT,
@@ -214,7 +214,7 @@ export interface DirectiveNode extends Node {
* 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,
2 changes: 1 addition & 1 deletion packages/compiler-core/src/codegen.ts
Original file line number Diff line number Diff line change
@@ -69,7 +69,7 @@ export interface CodegenResult {
map?: RawSourceMap
}

const enum NewlineType {
enum NewlineType {
Start = 0,
End = -1,
None = -2,
2 changes: 1 addition & 1 deletion packages/compiler-core/src/compat/compatConfig.ts
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@ export interface CompilerCompatOptions {
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',
2 changes: 1 addition & 1 deletion packages/compiler-core/src/errors.ts
Original file line number Diff line number Diff line change
@@ -37,7 +37,7 @@ export function createCompilerError<T extends number>(
return error
}

export const enum ErrorCodes {
export enum ErrorCodes {
// parse errors
ABRUPT_CLOSING_OF_EMPTY_COMMENT,
CDATA_IN_HTML_CONTENT,
2 changes: 1 addition & 1 deletion packages/compiler-core/src/options.ts
Original file line number Diff line number Diff line change
@@ -94,7 +94,7 @@ export type HoistTransform = (
parent: ParentNode
) => void

export const enum BindingTypes {
export enum BindingTypes {
/**
* returned from data()
*/
9 changes: 4 additions & 5 deletions packages/compiler-core/src/tokenizer.ts
Original file line number Diff line number Diff line change
@@ -38,13 +38,13 @@ import {
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"
@@ -72,7 +72,6 @@ export const enum CharCodes {
UpperZ = 0x5a, // "Z"
LowerZ = 0x7a, // "z"
LowerX = 0x78, // "x"
OpeningSquareBracket = 0x5b, // "["
LowerV = 0x76, // "v"
Dot = 0x2e, // "."
Colon = 0x3a, // ":"
@@ -85,7 +84,7 @@ const defaultDelimitersOpen = new Uint8Array([123, 123]) // "{{"
const defaultDelimitersClose = new Uint8Array([125, 125]) // "}}"

/** All the states the tokenizer can be in. */
export const enum State {
export enum State {
Text = 1,

// interpolation
@@ -820,7 +819,7 @@ export default class Tokenizer {
}
}
private stateBeforeDeclaration(c: number): void {
if (c === CharCodes.OpeningSquareBracket) {
if (c === CharCodes.LeftSqaure) {
this.state = State.CDATASequence
this.sequenceIndex = 0
} else {
2 changes: 1 addition & 1 deletion packages/compiler-core/src/utils.ts
Original file line number Diff line number Diff line change
@@ -65,7 +65,7 @@ const nonIdentifierRE = /^\d|[^\$\w]/
export const isSimpleIdentifier = (name: string): boolean =>
!nonIdentifierRE.test(name)

const enum MemberExpLexState {
enum MemberExpLexState {
inMemberExp,
inBrackets,
inParens,
4 changes: 2 additions & 2 deletions packages/compiler-dom/src/errors.ts
Original file line number Diff line number Diff line change
@@ -20,7 +20,7 @@ export function createDOMCompilerError(
) 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,
@@ -36,7 +36,7 @@ export const enum DOMErrorCodes {
}

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__) {
2 changes: 1 addition & 1 deletion packages/compiler-dom/src/transforms/stringifyStatic.ts
Original file line number Diff line number Diff line change
@@ -33,7 +33,7 @@ import {
isBooleanAttr
} from '@vue/shared'

export const enum StringifyThresholds {
export enum StringifyThresholds {
ELEMENT_WITH_BINDING_COUNT = 5,
NODE_COUNT = 20
}
2 changes: 1 addition & 1 deletion packages/compiler-sfc/src/style/cssVars.ts
Original file line number Diff line number Diff line change
@@ -70,7 +70,7 @@ export function parseCssVars(sfc: SFCDescriptor): string[] {
return vars
}

const enum LexerState {
enum LexerState {
inParens,
inSingleQuoteString,
inDoubleQuoteString
4 changes: 2 additions & 2 deletions packages/compiler-ssr/src/errors.ts
Original file line number Diff line number Diff line change
@@ -16,14 +16,14 @@ export function createSSRCompilerError(
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__) {
8 changes: 4 additions & 4 deletions packages/reactivity/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
// 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',
IS_SHALLOW = '__v_isShallow',
RAW = '__v_raw'
}

export const enum DirtyLevels {
export enum DirtyLevels {
NotDirty = 0,
ComputedValueMaybeDirty = 1,
ComputedValueDirty = 2,
6 changes: 1 addition & 5 deletions packages/reactivity/src/index.ts
Original file line number Diff line number Diff line change
@@ -68,8 +68,4 @@ export {
getCurrentScope,
onScopeDispose
} from './effectScope'
export {
TrackOpTypes /* @remove */,
TriggerOpTypes /* @remove */,
ReactiveFlags /* @remove */
} from './constants'
export { TrackOpTypes, TriggerOpTypes, ReactiveFlags } from './constants'
2 changes: 1 addition & 1 deletion packages/reactivity/src/reactive.ts
Original file line number Diff line number Diff line change
@@ -27,7 +27,7 @@ export const shallowReactiveMap = new WeakMap<Target, any>()
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
2 changes: 1 addition & 1 deletion packages/runtime-core/src/compat/compatConfig.ts
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ import {
} 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',
2 changes: 1 addition & 1 deletion packages/runtime-core/src/componentOptions.ts
Original file line number Diff line number Diff line change
@@ -586,7 +586,7 @@ export type OptionTypesType<
Defaults: Defaults
}

const enum OptionTypes {
enum OptionTypes {
PROPS = 'Props',
DATA = 'Data',
COMPUTED = 'Computed',
2 changes: 1 addition & 1 deletion packages/runtime-core/src/componentProps.ts
Original file line number Diff line number Diff line change
@@ -164,7 +164,7 @@ export type ExtractPublicPropTypes<O> = {
[K in keyof Pick<O, PublicOptionalKeys<O>>]?: InferPropType<O[K]>
}

const enum BooleanFlags {
enum BooleanFlags {
shouldCast,
shouldCastTrue
}
2 changes: 1 addition & 1 deletion packages/runtime-core/src/componentPublicInstance.ts
Original file line number Diff line number Diff line change
@@ -281,7 +281,7 @@ if (__COMPAT__) {
installCompatInstanceProperties(publicPropertiesMap)
}

const enum AccessTypes {
enum AccessTypes {
OTHER,
SETUP,
DATA,
2 changes: 1 addition & 1 deletion packages/runtime-core/src/components/Teleport.ts
Original file line number Diff line number Diff line change
@@ -269,7 +269,7 @@ export const TeleportImpl = {
hydrate: hydrateTeleport
}

export const enum TeleportMoveTypes {
export enum TeleportMoveTypes {
TARGET_CHANGE,
TOGGLE, // enable / disable
REORDER // moved in the main view
2 changes: 1 addition & 1 deletion packages/runtime-core/src/devtools.ts
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ interface AppRecord {
types: Record<string, string | Symbol>
}

const enum DevtoolsHooks {
enum DevtoolsHooks {
APP_INIT = 'app:init',
APP_UNMOUNT = 'app:unmount',
COMPONENT_UPDATED = 'component:updated',
2 changes: 1 addition & 1 deletion packages/runtime-core/src/enums.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const enum LifecycleHooks {
export enum LifecycleHooks {
BEFORE_CREATE = 'bc',
CREATED = 'c',
BEFORE_MOUNT = 'bm',
2 changes: 1 addition & 1 deletion packages/runtime-core/src/errorHandling.ts
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@ import { LifecycleHooks } from './enums'

// 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,
2 changes: 1 addition & 1 deletion packages/runtime-core/src/hydration.ts
Original file line number Diff line number Diff line change
@@ -30,7 +30,7 @@ export type RootHydrateFunction = (
container: (Element | ShadowRoot) & { _vnode?: VNode }
) => void

const enum DOMNodeTypes {
enum DOMNodeTypes {
ELEMENT = 1,
TEXT = 3,
COMMENT = 8
6 changes: 5 additions & 1 deletion packages/runtime-core/src/index.ts
Original file line number Diff line number Diff line change
@@ -361,7 +361,7 @@ export const ssrUtils = (__SSR__ ? _ssrUtils : null) as typeof _ssrUtils

// 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'

@@ -393,3 +393,7 @@ const _compatUtils = {
export const compatUtils = (
__COMPAT__ ? _compatUtils : null
) as typeof _compatUtils

export const DeprecationTypes = (
__COMPAT__ ? _DeprecationTypes : null
) as typeof _DeprecationTypes
2 changes: 1 addition & 1 deletion packages/runtime-core/src/renderer.ts
Original file line number Diff line number Diff line change
@@ -265,7 +265,7 @@ export type SetupRenderEffectFn = (
optimized: boolean
) => void

export const enum MoveType {
export enum MoveType {
ENTER,
LEAVE,
REORDER
4 changes: 2 additions & 2 deletions packages/runtime-test/src/nodeOps.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
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',
2 changes: 1 addition & 1 deletion packages/shared/src/patchFlags.ts
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@
* 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)
*/
2 changes: 1 addition & 1 deletion packages/shared/src/shapeFlags.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const enum ShapeFlags {
export enum ShapeFlags {
ELEMENT = 1,
FUNCTIONAL_COMPONENT = 1 << 1,
STATEFUL_COMPONENT = 1 << 2,
2 changes: 1 addition & 1 deletion packages/shared/src/slotFlags.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
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
2 changes: 1 addition & 1 deletion packages/vue/macros.d.ts
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ import {

export declare const RefType: unique symbol

export declare const enum RefTypes {
export declare enum RefTypes {
Ref = 1,
ComputedRef = 2,
WritableComputedRef = 3
24 changes: 21 additions & 3 deletions pnpm-lock.yaml
4 changes: 2 additions & 2 deletions rollup.config.js
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ import terser from '@rollup/plugin-terser'
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.')
@@ -32,7 +32,7 @@ const pkg = require(resolve(`package.json`))
const packageOptions = pkg.buildOptions || {}
const name = packageOptions.filename || path.basename(packageDir)

const [enumPlugin, enumDefines] = constEnum()
const [enumPlugin, enumDefines] = inlineEnums()

const outputConfigs = {
'esm-bundler': {
39 changes: 22 additions & 17 deletions rollup.dts.config.js
Original file line number Diff line number Diff line change
@@ -17,26 +17,29 @@ const targetPackages = targets
? 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
@@ -45,6 +48,8 @@ export default targetPackages.map(pkg => {
* 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) {
2 changes: 1 addition & 1 deletion scripts/build.js
Original file line number Diff line number Diff line change
@@ -26,7 +26,7 @@ import { execa, execaSync } from 'execa'
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)
255 changes: 0 additions & 255 deletions scripts/const-enum.js

This file was deleted.

3 changes: 2 additions & 1 deletion scripts/dev.js
Original file line number Diff line number Diff line change
@@ -41,6 +41,7 @@ const relativeOutfile = relative(process.cwd(), outfile)

// resolve externals
// TODO this logic is largely duplicated from rollup.config.js
/** @type {string[]} */
let external = []
if (!inlineDeps) {
// cjs & esm-bundler: external all deps
@@ -80,7 +81,7 @@ if (!inlineDeps) {
]
}
}

/** @type {Array<import('esbuild').Plugin>} */
const plugins = [
{
name: 'log-rebuild',
279 changes: 279 additions & 0 deletions scripts/inline-enums.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
// @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]
}
4 changes: 3 additions & 1 deletion tsconfig.build.json
Original file line number Diff line number Diff line change
@@ -10,6 +10,8 @@
"packages/runtime-test",
"packages/template-explorer",
"packages/sfc-playground",
"packages/dts-test"
"packages/dts-test",
"rollup.config.js",
"scripts/*"
]
}
8 changes: 5 additions & 3 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -8,7 +8,7 @@
"useDefineForClassFields": false,
"module": "esnext",
"moduleResolution": "bundler",
"allowJs": false,
"allowJs": true,
"strict": true,
"noUnusedLocals": true,
"experimentalDecorators": true,
@@ -34,6 +34,8 @@
"packages/*/__tests__",
"packages/dts-test",
"packages/vue/jsx-runtime",
"scripts/setupVitest.ts"
]
"scripts/*",
"rollup.*.js"
],
"exclude": ["rollup.config.js", "scripts/*"]
}